diff --git a/backend/models/brain_entity.py b/backend/models/brain_entity.py
index f98e0b87d7b2..a101af601076 100644
--- a/backend/models/brain_entity.py
+++ b/backend/models/brain_entity.py
@@ -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
diff --git a/backend/models/databases/supabase/brains.py b/backend/models/databases/supabase/brains.py
index 32af7249a4e0..5389d0848dff 100644
--- a/backend/models/databases/supabase/brains.py
+++ b/backend/models/databases/supabase/brains.py
@@ -2,7 +2,7 @@
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 @@ def get_user_brains(self, user_id) -> list[MinimalBrainEntity]:
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 @@ def get_brain_by_id(self, brain_id: UUID) -> BrainEntity | None:
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"]
diff --git a/backend/repository/brain/create_brain.py b/backend/repository/brain/create_brain.py
index 84d7665aba27..e722dcdcc68e 100644
--- a/backend/repository/brain/create_brain.py
+++ b/backend/repository/brain/create_brain.py
@@ -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:
diff --git a/backend/repository/brain/get_public_brains.py b/backend/repository/brain/get_public_brains.py
new file mode 100644
index 000000000000..2f93f5bed093
--- /dev/null
+++ b/backend/repository/brain/get_public_brains.py
@@ -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()
diff --git a/backend/routes/brain_routes.py b/backend/routes/brain_routes.py
index 24d7dc374938..fbe627af08a6 100644
--- a/backend/routes/brain_routes.py
+++ b/backend/routes/brain_routes.py
@@ -4,6 +4,7 @@
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 @@
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"]
diff --git a/frontend/app/brains-management/[brainId]/components/BrainListItem/BrainListItem.tsx b/frontend/app/brains-management/[brainId]/components/BrainListItem/BrainListItem.tsx
index 60d2494e22cf..b1f4c527a9cd 100644
--- a/frontend/app/brains-management/[brainId]/components/BrainListItem/BrainListItem.tsx
+++ b/frontend/app/brains-management/[brainId]/components/BrainListItem/BrainListItem.tsx
@@ -31,7 +31,7 @@ export const BrainListItem = ({ brain }: BrainsListItemProps): JSX.Element => {
href={`/brains-management/${brain.id}`}
key={brain.id}
>
-
+
{brain.name}
diff --git a/frontend/app/brains-management/[brainId]/components/BrainsList.tsx b/frontend/app/brains-management/[brainId]/components/BrainsList.tsx
index 66fbaad12af6..8f4baf8beacd 100644
--- a/frontend/app/brains-management/[brainId]/components/BrainsList.tsx
+++ b/frontend/app/brains-management/[brainId]/components/BrainsList.tsx
@@ -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 (
{
))}
-
+
+
+
+
diff --git a/frontend/app/brains-management/[brainId]/hooks/useBrainsList.ts b/frontend/app/brains-management/[brainId]/hooks/useBrainsList.ts
index 9e84e7a33e07..57d697886c95 100644
--- a/frontend/app/brains-management/[brainId]/hooks/useBrainsList.ts
+++ b/frontend/app/brains-management/[brainId]/hooks/useBrainsList.ts
@@ -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,
diff --git a/frontend/app/brains-management/library/components/PublicBrainItem.tsx b/frontend/app/brains-management/library/components/PublicBrainItem.tsx
new file mode 100644
index 000000000000..c0e708364726
--- /dev/null
+++ b/frontend/app/brains-management/library/components/PublicBrainItem.tsx
@@ -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 (
+
+
{brain.name}
+
{brain.description ?? ""}
+
+
+ );
+};
diff --git a/frontend/app/brains-management/library/hooks/useBrainsLibrary.tsx b/frontend/app/brains-management/library/hooks/useBrainsLibrary.tsx
new file mode 100644
index 000000000000..4ffddf43420d
--- /dev/null
+++ b/frontend/app/brains-management/library/hooks/useBrainsLibrary.tsx
@@ -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
(
+ []
+ );
+
+ 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,
+ };
+};
diff --git a/frontend/app/brains-management/library/page.tsx b/frontend/app/brains-management/library/page.tsx
new file mode 100644
index 000000000000..f4d72de6dc48
--- /dev/null
+++ b/frontend/app/brains-management/library/page.tsx
@@ -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 (
+
+
+ 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")}
+ />
+
+
+
+ {displayingPublicBrains.map((brain) => (
+
+ ))}
+
+
+ );
+};
+
+export default BrainsLibrary;
diff --git a/frontend/lib/api/brain/__tests__/useBrainApi.test.ts b/frontend/lib/api/brain/__tests__/useBrainApi.test.ts
index 4d37e79919be..24662d317ade 100644
--- a/frontend/lib/api/brain/__tests__/useBrainApi.test.ts
+++ b/frontend/lib/api/brain/__tests__/useBrainApi.test.ts
@@ -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`);
+ });
});
diff --git a/frontend/lib/api/brain/brain.ts b/frontend/lib/api/brain/brain.ts
index 211d605ceeb4..46b06bd7822a 100644
--- a/frontend/lib/api/brain/brain.ts
+++ b/frontend/lib/api/brain/brain.ts
@@ -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 => {
await axiosInstance.put(`/brains/${brainId}/`, brain);
};
+
+export const getPublicBrains = async (
+ axiosInstance: AxiosInstance
+): Promise => {
+ return (await axiosInstance.get(`/brains/public`)).data;
+};
diff --git a/frontend/lib/api/brain/config.ts b/frontend/lib/api/brain/config.ts
index 32f7aedb78e0..d3da5381c874 100644
--- a/frontend/lib/api/brain/config.ts
+++ b/frontend/lib/api/brain/config.ts
@@ -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";
diff --git a/frontend/lib/api/brain/useBrainApi.ts b/frontend/lib/api/brain/useBrainApi.ts
index ad4a1bbc62fa..1a2b09497b6c 100644
--- a/frontend/lib/api/brain/useBrainApi.ts
+++ b/frontend/lib/api/brain/useBrainApi.ts
@@ -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),
};
};
diff --git a/frontend/lib/components/AddBrainModal/AddBrainModal.tsx b/frontend/lib/components/AddBrainModal/AddBrainModal.tsx
index 2d2c33256b37..e985ac68d9e8 100644
--- a/frontend/lib/components/AddBrainModal/AddBrainModal.tsx
+++ b/frontend/lib/components/AddBrainModal/AddBrainModal.tsx
@@ -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 => {