mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-15 01:21:48 +03:00
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):
This commit is contained in:
parent
6cde04b65f
commit
e7ce2fa54b
@ -98,7 +98,7 @@ def update_user_usage(usage: UserUsage, user_settings, cost: int = 100):
|
|||||||
if int(montly_usage + cost) > int(monthly_chat_credit):
|
if int(montly_usage + cost) > int(monthly_chat_credit):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=429, # pyright: ignore reportPrivateUsage=none
|
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:
|
else:
|
||||||
usage.handle_increment_user_request_count(date, cost)
|
usage.handle_increment_user_request_count(date, cost)
|
||||||
|
@ -78,3 +78,16 @@ def get_user_identity_route(
|
|||||||
Get user identity.
|
Get user identity.
|
||||||
"""
|
"""
|
||||||
return user_repository.get_user_identity(current_user.id)
|
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)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
import time
|
||||||
from models.settings import get_supabase_client
|
from models.settings import get_supabase_client
|
||||||
from modules.user.entity.user_identity import UserIdentity
|
from modules.user.entity.user_identity import UserIdentity
|
||||||
from modules.user.repository.users_interface import UsersInterface
|
from modules.user.repository.users_interface import UsersInterface
|
||||||
|
from modules.user.service import user_usage
|
||||||
|
|
||||||
|
|
||||||
class Users(UsersInterface):
|
class Users(UsersInterface):
|
||||||
@ -73,3 +75,11 @@ class Users(UsersInterface):
|
|||||||
"get_user_email_by_user_id", {"user_id": str(user_id)}
|
"get_user_email_by_user_id", {"user_id": str(user_id)}
|
||||||
).execute()
|
).execute()
|
||||||
return response.data[0]["email"]
|
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
|
||||||
|
@ -44,3 +44,10 @@ class UsersInterface(ABC):
|
|||||||
Get the user email by user id
|
Get the user email by user id
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_user_credits(self, user_id: UUID) -> int:
|
||||||
|
"""
|
||||||
|
Get user remaining credits
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
@ -54,7 +54,7 @@ class UserUsage(UserIdentity):
|
|||||||
|
|
||||||
def get_user_monthly_usage(self, date):
|
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()
|
posthog = PostHogSettings()
|
||||||
request = self.supabase_db.get_user_requests_count_for_month(self.id, date)
|
request = self.supabase_db.get_user_requests_count_for_month(self.id, date)
|
||||||
|
@ -7,9 +7,11 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { CHATS_DATA_KEY } from "@/lib/api/chat/config";
|
import { CHATS_DATA_KEY } from "@/lib/api/chat/config";
|
||||||
import { useChatApi } from "@/lib/api/chat/useChatApi";
|
import { useChatApi } from "@/lib/api/chat/useChatApi";
|
||||||
|
import { useUserApi } from "@/lib/api/user/useUserApi";
|
||||||
import { useChatContext } from "@/lib/context";
|
import { useChatContext } from "@/lib/context";
|
||||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||||
import { useSearchModalContext } from "@/lib/context/SearchModalProvider/hooks/useSearchModalContext";
|
import { useSearchModalContext } from "@/lib/context/SearchModalProvider/hooks/useSearchModalContext";
|
||||||
|
import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks/useUserSettingsContext";
|
||||||
import { getChatNameFromQuestion } from "@/lib/helpers/getChatNameFromQuestion";
|
import { getChatNameFromQuestion } from "@/lib/helpers/getChatNameFromQuestion";
|
||||||
import { useToast } from "@/lib/hooks";
|
import { useToast } from "@/lib/hooks";
|
||||||
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
||||||
@ -42,6 +44,8 @@ export const useChat = () => {
|
|||||||
chatConfig: { model, maxTokens, temperature },
|
chatConfig: { model, maxTokens, temperature },
|
||||||
} = useLocalStorageChatConfig();
|
} = useLocalStorageChatConfig();
|
||||||
const { isVisible } = useSearchModalContext();
|
const { isVisible } = useSearchModalContext();
|
||||||
|
const { getUserCredits } = useUserApi();
|
||||||
|
const { setRemainingCredits } = useUserSettingsContext();
|
||||||
|
|
||||||
const { addStreamQuestion } = useQuestion();
|
const { addStreamQuestion } = useQuestion();
|
||||||
const { t } = useTranslation(["chat"]);
|
const { t } = useTranslation(["chat"]);
|
||||||
@ -95,6 +99,10 @@ export const useChat = () => {
|
|||||||
|
|
||||||
callback?.();
|
callback?.();
|
||||||
await addStreamQuestion(currentChatId, chatQuestion);
|
await addStreamQuestion(currentChatId, chatQuestion);
|
||||||
|
void (async () => {
|
||||||
|
const res = await getUserCredits();
|
||||||
|
setRemainingCredits(res);
|
||||||
|
})();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error({ error });
|
console.error({ error });
|
||||||
|
|
||||||
|
@ -82,6 +82,22 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 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 {
|
.first_brain_button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: Spacings.$spacing07;
|
right: Spacings.$spacing07;
|
||||||
|
@ -86,12 +86,22 @@ const Search = (): JSX.Element => {
|
|||||||
!userIdentityData?.onboarded &&
|
!userIdentityData?.onboarded &&
|
||||||
!!isUserDataFetched && (
|
!!isUserDataFetched && (
|
||||||
<div className={styles.onboarding_overlay}>
|
<div className={styles.onboarding_overlay}>
|
||||||
<div className={styles.first_brain_button}>
|
<div className={styles.main_message_wrapper}>
|
||||||
<MessageInfoBox type="tutorial">
|
<MessageInfoBox type="tutorial">
|
||||||
<span>
|
<div className={styles.main_message}>
|
||||||
Press the following button to create your first brain
|
<span>Welcome {userIdentityData?.username}!</span>
|
||||||
</span>
|
<span>
|
||||||
|
We will guide you through your quivr adventure and the
|
||||||
|
creation of your first brain.
|
||||||
|
</span>
|
||||||
|
<span className={styles.bolder}>
|
||||||
|
First, Press the Create Brain button on the top right corner
|
||||||
|
to create your first brain.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</MessageInfoBox>
|
</MessageInfoBox>
|
||||||
|
</div>
|
||||||
|
<div className={styles.first_brain_button}>
|
||||||
<QuivrButton
|
<QuivrButton
|
||||||
iconName="brain"
|
iconName="brain"
|
||||||
label="Create Brain"
|
label="Create Brain"
|
||||||
|
@ -2,6 +2,7 @@ import { useAxios } from "@/lib/hooks";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
getUser,
|
getUser,
|
||||||
|
getUserCredits,
|
||||||
getUserIdentity,
|
getUserIdentity,
|
||||||
updateUserIdentity,
|
updateUserIdentity,
|
||||||
UserIdentityUpdatableProperties,
|
UserIdentityUpdatableProperties,
|
||||||
@ -17,5 +18,6 @@ export const useUserApi = () => {
|
|||||||
) => updateUserIdentity(userIdentityUpdatableProperties, axiosInstance),
|
) => updateUserIdentity(userIdentityUpdatableProperties, axiosInstance),
|
||||||
getUserIdentity: async () => getUserIdentity(axiosInstance),
|
getUserIdentity: async () => getUserIdentity(axiosInstance),
|
||||||
getUser: async () => getUser(axiosInstance),
|
getUser: async () => getUser(axiosInstance),
|
||||||
|
getUserCredits: async () => getUserCredits(axiosInstance),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -53,3 +53,7 @@ export const getUserIdentity = async (
|
|||||||
export const getUser = async (
|
export const getUser = async (
|
||||||
axiosInstance: AxiosInstance
|
axiosInstance: AxiosInstance
|
||||||
): Promise<UserStats> => (await axiosInstance.get<UserStats>("/user")).data;
|
): Promise<UserStats> => (await axiosInstance.get<UserStats>("/user")).data;
|
||||||
|
|
||||||
|
export const getUserCredits = async (
|
||||||
|
axiosInstance: AxiosInstance
|
||||||
|
): Promise<number> => (await axiosInstance.get<number>("/user/credits")).data;
|
||||||
|
@ -30,7 +30,18 @@
|
|||||||
|
|
||||||
.buttons_wrapper {
|
.buttons_wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: Spacings.$spacing03;
|
gap: Spacings.$spacing04;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
.credits {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: Spacings.$spacing02;
|
||||||
|
|
||||||
|
.number {
|
||||||
|
font-size: Typography.$tiny;
|
||||||
|
color: var(--gold);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useUserApi } from "@/lib/api/user/useUserApi";
|
||||||
import { useMenuContext } from "@/lib/context/MenuProvider/hooks/useMenuContext";
|
import { useMenuContext } from "@/lib/context/MenuProvider/hooks/useMenuContext";
|
||||||
import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks/useUserSettingsContext";
|
import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks/useUserSettingsContext";
|
||||||
import { ButtonType } from "@/lib/types/QuivrButton";
|
import { ButtonType } from "@/lib/types/QuivrButton";
|
||||||
@ -23,6 +24,8 @@ export const PageHeader = ({
|
|||||||
const { isOpened } = useMenuContext();
|
const { isOpened } = useMenuContext();
|
||||||
const { isDarkMode, setIsDarkMode } = useUserSettingsContext();
|
const { isDarkMode, setIsDarkMode } = useUserSettingsContext();
|
||||||
const [lightModeIconName, setLightModeIconName] = useState("sun");
|
const [lightModeIconName, setLightModeIconName] = useState("sun");
|
||||||
|
const { remainingCredits, setRemainingCredits } = useUserSettingsContext();
|
||||||
|
const { getUserCredits } = useUserApi();
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
setIsDarkMode(!isDarkMode);
|
setIsDarkMode(!isDarkMode);
|
||||||
@ -32,6 +35,13 @@ export const PageHeader = ({
|
|||||||
setLightModeIconName(isDarkMode ? "sun" : "moon");
|
setLightModeIconName(isDarkMode ? "sun" : "moon");
|
||||||
}, [isDarkMode]);
|
}, [isDarkMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
const res = await getUserCredits();
|
||||||
|
setRemainingCredits(res);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.page_header_wrapper}>
|
<div className={styles.page_header_wrapper}>
|
||||||
<div className={`${styles.left} ${!isOpened ? styles.menu_closed : ""}`}>
|
<div className={`${styles.left} ${!isOpened ? styles.menu_closed : ""}`}>
|
||||||
@ -49,6 +59,12 @@ export const PageHeader = ({
|
|||||||
hidden={button.hidden}
|
hidden={button.hidden}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{remainingCredits !== null && (
|
||||||
|
<div className={styles.credits}>
|
||||||
|
<span className={styles.number}>{remainingCredits}</span>
|
||||||
|
<Icon name="coin" color="gold" size="normal"></Icon>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Icon
|
<Icon
|
||||||
name={lightModeIconName}
|
name={lightModeIconName}
|
||||||
color="black"
|
color="black"
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import { createContext, useEffect, useState } from "react";
|
import { createContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useUserApi } from "@/lib/api/user/useUserApi";
|
||||||
import { parseBoolean } from "@/lib/helpers/parseBoolean";
|
import { parseBoolean } from "@/lib/helpers/parseBoolean";
|
||||||
|
|
||||||
type UserSettingsContextType = {
|
type UserSettingsContextType = {
|
||||||
isDarkMode: boolean;
|
isDarkMode: boolean;
|
||||||
setIsDarkMode: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsDarkMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
remainingCredits: number | null;
|
||||||
|
setRemainingCredits: React.Dispatch<React.SetStateAction<number | null>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserSettingsContext = createContext<
|
export const UserSettingsContext = createContext<
|
||||||
@ -16,6 +19,8 @@ export const UserSettingsProvider = ({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
|
const { getUserCredits } = useUserApi();
|
||||||
|
const [remainingCredits, setRemainingCredits] = useState<number | null>(null);
|
||||||
const [isDarkMode, setIsDarkMode] = useState<boolean>(() => {
|
const [isDarkMode, setIsDarkMode] = useState<boolean>(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
return true;
|
return true;
|
||||||
@ -24,6 +29,13 @@ export const UserSettingsProvider = ({
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
const res = await getUserCredits();
|
||||||
|
setRemainingCredits(res);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const prefersDarkMode = window.matchMedia(
|
const prefersDarkMode = window.matchMedia(
|
||||||
@ -66,6 +78,8 @@ export const UserSettingsProvider = ({
|
|||||||
value={{
|
value={{
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
setIsDarkMode,
|
setIsDarkMode,
|
||||||
|
remainingCredits,
|
||||||
|
setRemainingCredits,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { AiOutlineLoading3Quarters } from "react-icons/ai";
|
import { AiOutlineLoading3Quarters } from "react-icons/ai";
|
||||||
|
import { BiCoin } from "react-icons/bi";
|
||||||
import {
|
import {
|
||||||
BsArrowRightShort,
|
BsArrowRightShort,
|
||||||
BsChatLeftText,
|
BsChatLeftText,
|
||||||
@ -90,6 +91,7 @@ export const iconList: { [name: string]: IconType } = {
|
|||||||
chevronLeft: LuChevronLeft,
|
chevronLeft: LuChevronLeft,
|
||||||
chevronRight: LuChevronRight,
|
chevronRight: LuChevronRight,
|
||||||
close: IoMdClose,
|
close: IoMdClose,
|
||||||
|
coin: BiCoin,
|
||||||
copy: LuCopy,
|
copy: LuCopy,
|
||||||
custom: MdDashboardCustomize,
|
custom: MdDashboardCustomize,
|
||||||
delete: MdDeleteOutline,
|
delete: MdDeleteOutline,
|
||||||
|
Loading…
Reference in New Issue
Block a user