From e7ce2fa54b843fbe721f288dc723b94e87113b10 Mon Sep 17 00:00:00 2001 From: Antoine Dewez <44063631+Zewed@users.noreply.github.com> Date: Sat, 27 Apr 2024 13:44:48 +0200 Subject: [PATCH] feat(frontend): show remaining credits (#2495) # Description Please include a summary of the changes and the related issue. Please also include relevant motivation and context. ## Checklist before requesting a review Please delete options that are not relevant. - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented hard-to-understand areas - [ ] I have ideally added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged ## Screenshots (if appropriate): --- backend/modules/chat/controller/chat/utils.py | 2 +- .../modules/user/controller/user_controller.py | 13 +++++++++++++ backend/modules/user/repository/users.py | 10 ++++++++++ .../modules/user/repository/users_interface.py | 7 +++++++ backend/modules/user/service/user_usage.py | 2 +- frontend/app/chat/[chatId]/hooks/useChat.ts | 8 ++++++++ frontend/app/search/page.module.scss | 16 ++++++++++++++++ frontend/app/search/page.tsx | 18 ++++++++++++++---- frontend/lib/api/user/useUserApi.ts | 2 ++ frontend/lib/api/user/user.ts | 4 ++++ .../PageHeader/PageHeader.module.scss | 13 ++++++++++++- .../lib/components/PageHeader/PageHeader.tsx | 16 ++++++++++++++++ .../User-settings.provider.tsx | 14 ++++++++++++++ frontend/lib/helpers/iconList.ts | 2 ++ 14 files changed, 120 insertions(+), 7 deletions(-) diff --git a/backend/modules/chat/controller/chat/utils.py b/backend/modules/chat/controller/chat/utils.py index ea7eee95a..9632e7ddd 100644 --- a/backend/modules/chat/controller/chat/utils.py +++ b/backend/modules/chat/controller/chat/utils.py @@ -98,7 +98,7 @@ def update_user_usage(usage: UserUsage, user_settings, cost: int = 100): if int(montly_usage + cost) > int(monthly_chat_credit): raise HTTPException( status_code=429, # pyright: ignore reportPrivateUsage=none - detail=f"You have reached your monthly chat limit of {monthly_chat_credit} requests per months. Please upgrade your plan to increase your daily chat limit.", + detail=f"You have reached your monthly chat limit of {monthly_chat_credit} requests per months. Please upgrade your plan to increase your monthly chat limit.", ) else: usage.handle_increment_user_request_count(date, cost) diff --git a/backend/modules/user/controller/user_controller.py b/backend/modules/user/controller/user_controller.py index a70b3e25e..716b5983d 100644 --- a/backend/modules/user/controller/user_controller.py +++ b/backend/modules/user/controller/user_controller.py @@ -78,3 +78,16 @@ def get_user_identity_route( Get user identity. """ return user_repository.get_user_identity(current_user.id) + +@user_router.get( + "/user/credits", + dependencies=[Depends(AuthBearer())], + tags=["User"], +) +def get_user_credits( + current_user: UserIdentity = Depends(get_current_user), +) -> int: + """ + Get user remaining credits. + """ + return user_repository.get_user_credits(current_user.id) diff --git a/backend/modules/user/repository/users.py b/backend/modules/user/repository/users.py index 9c6ffaa92..cdd181909 100644 --- a/backend/modules/user/repository/users.py +++ b/backend/modules/user/repository/users.py @@ -1,6 +1,8 @@ +import time from models.settings import get_supabase_client from modules.user.entity.user_identity import UserIdentity from modules.user.repository.users_interface import UsersInterface +from modules.user.service import user_usage class Users(UsersInterface): @@ -73,3 +75,11 @@ class Users(UsersInterface): "get_user_email_by_user_id", {"user_id": str(user_id)} ).execute() return response.data[0]["email"] + + def get_user_credits(self, user_id): + user_usage_instance = user_usage.UserUsage(id=user_id) + + user_monthly_usage = user_usage_instance.get_user_monthly_usage(time.strftime("%Y%m%d")) + monthly_chat_credit = self.db.from_("user_settings").select("monthly_chat_credit").filter("user_id", "eq", str(user_id)).execute().data[0]["monthly_chat_credit"] + + return monthly_chat_credit - user_monthly_usage diff --git a/backend/modules/user/repository/users_interface.py b/backend/modules/user/repository/users_interface.py index 69506e33c..a188b455b 100644 --- a/backend/modules/user/repository/users_interface.py +++ b/backend/modules/user/repository/users_interface.py @@ -44,3 +44,10 @@ class UsersInterface(ABC): Get the user email by user id """ pass + + @abstractmethod + def get_user_credits(self, user_id: UUID) -> int: + """ + Get user remaining credits + """ + pass diff --git a/backend/modules/user/service/user_usage.py b/backend/modules/user/service/user_usage.py index cf7834ad9..dc73df750 100644 --- a/backend/modules/user/service/user_usage.py +++ b/backend/modules/user/service/user_usage.py @@ -54,7 +54,7 @@ class UserUsage(UserIdentity): def get_user_monthly_usage(self, date): """ - Fetch the user daily usage from the database + Fetch the user monthly usage from the database """ posthog = PostHogSettings() request = self.supabase_db.get_user_requests_count_for_month(self.id, date) diff --git a/frontend/app/chat/[chatId]/hooks/useChat.ts b/frontend/app/chat/[chatId]/hooks/useChat.ts index 2d846f460..5b786542b 100644 --- a/frontend/app/chat/[chatId]/hooks/useChat.ts +++ b/frontend/app/chat/[chatId]/hooks/useChat.ts @@ -7,9 +7,11 @@ import { useTranslation } from "react-i18next"; import { CHATS_DATA_KEY } from "@/lib/api/chat/config"; import { useChatApi } from "@/lib/api/chat/useChatApi"; +import { useUserApi } from "@/lib/api/user/useUserApi"; import { useChatContext } from "@/lib/context"; import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext"; import { useSearchModalContext } from "@/lib/context/SearchModalProvider/hooks/useSearchModalContext"; +import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks/useUserSettingsContext"; import { getChatNameFromQuestion } from "@/lib/helpers/getChatNameFromQuestion"; import { useToast } from "@/lib/hooks"; import { useOnboarding } from "@/lib/hooks/useOnboarding"; @@ -42,6 +44,8 @@ export const useChat = () => { chatConfig: { model, maxTokens, temperature }, } = useLocalStorageChatConfig(); const { isVisible } = useSearchModalContext(); + const { getUserCredits } = useUserApi(); + const { setRemainingCredits } = useUserSettingsContext(); const { addStreamQuestion } = useQuestion(); const { t } = useTranslation(["chat"]); @@ -95,6 +99,10 @@ export const useChat = () => { callback?.(); await addStreamQuestion(currentChatId, chatQuestion); + void (async () => { + const res = await getUserCredits(); + setRemainingCredits(res); + })(); } catch (error) { console.error({ error }); diff --git a/frontend/app/search/page.module.scss b/frontend/app/search/page.module.scss index a33ea829f..c47212eac 100644 --- a/frontend/app/search/page.module.scss +++ b/frontend/app/search/page.module.scss @@ -82,6 +82,22 @@ top: 0; left: 0; + .main_message_wrapper { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + .main_message { + display: flex; + flex-direction: column; + + .bolder { + font-weight: bold; + } + } + } + .first_brain_button { position: absolute; right: Spacings.$spacing07; diff --git a/frontend/app/search/page.tsx b/frontend/app/search/page.tsx index dd6c74ad1..d4ee1c025 100644 --- a/frontend/app/search/page.tsx +++ b/frontend/app/search/page.tsx @@ -86,12 +86,22 @@ const Search = (): JSX.Element => { !userIdentityData?.onboarded && !!isUserDataFetched && (
-
+
- - Press the following button to create your first brain - +
+ Welcome {userIdentityData?.username}! + + We will guide you through your quivr adventure and the + creation of your first brain. + + + First, Press the Create Brain button on the top right corner + to create your first brain. + +
+
+
{ ) => updateUserIdentity(userIdentityUpdatableProperties, axiosInstance), getUserIdentity: async () => getUserIdentity(axiosInstance), getUser: async () => getUser(axiosInstance), + getUserCredits: async () => getUserCredits(axiosInstance), }; }; diff --git a/frontend/lib/api/user/user.ts b/frontend/lib/api/user/user.ts index 6b62fa5c5..6604d8b96 100644 --- a/frontend/lib/api/user/user.ts +++ b/frontend/lib/api/user/user.ts @@ -53,3 +53,7 @@ export const getUserIdentity = async ( export const getUser = async ( axiosInstance: AxiosInstance ): Promise => (await axiosInstance.get("/user")).data; + +export const getUserCredits = async ( + axiosInstance: AxiosInstance +): Promise => (await axiosInstance.get("/user/credits")).data; diff --git a/frontend/lib/components/PageHeader/PageHeader.module.scss b/frontend/lib/components/PageHeader/PageHeader.module.scss index 49743888b..7f64f8ab6 100644 --- a/frontend/lib/components/PageHeader/PageHeader.module.scss +++ b/frontend/lib/components/PageHeader/PageHeader.module.scss @@ -30,7 +30,18 @@ .buttons_wrapper { display: flex; - gap: Spacings.$spacing03; + gap: Spacings.$spacing04; align-items: center; + + .credits { + display: flex; + align-items: center; + gap: Spacings.$spacing02; + + .number { + font-size: Typography.$tiny; + color: var(--gold); + } + } } } diff --git a/frontend/lib/components/PageHeader/PageHeader.tsx b/frontend/lib/components/PageHeader/PageHeader.tsx index 953ce5b08..21ee219fe 100644 --- a/frontend/lib/components/PageHeader/PageHeader.tsx +++ b/frontend/lib/components/PageHeader/PageHeader.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; +import { useUserApi } from "@/lib/api/user/useUserApi"; import { useMenuContext } from "@/lib/context/MenuProvider/hooks/useMenuContext"; import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks/useUserSettingsContext"; import { ButtonType } from "@/lib/types/QuivrButton"; @@ -23,6 +24,8 @@ export const PageHeader = ({ const { isOpened } = useMenuContext(); const { isDarkMode, setIsDarkMode } = useUserSettingsContext(); const [lightModeIconName, setLightModeIconName] = useState("sun"); + const { remainingCredits, setRemainingCredits } = useUserSettingsContext(); + const { getUserCredits } = useUserApi(); const toggleTheme = () => { setIsDarkMode(!isDarkMode); @@ -32,6 +35,13 @@ export const PageHeader = ({ setLightModeIconName(isDarkMode ? "sun" : "moon"); }, [isDarkMode]); + useEffect(() => { + void (async () => { + const res = await getUserCredits(); + setRemainingCredits(res); + })(); + }, []); + return (
@@ -49,6 +59,12 @@ export const PageHeader = ({ hidden={button.hidden} /> ))} + {remainingCredits !== null && ( +
+ {remainingCredits} + +
+ )} >; + remainingCredits: number | null; + setRemainingCredits: React.Dispatch>; }; export const UserSettingsContext = createContext< @@ -16,6 +19,8 @@ export const UserSettingsProvider = ({ }: { children: React.ReactNode; }): JSX.Element => { + const { getUserCredits } = useUserApi(); + const [remainingCredits, setRemainingCredits] = useState(null); const [isDarkMode, setIsDarkMode] = useState(() => { if (typeof window !== "undefined") { return true; @@ -24,6 +29,13 @@ export const UserSettingsProvider = ({ return true; }); + useEffect(() => { + void (async () => { + const res = await getUserCredits(); + setRemainingCredits(res); + })(); + }, []); + useEffect(() => { if (typeof window !== "undefined") { const prefersDarkMode = window.matchMedia( @@ -66,6 +78,8 @@ export const UserSettingsProvider = ({ value={{ isDarkMode, setIsDarkMode, + remainingCredits, + setRemainingCredits, }} > {children} diff --git a/frontend/lib/helpers/iconList.ts b/frontend/lib/helpers/iconList.ts index b1e091b39..478825f91 100644 --- a/frontend/lib/helpers/iconList.ts +++ b/frontend/lib/helpers/iconList.ts @@ -1,4 +1,5 @@ import { AiOutlineLoading3Quarters } from "react-icons/ai"; +import { BiCoin } from "react-icons/bi"; import { BsArrowRightShort, BsChatLeftText, @@ -90,6 +91,7 @@ export const iconList: { [name: string]: IconType } = { chevronLeft: LuChevronLeft, chevronRight: LuChevronRight, close: IoMdClose, + coin: BiCoin, copy: LuCopy, custom: MdDashboardCustomize, delete: MdDeleteOutline,