mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-15 01:21:48 +03:00
feat: add public brain page (#1230)
* feat: add brain library button * feat(Field): add inputClassName key * feat: add GET brains/public route * feat: add brains library layout * feat: add brain subscriber count
This commit is contained in:
parent
f74a1c50c8
commit
8172704b61
@ -33,3 +33,10 @@ class MinimalBrainEntity(BaseModel):
|
||||
name: str
|
||||
rights: RoleEnum
|
||||
status: str
|
||||
|
||||
|
||||
class PublicBrain(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
description: Optional[str]
|
||||
number_of_subscribers: int = 0
|
||||
|
@ -2,7 +2,7 @@ from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from logger import get_logger
|
||||
from models.brain_entity import BrainEntity, MinimalBrainEntity
|
||||
from models.brain_entity import BrainEntity, MinimalBrainEntity, PublicBrain
|
||||
from models.databases.repository import Repository
|
||||
from pydantic import BaseModel
|
||||
|
||||
@ -75,6 +75,24 @@ class Brain(Repository):
|
||||
user_brains[-1].rights = item["rights"]
|
||||
return user_brains
|
||||
|
||||
def get_public_brains(self) -> list[PublicBrain]:
|
||||
response = (
|
||||
self.db.from_("brains")
|
||||
.select("id:brain_id, name, description")
|
||||
.filter("status", "eq", "public")
|
||||
.execute()
|
||||
)
|
||||
public_brains: list[PublicBrain] = []
|
||||
for item in response.data:
|
||||
brain = PublicBrain(
|
||||
id=item["id"],
|
||||
name=item["name"],
|
||||
description=item["description"],
|
||||
)
|
||||
brain.number_of_subscribers = self.get_brain_subscribers_count(brain.id)
|
||||
public_brains.append(brain)
|
||||
return public_brains
|
||||
|
||||
def get_brain_for_user(self, user_id, brain_id) -> MinimalBrainEntity | None:
|
||||
response = (
|
||||
self.db.from_("brains_users")
|
||||
@ -274,3 +292,16 @@ class Brain(Repository):
|
||||
return None
|
||||
|
||||
return BrainEntity(**response[0])
|
||||
|
||||
def get_brain_subscribers_count(self, brain_id: UUID) -> int:
|
||||
response = (
|
||||
self.db.from_("brains_users")
|
||||
.select(
|
||||
"count",
|
||||
)
|
||||
.filter("brain_id", "eq", str(brain_id))
|
||||
.execute()
|
||||
).data
|
||||
if len(response) == 0:
|
||||
raise ValueError(f"Brain with id {brain_id} does not exist.")
|
||||
return response[0]["count"]
|
||||
|
@ -1,5 +1,5 @@
|
||||
from models.databases.supabase.brains import CreateBrainProperties
|
||||
from models import BrainEntity, get_supabase_db
|
||||
from models.databases.supabase.brains import CreateBrainProperties
|
||||
|
||||
|
||||
def create_brain(brain: CreateBrainProperties) -> BrainEntity:
|
||||
|
7
backend/repository/brain/get_public_brains.py
Normal file
7
backend/repository/brain/get_public_brains.py
Normal file
@ -0,0 +1,7 @@
|
||||
from models import get_supabase_db
|
||||
from models.brain_entity import PublicBrain
|
||||
|
||||
|
||||
def get_public_brains() -> list[PublicBrain]:
|
||||
supabase_db = get_supabase_db()
|
||||
return supabase_db.get_public_brains()
|
@ -4,6 +4,7 @@ from auth import AuthBearer, get_current_user
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from logger import get_logger
|
||||
from models import UserIdentity, UserUsage
|
||||
from models.brain_entity import PublicBrain
|
||||
from models.databases.supabase.brains import (
|
||||
BrainQuestionRequest,
|
||||
BrainUpdatableProperties,
|
||||
@ -20,6 +21,7 @@ from repository.brain import (
|
||||
set_as_default_brain_for_user,
|
||||
update_brain_by_id,
|
||||
)
|
||||
from repository.brain.get_public_brains import get_public_brains
|
||||
from repository.prompt import delete_prompt_by_id, get_prompt_by_id
|
||||
|
||||
from routes.authorizations.brain_authorization import has_brain_authorization
|
||||
@ -48,6 +50,16 @@ async def brain_endpoint(
|
||||
return {"brains": brains}
|
||||
|
||||
|
||||
@brain_router.get(
|
||||
"/brains/public", dependencies=[Depends(AuthBearer())], tags=["Brain"]
|
||||
)
|
||||
async def public_brains_endpoint() -> list[PublicBrain]:
|
||||
"""
|
||||
Retrieve all Quivr public brains
|
||||
"""
|
||||
return get_public_brains()
|
||||
|
||||
|
||||
# get default brain
|
||||
@brain_router.get(
|
||||
"/brains/default/", dependencies=[Depends(AuthBearer())], tags=["Brain"]
|
||||
|
@ -31,7 +31,7 @@ export const BrainListItem = ({ brain }: BrainsListItemProps): JSX.Element => {
|
||||
href={`/brains-management/${brain.id}`}
|
||||
key={brain.id}
|
||||
>
|
||||
<div className="flex flex-row flex-1">
|
||||
<div className="flex flex-row flex-1 w-max">
|
||||
<div className="flex items-center gap-2">
|
||||
<FaBrain className="text-xl" />
|
||||
<p>{brain.name}</p>
|
||||
|
@ -1,8 +1,11 @@
|
||||
"use client";
|
||||
import { motion, MotionConfig } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MdChevronRight } from "react-icons/md";
|
||||
|
||||
import { AddBrainModal } from "@/lib/components/AddBrainModal/AddBrainModal";
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { BrainListItem } from "./BrainListItem";
|
||||
@ -13,6 +16,8 @@ export const BrainsList = (): JSX.Element => {
|
||||
const { open, setOpen, searchQuery, setSearchQuery, brains } =
|
||||
useBrainsList();
|
||||
|
||||
const { t } = useTranslation("brain");
|
||||
|
||||
return (
|
||||
<MotionConfig transition={{ massq: 1, damping: 10 }}>
|
||||
<motion.div
|
||||
@ -53,7 +58,18 @@ export const BrainsList = (): JSX.Element => {
|
||||
))}
|
||||
</div>
|
||||
<div className="m-2 mb flex flex-col">
|
||||
<AddBrainModal />
|
||||
<Link
|
||||
href="/brains-management/library"
|
||||
className="flex flex-row flex-1"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
className="bg-purple-600 text-white py-2 mb-2 flex flex-row flex-1"
|
||||
>
|
||||
{t("brain_library_button_label")}
|
||||
</Button>
|
||||
</Link>
|
||||
<AddBrainModal triggerClassName="border-solid border-2 border-gray-300" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
@ -36,10 +36,14 @@ export const useBrainsList = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentBrainId !== null) {
|
||||
if (
|
||||
currentBrainId !== null &&
|
||||
pathname !== null &&
|
||||
!pathname.includes("library")
|
||||
) {
|
||||
redirect(`/brains-management/${currentBrainId}`);
|
||||
}
|
||||
}, [currentBrainId]);
|
||||
}, [brainId, currentBrainId, pathname]);
|
||||
|
||||
return {
|
||||
open,
|
||||
|
@ -0,0 +1,26 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MdAdd } from "react-icons/md";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { Brain } from "@/lib/context/BrainProvider/types";
|
||||
|
||||
type PublicBrainItemProps = {
|
||||
brain: Brain;
|
||||
};
|
||||
|
||||
export const PublicBrainItem = ({
|
||||
brain,
|
||||
}: PublicBrainItemProps): JSX.Element => {
|
||||
const { t } = useTranslation("brain");
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center 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 md:p-5">
|
||||
<p className="font-bold mb-5 text-xl">{brain.name}</p>
|
||||
<p className="line-clamp-2 text-center px-5">{brain.description ?? ""}</p>
|
||||
<Button className="bg-purple-600 text-white p-0 px-3 rounded-xl border-0 w-content mt-3">
|
||||
{t("public_brain_subscribe_button_label")}
|
||||
<MdAdd className="text-md" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { PUBLIC_BRAINS_KEY } from "@/lib/api/brain/config";
|
||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||
import { Brain } from "@/lib/context/BrainProvider/types";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useBrainsLibrary = () => {
|
||||
const [searchBarText, setSearchBarText] = useState("");
|
||||
const { getPublicBrains } = useBrainApi();
|
||||
const { data: publicBrains = [] } = useQuery({
|
||||
queryKey: [PUBLIC_BRAINS_KEY],
|
||||
queryFn: getPublicBrains,
|
||||
});
|
||||
|
||||
const [displayingPublicBrains, setDisplayingPublicBrains] = useState<Brain[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayingPublicBrains(publicBrains);
|
||||
}, [publicBrains]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchBarText === "") {
|
||||
setDisplayingPublicBrains(publicBrains);
|
||||
|
||||
return;
|
||||
}
|
||||
setDisplayingPublicBrains(
|
||||
publicBrains.filter((brain) =>
|
||||
brain.name.toLowerCase().includes(searchBarText.toLowerCase())
|
||||
)
|
||||
);
|
||||
}, [publicBrains, searchBarText]);
|
||||
|
||||
return {
|
||||
displayingPublicBrains,
|
||||
searchBarText,
|
||||
setSearchBarText,
|
||||
};
|
||||
};
|
38
frontend/app/brains-management/library/page.tsx
Normal file
38
frontend/app/brains-management/library/page.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
|
||||
import { PublicBrainItem } from "./components/PublicBrainItem";
|
||||
import { useBrainsLibrary } from "./hooks/useBrainsLibrary";
|
||||
|
||||
const BrainsLibrary = (): JSX.Element => {
|
||||
const { displayingPublicBrains, searchBarText, setSearchBarText } =
|
||||
useBrainsLibrary();
|
||||
const { t } = useTranslation("brain");
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center m-20 border-2 border-gray-100 border-solid rounded-xl">
|
||||
<div className="flex">
|
||||
<Field
|
||||
value={searchBarText}
|
||||
onChange={(e) => setSearchBarText(e.target.value)}
|
||||
name="search"
|
||||
inputClassName="w-max lg:min-w-[300px] md:min-w-[200px] min-w-[100px] mt-10 rounded-3xl bg-white"
|
||||
placeholder={t("public_brains_search_bar_placeholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-stretch w-full">
|
||||
{displayingPublicBrains.map((brain) => (
|
||||
<div key={brain.id} className="lg:w-1/3 md:w-1/2 w-1 md:p-5">
|
||||
<PublicBrainItem brain={brain} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrainsLibrary;
|
@ -226,4 +226,14 @@ describe("useBrainApi", () => {
|
||||
expect(axiosPutMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosPutMock).toHaveBeenCalledWith(`/brains/${brainId}/`, brain);
|
||||
});
|
||||
it("should call getPublicBrains with correct parameters", async () => {
|
||||
const {
|
||||
result: {
|
||||
current: { getPublicBrains },
|
||||
},
|
||||
} = renderHook(() => useBrainApi());
|
||||
await getPublicBrains();
|
||||
expect(axiosGetMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosGetMock).toHaveBeenCalledWith(`/brains/public`);
|
||||
});
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
BackendMinimalBrainForUser,
|
||||
Brain,
|
||||
MinimalBrainForUser,
|
||||
PublicBrain,
|
||||
} from "@/lib/context/BrainProvider/types";
|
||||
import { Document } from "@/lib/types/Document";
|
||||
|
||||
@ -136,3 +137,9 @@ export const updateBrain = async (
|
||||
): Promise<void> => {
|
||||
await axiosInstance.put(`/brains/${brainId}/`, brain);
|
||||
};
|
||||
|
||||
export const getPublicBrains = async (
|
||||
axiosInstance: AxiosInstance
|
||||
): Promise<PublicBrain[]> => {
|
||||
return (await axiosInstance.get<PublicBrain[]>(`/brains/public`)).data;
|
||||
};
|
||||
|
@ -1,7 +1,9 @@
|
||||
const brainDataKey = "quivr-brains";
|
||||
const BRAIN_DATA_KEY = "quivr-brains";
|
||||
|
||||
export const getBrainDataKey = (brainId: string): string =>
|
||||
`${brainDataKey}-${brainId}`;
|
||||
`${BRAIN_DATA_KEY}-${brainId}`;
|
||||
|
||||
export const getBrainKnowledgeDataKey = (brainId: string): string =>
|
||||
`${brainDataKey}-${brainId}-knowledge`;
|
||||
`${BRAIN_DATA_KEY}-${brainId}-knowledge`;
|
||||
|
||||
export const PUBLIC_BRAINS_KEY = "quivr-public-brains";
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
getBrains,
|
||||
getBrainUsers,
|
||||
getDefaultBrain,
|
||||
getPublicBrains,
|
||||
setAsDefaultBrain,
|
||||
Subscription,
|
||||
updateBrain,
|
||||
@ -48,5 +49,6 @@ export const useBrainApi = () => {
|
||||
setAsDefaultBrain(brainId, axiosInstance),
|
||||
updateBrain: async (brainId: string, brain: UpdateBrainInput) =>
|
||||
updateBrain(brainId, brain, axiosInstance),
|
||||
getPublicBrains: async () => getPublicBrains(axiosInstance),
|
||||
};
|
||||
};
|
||||
|
@ -9,6 +9,7 @@ import Button from "@/lib/components/ui/Button";
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
import { Modal } from "@/lib/components/ui/Modal";
|
||||
import { defineMaxTokens } from "@/lib/helpers/defineMaxTokens";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { PublicAccessConfirmationModal } from "./components/PublicAccessConfirmationModal";
|
||||
import { useAddBrainModal } from "./hooks/useAddBrainModal";
|
||||
@ -16,7 +17,13 @@ import { Divider } from "../ui/Divider";
|
||||
import { Radio } from "../ui/Radio";
|
||||
import { TextArea } from "../ui/TextArea";
|
||||
|
||||
export const AddBrainModal = (): JSX.Element => {
|
||||
type AddBrainModalProps = {
|
||||
triggerClassName?: string;
|
||||
};
|
||||
|
||||
export const AddBrainModal = ({
|
||||
triggerClassName,
|
||||
}: AddBrainModalProps): JSX.Element => {
|
||||
const { t } = useTranslation(["translation", "brain", "config"]);
|
||||
const {
|
||||
handleSubmit,
|
||||
@ -43,7 +50,7 @@ export const AddBrainModal = (): JSX.Element => {
|
||||
<Button
|
||||
onClick={() => void 0}
|
||||
variant={"tertiary"}
|
||||
className="border-0"
|
||||
className={cn("border-0", triggerClassName)}
|
||||
data-testid="add-brain-button"
|
||||
>
|
||||
{t("newBrain", { ns: "brain" })}
|
||||
|
@ -15,11 +15,20 @@ interface FieldProps
|
||||
label?: string;
|
||||
name: string;
|
||||
icon?: React.ReactNode;
|
||||
inputClassName?: string;
|
||||
}
|
||||
|
||||
const Field = forwardRef(
|
||||
(
|
||||
{ label, className, name, required = false, icon, ...props }: FieldProps,
|
||||
{
|
||||
label,
|
||||
className,
|
||||
name,
|
||||
inputClassName,
|
||||
required = false,
|
||||
icon,
|
||||
...props
|
||||
}: FieldProps,
|
||||
forwardedRef
|
||||
) => {
|
||||
return (
|
||||
@ -33,7 +42,10 @@ const Field = forwardRef(
|
||||
<div className="relative">
|
||||
<input
|
||||
ref={forwardedRef as RefObject<HTMLInputElement>}
|
||||
className={`w-full bg-gray-50 dark:bg-gray-900 px-4 py-2 border rounded-md border-black/10 dark:border-white/25`}
|
||||
className={cn(
|
||||
`w-full bg-gray-50 dark:bg-gray-900 px-4 py-2 border rounded-md border-black/10 dark:border-white/25`,
|
||||
inputClassName
|
||||
)}
|
||||
name={name}
|
||||
id={name}
|
||||
{...props}
|
||||
|
@ -33,4 +33,11 @@ export type BackendMinimalBrainForUser = Omit<MinimalBrainForUser, "role"> & {
|
||||
rights: BrainRoleType;
|
||||
};
|
||||
|
||||
export type PublicBrain = {
|
||||
id: UUID;
|
||||
name: string;
|
||||
description?: string;
|
||||
number_of_subscribers: number;
|
||||
};
|
||||
|
||||
export type BrainContextType = ReturnType<typeof useBrainProvider>;
|
||||
|
@ -33,5 +33,8 @@
|
||||
"set_brain_status_to_public_modal_title": "Are you sure you want to set this as <span class='text-purple-800'>Public</span>?<br/><br/>",
|
||||
"set_brain_status_to_public_modal_description": "Every Quivr user will be able to:<br/>- Subscribe to your brain in the 'brains library'.<br/>- Use this brain and check the prompt and model configurations.<br/><br/>They won't have access to your uploaded files and people section.",
|
||||
"confirm_set_brain_status_to_public": "Yes, set as public",
|
||||
"cancel_set_brain_status_to_public": "No, keep it private"
|
||||
"cancel_set_brain_status_to_public": "No, keep it private",
|
||||
"brain_library_button_label":"Brains library",
|
||||
"public_brains_search_bar_placeholder":"Search public brains",
|
||||
"public_brain_subscribe_button_label":"Subscribe"
|
||||
}
|
@ -33,5 +33,8 @@
|
||||
"set_brain_status_to_public_modal_title": "¿Estás seguro de querer establecer esto como <span class='text-purple-800'>Público</span>?<br/><br/>",
|
||||
"set_brain_status_to_public_modal_description": "Cada usuario de Quivr podrá:<br/>- Suscribirse a tu cerebro en la 'biblioteca de cerebros'.<br/>- Usar este cerebro y comprobar las configuraciones de las indicaciones y el modelo.<br/><br/>No tendrán acceso a tus archivos cargados ni a la sección de personas.",
|
||||
"confirm_set_brain_status_to_public": "Sí, establecer como público",
|
||||
"cancel_set_brain_status_to_public": "No, mantenerlo privado"
|
||||
"cancel_set_brain_status_to_public": "No, mantenerlo privado",
|
||||
"brain_library_button_label": "Biblioteca de cerebros",
|
||||
"public_brains_search_bar_placeholder": "Buscar cerebros públicos",
|
||||
"public_brain_subscribe_button_label": "Suscribirse"
|
||||
}
|
@ -33,5 +33,8 @@
|
||||
"set_brain_status_to_public_modal_title": "Êtes-vous sûr de vouloir définir ceci comme <span class='text-purple-800'>Public</span>?<br/><br/>",
|
||||
"set_brain_status_to_public_modal_description": "Chaque utilisateur de Quivr pourra :<br/>- S'abonner à votre cerveau dans la 'bibliothèque des cerveaux'.<br/>- Utiliser ce cerveau et vérifier les configurations de prompts et de modèles.<br/><br/>Ils n'auront pas accès à vos fichiers téléchargés et à la section des personnes.",
|
||||
"confirm_set_brain_status_to_public": "Oui, définir comme public",
|
||||
"cancel_set_brain_status_to_public": "Non, le garder privé"
|
||||
"cancel_set_brain_status_to_public": "Non, le garder privé",
|
||||
"brain_library_button_label": "Bibliothèque des cerveaux",
|
||||
"public_brains_search_bar_placeholder": "Rechercher des cerveaux publics",
|
||||
"public_brain_subscribe_button_label": "S'abonner"
|
||||
}
|
@ -33,5 +33,8 @@
|
||||
"set_brain_status_to_public_modal_title": "Tem certeza de que deseja definir isso como <span class='text-purple-800'>Público</span>?<br/><br/>",
|
||||
"set_brain_status_to_public_modal_description": "Cada usuário do Quivr poderá:<br/>- Se inscrever em seu cérebro na 'biblioteca de cérebros'.<br/>- Usar este cérebro e verificar as configurações de prompts e modelos.<br/><br/>Eles não terão acesso aos seus arquivos enviados e à seção de pessoas.",
|
||||
"confirm_set_brain_status_to_public": "Sim, definir como público",
|
||||
"cancel_set_brain_status_to_public": "Não, mantê-lo privado"
|
||||
"cancel_set_brain_status_to_public": "Não, mantê-lo privado",
|
||||
"brain_library_button_label": "Biblioteca de cérebros",
|
||||
"public_brains_search_bar_placeholder": "Pesquisar cérebros públicos",
|
||||
"public_brain_subscribe_button_label": "Inscrever-se"
|
||||
}
|
@ -33,5 +33,8 @@
|
||||
"set_brain_status_to_public_modal_title": "Вы уверены, что хотите установить это как <span class='text-purple-800'>Публичный</span>?<br/><br/>",
|
||||
"set_brain_status_to_public_modal_description": "Каждый пользователь Quivr сможет:<br/>- Подписаться на ваш мозг в 'библиотеке мозгов'.<br/>- Использовать этот мозг и проверить настройки подсказок и модели.<br/><br/>У них не будет доступа к вашим загруженным файлам и разделу 'люди'.",
|
||||
"confirm_set_brain_status_to_public": "Да, установить как публичный",
|
||||
"cancel_set_brain_status_to_public": "Нет, оставить приватным"
|
||||
"cancel_set_brain_status_to_public": "Нет, оставить приватным",
|
||||
"brain_library_button_label": "Библиотека мозгов",
|
||||
"public_brains_search_bar_placeholder": "Поиск общественных мозгов",
|
||||
"public_brain_subscribe_button_label": "Подписаться"
|
||||
}
|
@ -33,5 +33,8 @@
|
||||
"set_brain_status_to_public_modal_title": "您确定要将此设置为<span class='text-purple-800'>公共</span>吗?<br/><br/>",
|
||||
"set_brain_status_to_public_modal_description": "每个 Quivr 用户将能够:<br/>- 在 '大脑库' 中订阅您的大脑。<br/>- 使用此大脑并检查提示和模型配置。<br/><br/>他们将无法访问您上传的文件和人员部分。",
|
||||
"confirm_set_brain_status_to_public": "是的,设为公共",
|
||||
"cancel_set_brain_status_to_public": "不,保持私密"
|
||||
"cancel_set_brain_status_to_public": "不,保持私密",
|
||||
"brain_library_button_label": "大脑库",
|
||||
"public_brains_search_bar_placeholder": "搜索公共大脑",
|
||||
"public_brain_subscribe_button_label": "订阅"
|
||||
}
|
Loading…
Reference in New Issue
Block a user