mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-14 17:03:29 +03:00
feat: activate public brain subscription (#1241)
* feat: add public brain details modal * feat(brain): add subscription route * feat: activate subscription button * feat: add last_update column to brain table * feat: display last update on public brain details page * feat: change RBAC rule for public brains * feat: maintain brain last_update time
This commit is contained in:
parent
6166b17123
commit
2c9a0c1ed2
@ -10,6 +10,9 @@ from models.files import File
|
||||
from models.notifications import NotificationsStatusEnum
|
||||
from models.settings import get_supabase_client
|
||||
from parsers.github import process_github
|
||||
from repository.brain.update_brain_last_update_time import (
|
||||
update_brain_last_update_time,
|
||||
)
|
||||
from repository.notification.update_notification import update_notification_by_id
|
||||
from utils.processors import filter_file
|
||||
|
||||
@ -98,6 +101,8 @@ def process_file_and_notify(
|
||||
message=str(notification_message),
|
||||
),
|
||||
)
|
||||
update_brain_last_update_time(brain_id)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -158,4 +163,5 @@ def process_crawl_and_notify(
|
||||
message=str(notification_message),
|
||||
),
|
||||
)
|
||||
update_brain_last_update_time(brain_id)
|
||||
return True
|
||||
|
@ -15,6 +15,7 @@ class BrainEntity(BaseModel):
|
||||
openai_api_key: Optional[str]
|
||||
status: Optional[str]
|
||||
prompt_id: Optional[UUID]
|
||||
last_update: str
|
||||
|
||||
@property
|
||||
def id(self) -> UUID:
|
||||
@ -40,3 +41,4 @@ class PublicBrain(BaseModel):
|
||||
name: str
|
||||
description: Optional[str]
|
||||
number_of_subscribers: int = 0
|
||||
last_update: str
|
||||
|
@ -78,7 +78,7 @@ class Brain(Repository):
|
||||
def get_public_brains(self) -> list[PublicBrain]:
|
||||
response = (
|
||||
self.db.from_("brains")
|
||||
.select("id:brain_id, name, description")
|
||||
.select("id:brain_id, name, description, last_update")
|
||||
.filter("status", "eq", "public")
|
||||
.execute()
|
||||
)
|
||||
@ -88,11 +88,17 @@ class Brain(Repository):
|
||||
id=item["id"],
|
||||
name=item["name"],
|
||||
description=item["description"],
|
||||
last_update=item["last_update"],
|
||||
)
|
||||
brain.number_of_subscribers = self.get_brain_subscribers_count(brain.id)
|
||||
public_brains.append(brain)
|
||||
return public_brains
|
||||
|
||||
def update_brain_last_update_time(self, brain_id: UUID) -> None:
|
||||
self.db.table("brains").update({"last_update": "now()"}).match(
|
||||
{"brain_id": brain_id}
|
||||
).execute()
|
||||
|
||||
def get_brain_for_user(self, user_id, brain_id) -> MinimalBrainEntity | None:
|
||||
response = (
|
||||
self.db.from_("brains_users")
|
||||
|
@ -3,9 +3,16 @@ from uuid import UUID
|
||||
from models import BrainEntity, get_supabase_db
|
||||
from models.databases.supabase.brains import BrainUpdatableProperties
|
||||
|
||||
from repository.brain.update_brain_last_update_time import update_brain_last_update_time
|
||||
|
||||
|
||||
def update_brain_by_id(brain_id: UUID, brain: BrainUpdatableProperties) -> BrainEntity:
|
||||
"""Update a prompt by id"""
|
||||
supabase_db = get_supabase_db()
|
||||
|
||||
return supabase_db.update_brain_by_id(brain_id, brain) # type: ignore
|
||||
brain_update_answer = supabase_db.update_brain_by_id(brain_id, brain)
|
||||
if brain_update_answer is None:
|
||||
raise Exception("Brain not found")
|
||||
|
||||
update_brain_last_update_time(brain_id)
|
||||
return brain_update_answer
|
||||
|
@ -0,0 +1,8 @@
|
||||
from uuid import UUID
|
||||
|
||||
from models.settings import get_supabase_db
|
||||
|
||||
|
||||
def update_brain_last_update_time(brain_id: UUID):
|
||||
supabase_db = get_supabase_db()
|
||||
supabase_db.update_brain_last_update_time(brain_id)
|
@ -5,6 +5,8 @@ from auth.auth_bearer import get_current_user
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from models import UserIdentity
|
||||
from repository.brain import get_brain_for_user
|
||||
from repository.brain.get_brain_details import get_brain_details
|
||||
|
||||
from routes.authorizations.types import RoleEnum
|
||||
|
||||
|
||||
@ -43,6 +45,11 @@ def validate_brain_authorization(
|
||||
return: None
|
||||
"""
|
||||
|
||||
brain = get_brain_details(brain_id)
|
||||
|
||||
if brain and brain.status == "public":
|
||||
return
|
||||
|
||||
if required_roles is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
@ -361,3 +361,45 @@ def update_brain_subscription(
|
||||
update_brain_user_rights(brain_id, user_id, subscription.rights)
|
||||
|
||||
return {"message": "Brain subscription updated successfully"}
|
||||
|
||||
|
||||
@subscription_router.post(
|
||||
"/brains/{brain_id}/subscribe",
|
||||
tags=["Subscription"],
|
||||
)
|
||||
async def subscribe_to_brain_handler(
|
||||
brain_id: UUID, current_user: UserIdentity = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Subscribe to a public 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 not None:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You are already subscribed to this brain",
|
||||
)
|
||||
try:
|
||||
create_brain_user(
|
||||
user_id=current_user.id,
|
||||
brain_id=brain_id,
|
||||
rights=RoleEnum.Viewer,
|
||||
is_default_brain=False,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Error adding user to brain: {e}")
|
||||
|
||||
return {"message": "You have successfully subscribed to the brain"}
|
||||
|
@ -1,12 +1,15 @@
|
||||
/* eslint-disable max-lines */
|
||||
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 { SettingsTab } from "./components/SettingsTab/SettingsTab";
|
||||
import { useBrainManagementTabs } from "./hooks/useBrainManagementTabs";
|
||||
import { isUserBrainOwner } from "./utils/isUserBrainOwner";
|
||||
|
||||
export const BrainManagementTabs = (): JSX.Element => {
|
||||
const { t } = useTranslation(["translation", "config", "delete_brain"]);
|
||||
@ -19,13 +22,21 @@ export const BrainManagementTabs = (): JSX.Element => {
|
||||
setIsDeleteModalOpen,
|
||||
brain,
|
||||
} = useBrainManagementTabs();
|
||||
|
||||
const isPubliclyAccessible = brain?.status === "public";
|
||||
const { allBrains } = useBrainContext();
|
||||
|
||||
if (brainId === undefined) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
const isCurrentUserBrainOwner = isUserBrainOwner({
|
||||
brainId,
|
||||
userAccessibleBrains: allBrains,
|
||||
});
|
||||
|
||||
const isPublicBrain = brain?.status === "public";
|
||||
|
||||
const hasEditRights = !isPublicBrain || isCurrentUserBrainOwner;
|
||||
|
||||
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"
|
||||
@ -41,7 +52,7 @@ export const BrainManagementTabs = (): JSX.Element => {
|
||||
value="settings"
|
||||
onChange={setSelectedTab}
|
||||
/>
|
||||
{!isPubliclyAccessible && (
|
||||
{hasEditRights && (
|
||||
<>
|
||||
<BrainTabTrigger
|
||||
selected={selectedTab === "people"}
|
||||
@ -73,7 +84,7 @@ export const BrainManagementTabs = (): JSX.Element => {
|
||||
|
||||
<div className="flex justify-center mt-4">
|
||||
<Button
|
||||
disabled={isPubliclyAccessible}
|
||||
disabled={!isCurrentUserBrainOwner}
|
||||
className="px-8 md:px-20 py-2 bg-red-500 text-white rounded-md"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
>
|
||||
@ -89,5 +100,3 @@ export const BrainManagementTabs = (): JSX.Element => {
|
||||
</Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrainManagementTabs;
|
||||
|
@ -9,16 +9,19 @@ import { Chip } from "@/lib/components/ui/Chip";
|
||||
import { Divider } from "@/lib/components/ui/Divider";
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
import { TextArea } from "@/lib/components/ui/TextArea";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { defineMaxTokens } from "@/lib/helpers/defineMaxTokens";
|
||||
import { SaveButton } from "@/shared/SaveButton";
|
||||
|
||||
import { PublicPrompts } from "./components/PublicPrompts/PublicPrompts";
|
||||
import { useSettingsTab } from "./hooks/useSettingsTab";
|
||||
import { isUserBrainOwner } from "../../utils/isUserBrainOwner";
|
||||
|
||||
type SettingsTabProps = {
|
||||
brainId: UUID;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
|
||||
const { t } = useTranslation(["translation", "brain", "config"]);
|
||||
const {
|
||||
@ -38,8 +41,16 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
|
||||
accessibleModels,
|
||||
brain,
|
||||
} = useSettingsTab({ brainId });
|
||||
const { allBrains } = useBrainContext();
|
||||
|
||||
const isPubliclyAccessible = brain?.status === "public";
|
||||
const isCurrentUserBrainOwner = isUserBrainOwner({
|
||||
brainId,
|
||||
userAccessibleBrains: allBrains,
|
||||
});
|
||||
|
||||
const isPublicBrain = brain?.status === "public";
|
||||
|
||||
const hasEditRights = !isPublicBrain || isCurrentUserBrainOwner;
|
||||
|
||||
return (
|
||||
<form
|
||||
@ -58,14 +69,14 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
|
||||
autoComplete="off"
|
||||
className="flex-1"
|
||||
required
|
||||
disabled={isPubliclyAccessible}
|
||||
disabled={!hasEditRights}
|
||||
{...register("name")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="flex flex-1 items-center flex-col">
|
||||
{isPubliclyAccessible && (
|
||||
{isPublicBrain && (
|
||||
<Chip className="mb-3 bg-purple-600 text-white w-full">
|
||||
{t("brain:public_brain_label")}
|
||||
</Chip>
|
||||
@ -80,7 +91,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
|
||||
isLoading={isSettingAsDefault}
|
||||
onClick={() => void setAsDefaultBrainHandler()}
|
||||
type="button"
|
||||
disabled={isPubliclyAccessible}
|
||||
disabled={!hasEditRights}
|
||||
>
|
||||
{t("setDefaultBrain", { ns: "brain" })}
|
||||
</Button>
|
||||
@ -93,7 +104,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
|
||||
placeholder={t("brainDescriptionPlaceholder", { ns: "brain" })}
|
||||
autoComplete="off"
|
||||
className="flex-1 m-3"
|
||||
disabled={isPubliclyAccessible}
|
||||
disabled={!hasEditRights}
|
||||
{...register("description")}
|
||||
/>
|
||||
<Divider text={t("modelSection", { ns: "config" })} />
|
||||
@ -102,7 +113,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
|
||||
placeholder={t("openAiKeyPlaceholder", { ns: "config" })}
|
||||
autoComplete="off"
|
||||
className="flex-1"
|
||||
disabled={isPubliclyAccessible}
|
||||
disabled={!hasEditRights}
|
||||
{...register("openAiKey")}
|
||||
/>
|
||||
<fieldset className="w-full flex flex-col mt-2">
|
||||
@ -111,7 +122,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
|
||||
</label>
|
||||
<select
|
||||
id="model"
|
||||
disabled={isPubliclyAccessible}
|
||||
disabled={!hasEditRights}
|
||||
{...register("model")}
|
||||
className="px-5 py-2 dark:bg-gray-700 bg-gray-200 rounded-md"
|
||||
onChange={() => {
|
||||
@ -136,7 +147,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={temperature}
|
||||
disabled={isPubliclyAccessible}
|
||||
disabled={!hasEditRights}
|
||||
{...register("temperature")}
|
||||
/>
|
||||
</fieldset>
|
||||
@ -149,24 +160,21 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
|
||||
min="10"
|
||||
max={defineMaxTokens(model)}
|
||||
value={maxTokens}
|
||||
disabled={isPubliclyAccessible}
|
||||
disabled={!hasEditRights}
|
||||
{...register("maxTokens")}
|
||||
/>
|
||||
</fieldset>
|
||||
<div className="flex w-full justify-end py-4">
|
||||
<SaveButton
|
||||
disabled={isPubliclyAccessible}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
<SaveButton disabled={!hasEditRights} handleSubmit={handleSubmit} />
|
||||
</div>
|
||||
<Divider text={t("customPromptSection", { ns: "config" })} />
|
||||
{!isPubliclyAccessible && <PublicPrompts onSelect={pickPublicPrompt} />}
|
||||
{hasEditRights && <PublicPrompts onSelect={pickPublicPrompt} />}
|
||||
<Field
|
||||
label={t("promptName", { ns: "config" })}
|
||||
placeholder={t("promptNamePlaceholder", { ns: "config" })}
|
||||
autoComplete="off"
|
||||
className="flex-1"
|
||||
disabled={isPubliclyAccessible}
|
||||
disabled={!hasEditRights}
|
||||
{...register("prompt.title")}
|
||||
/>
|
||||
<TextArea
|
||||
@ -174,18 +182,15 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
|
||||
placeholder={t("promptContentPlaceholder", { ns: "config" })}
|
||||
autoComplete="off"
|
||||
className="flex-1"
|
||||
disabled={isPubliclyAccessible}
|
||||
disabled={!hasEditRights}
|
||||
{...register("prompt.content")}
|
||||
/>
|
||||
<div className="flex w-full justify-end py-4">
|
||||
<SaveButton
|
||||
disabled={isPubliclyAccessible}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
<SaveButton disabled={!hasEditRights} handleSubmit={handleSubmit} />
|
||||
</div>
|
||||
{promptId !== "" && (
|
||||
<Button
|
||||
disabled={isUpdating || isPubliclyAccessible}
|
||||
disabled={isUpdating || !hasEditRights}
|
||||
onClick={() => void removeBrainPrompt()}
|
||||
>
|
||||
{t("removePrompt", { ns: "config" })}
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { UUID } from "crypto";
|
||||
|
||||
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
|
||||
|
||||
type IsUserBrainOwnerProps = {
|
||||
userAccessibleBrains: MinimalBrainForUser[];
|
||||
brainId?: UUID;
|
||||
};
|
||||
export const isUserBrainOwner = ({
|
||||
brainId,
|
||||
userAccessibleBrains,
|
||||
}: IsUserBrainOwnerProps): boolean => {
|
||||
const brain = userAccessibleBrains.find(({ id }) => id === brainId);
|
||||
if (brain === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return brain.role === "Owner";
|
||||
};
|
@ -1,26 +0,0 @@
|
||||
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,68 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MdAdd } from "react-icons/md";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { Modal } from "@/lib/components/ui/Modal";
|
||||
import { PublicBrain } from "@/lib/context/BrainProvider/types";
|
||||
|
||||
import { usePublicBrainItem } from "./hooks/usePublicBrainItem";
|
||||
import { formatDate } from "./utils/formatDate";
|
||||
|
||||
type PublicBrainItemProps = {
|
||||
brain: PublicBrain;
|
||||
};
|
||||
|
||||
export const PublicBrainItem = ({
|
||||
brain,
|
||||
}: PublicBrainItemProps): JSX.Element => {
|
||||
const { handleSubscribeToBrain, subscriptionRequestPending } =
|
||||
usePublicBrainItem({
|
||||
brainId: brain.id,
|
||||
});
|
||||
|
||||
const { t } = useTranslation("brain");
|
||||
|
||||
const subscribeButton = (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
void handleSubscribeToBrain();
|
||||
}}
|
||||
disabled={subscriptionRequestPending}
|
||||
isLoading={subscriptionRequestPending}
|
||||
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>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
CloseTrigger={<div />}
|
||||
Trigger={
|
||||
<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 cursor-pointer">
|
||||
<p className="font-bold mb-5 text-xl">{brain.name}</p>
|
||||
<p className="line-clamp-2 text-center px-5">
|
||||
{brain.description ?? ""}
|
||||
</p>
|
||||
{subscribeButton}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-center mb-10">{brain.name}</p>
|
||||
<p className="mb-10">{brain.description ?? ""}</p>
|
||||
|
||||
<p className="font-bold mb-5">
|
||||
<span>
|
||||
<span className="mr-2">{t("public_brain_last_update_label")}:</span>
|
||||
{formatDate(brain.last_update)}
|
||||
</span>
|
||||
</p>
|
||||
<div className="flex flex-1 justify-end">{subscribeButton}</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -0,0 +1,52 @@
|
||||
import { UUID } from "crypto";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useSubscriptionApi } from "@/lib/api/subscription/useSubscriptionApi";
|
||||
import { getAxiosErrorParams } from "@/lib/helpers/getAxiosErrorParams";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
|
||||
type UseSubscribeToBrainProps = {
|
||||
brainId: UUID;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const usePublicBrainItem = ({ brainId }: UseSubscribeToBrainProps) => {
|
||||
const { subscribeToBrain } = useSubscriptionApi();
|
||||
const [subscriptionRequestPending, setSubscriptionRequestPending] =
|
||||
useState(false);
|
||||
const { publish } = useToast();
|
||||
|
||||
const { t } = useTranslation("brain");
|
||||
const handleSubscribeToBrain = async () => {
|
||||
try {
|
||||
setSubscriptionRequestPending(true);
|
||||
await subscribeToBrain(brainId);
|
||||
publish({
|
||||
text: t("public_brain_subscription_success_message"),
|
||||
variant: "success",
|
||||
});
|
||||
} catch (e) {
|
||||
const error = getAxiosErrorParams(e);
|
||||
if (error !== undefined) {
|
||||
publish({
|
||||
text: error.message,
|
||||
variant: "danger",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
publish({
|
||||
text: JSON.stringify(error),
|
||||
variant: "danger",
|
||||
});
|
||||
} finally {
|
||||
setSubscriptionRequestPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleSubscribeToBrain,
|
||||
subscriptionRequestPending,
|
||||
};
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from "./PublicBrainItem";
|
@ -0,0 +1,2 @@
|
||||
export const formatDate = (date: string): string =>
|
||||
new Date(date).toLocaleDateString();
|
@ -0,0 +1 @@
|
||||
export * from "./PublicBrainItem";
|
@ -3,7 +3,7 @@ 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";
|
||||
import { PublicBrain } from "@/lib/context/BrainProvider/types";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useBrainsLibrary = () => {
|
||||
@ -14,9 +14,9 @@ export const useBrainsLibrary = () => {
|
||||
queryFn: getPublicBrains,
|
||||
});
|
||||
|
||||
const [displayingPublicBrains, setDisplayingPublicBrains] = useState<Brain[]>(
|
||||
[]
|
||||
);
|
||||
const [displayingPublicBrains, setDisplayingPublicBrains] = useState<
|
||||
PublicBrain[]
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayingPublicBrains(publicBrains);
|
||||
|
@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
|
||||
import { PublicBrainItem } from "./components/PublicBrainItem";
|
||||
import { PublicBrainItem } from "./components/PublicBrainItem/PublicBrainItem";
|
||||
import { useBrainsLibrary } from "./hooks/useBrainsLibrary";
|
||||
|
||||
const BrainsLibrary = (): JSX.Element => {
|
||||
|
@ -54,3 +54,16 @@ export const getInvitation = async (
|
||||
role: invitation.rights,
|
||||
};
|
||||
};
|
||||
|
||||
export const subscribeToBrain = async (
|
||||
brainId: UUID,
|
||||
axiosInstance: AxiosInstance
|
||||
): Promise<{ message: string }> => {
|
||||
const subscribedToBrain = (
|
||||
await axiosInstance.post<{ message: string }>(
|
||||
`/brains/${brainId}/subscribe`
|
||||
)
|
||||
).data;
|
||||
|
||||
return subscribedToBrain;
|
||||
};
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
acceptInvitation,
|
||||
declineInvitation,
|
||||
getInvitation,
|
||||
subscribeToBrain,
|
||||
} from "./subscription";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
@ -19,5 +20,7 @@ export const useSubscriptionApi = () => {
|
||||
declineInvitation(brainId, axiosInstance),
|
||||
getInvitation: async (brainId: UUID) =>
|
||||
getInvitation(brainId, axiosInstance),
|
||||
subscribeToBrain: async (brainId: UUID) =>
|
||||
subscribeToBrain(brainId, axiosInstance),
|
||||
};
|
||||
};
|
||||
|
@ -38,6 +38,7 @@ export type PublicBrain = {
|
||||
name: string;
|
||||
description?: string;
|
||||
number_of_subscribers: number;
|
||||
last_update: string;
|
||||
};
|
||||
|
||||
export type BrainContextType = ReturnType<typeof useBrainProvider>;
|
||||
|
@ -36,5 +36,7 @@
|
||||
"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"
|
||||
"public_brain_subscribe_button_label":"Subscribe",
|
||||
"public_brain_subscription_success_message":"You have successfully subscribed to the brain",
|
||||
"public_brain_last_update_label":"Last update"
|
||||
}
|
@ -36,5 +36,7 @@
|
||||
"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"
|
||||
"public_brain_subscribe_button_label": "Suscribirse",
|
||||
"public_brain_subscription_success_message": "Te has suscrito con éxito al cerebro",
|
||||
"public_brain_last_update_label": "Última actualización"
|
||||
}
|
@ -36,5 +36,7 @@
|
||||
"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"
|
||||
"public_brain_subscribe_button_label": "S'abonner",
|
||||
"public_brain_subscription_success_message": "Vous vous êtes abonné avec succès au cerveau",
|
||||
"public_brain_last_update_label": "Dernière mise à jour"
|
||||
}
|
@ -36,5 +36,7 @@
|
||||
"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"
|
||||
"public_brain_subscribe_button_label": "Inscrever-se",
|
||||
"public_brain_subscription_success_message": "Você se inscreveu com sucesso no cérebro",
|
||||
"public_brain_last_update_label": "Última atualização"
|
||||
}
|
@ -36,5 +36,7 @@
|
||||
"cancel_set_brain_status_to_public": "Нет, оставить приватным",
|
||||
"brain_library_button_label": "Библиотека мозгов",
|
||||
"public_brains_search_bar_placeholder": "Поиск общественных мозгов",
|
||||
"public_brain_subscribe_button_label": "Подписаться"
|
||||
"public_brain_subscribe_button_label": "Подписаться",
|
||||
"public_brain_subscription_success_message": "Вы успешно подписались на мозг",
|
||||
"public_brain_last_update_label": "Последнее обновление"
|
||||
}
|
@ -36,5 +36,7 @@
|
||||
"cancel_set_brain_status_to_public": "不,保持私密",
|
||||
"brain_library_button_label": "大脑库",
|
||||
"public_brains_search_bar_placeholder": "搜索公共大脑",
|
||||
"public_brain_subscribe_button_label": "订阅"
|
||||
"public_brain_subscribe_button_label": "订阅",
|
||||
"public_brain_subscription_success_message": "Вы успешно подписались на мозг",
|
||||
"public_brain_last_update_label": "Последнее обновление"
|
||||
}
|
23
scripts/20230921160000_add_last_update_field_to_brain.sql
Normal file
23
scripts/20230921160000_add_last_update_field_to_brain.sql
Normal file
@ -0,0 +1,23 @@
|
||||
-- Add last_update column to 'brains' table if it doesn't exist
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'brains'
|
||||
AND column_name = 'last_update'
|
||||
) THEN
|
||||
ALTER TABLE brains ADD COLUMN last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Insert migration record if it doesn't exist
|
||||
INSERT INTO migrations (name)
|
||||
SELECT '20230921160000_add_last_update_field_to_brain'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM migrations WHERE name = '20230921160000_add_last_update_field_to_brain'
|
||||
);
|
||||
|
||||
-- Commit the changes
|
||||
COMMIT;
|
@ -269,9 +269,9 @@ CREATE POLICY "Access Quivr Storage 1jccrwz_2" ON storage.objects FOR UPDATE TO
|
||||
CREATE POLICY "Access Quivr Storage 1jccrwz_3" ON storage.objects FOR DELETE TO anon USING (bucket_id = 'quivr');
|
||||
|
||||
INSERT INTO migrations (name)
|
||||
SELECT '202309151054032_add_knowledge_tables'
|
||||
SELECT '20230921160000_add_last_update_field_to_brain'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM migrations WHERE name = '202309151054032_add_knowledge_tables'
|
||||
SELECT 1 FROM migrations WHERE name = '20230921160000_add_last_update_field_to_brain'
|
||||
);
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user