Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: count public brains number of subscribers #1236

Merged
merged 5 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions backend/models/brain_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
33 changes: 32 additions & 1 deletion backend/models/databases/supabase/brains.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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"]
2 changes: 1 addition & 1 deletion backend/repository/brain/create_brain.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
7 changes: 7 additions & 0 deletions backend/repository/brain/get_public_brains.py
Original file line number Diff line number Diff line change
@@ -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()
12 changes: 12 additions & 0 deletions backend/routes/brain_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
};
43 changes: 43 additions & 0 deletions frontend/app/brains-management/library/hooks/useBrainsLibrary.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 38 additions & 0 deletions frontend/app/brains-management/library/page.tsx
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 10 additions & 0 deletions frontend/lib/api/brain/__tests__/useBrainApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
});
});
7 changes: 7 additions & 0 deletions frontend/lib/api/brain/brain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
BackendMinimalBrainForUser,
Brain,
MinimalBrainForUser,
PublicBrain,
} from "@/lib/context/BrainProvider/types";
import { Document } from "@/lib/types/Document";

Expand Down Expand Up @@ -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;
};
8 changes: 5 additions & 3 deletions frontend/lib/api/brain/config.ts
Original file line number Diff line number Diff line change
@@ -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";
2 changes: 2 additions & 0 deletions frontend/lib/api/brain/useBrainApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getBrains,
getBrainUsers,
getDefaultBrain,
getPublicBrains,
setAsDefaultBrain,
Subscription,
updateBrain,
Expand Down Expand Up @@ -48,5 +49,6 @@ export const useBrainApi = () => {
setAsDefaultBrain(brainId, axiosInstance),
updateBrain: async (brainId: string, brain: UpdateBrainInput) =>
updateBrain(brainId, brain, axiosInstance),
getPublicBrains: async () => getPublicBrains(axiosInstance),
};
};
11 changes: 9 additions & 2 deletions frontend/lib/components/AddBrainModal/AddBrainModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@ 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";
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,
Expand All @@ -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" })}
Expand Down
Loading
Loading