mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-14 17:03:29 +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):
|
||||
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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 });
|
||||
|
||||
|
@ -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;
|
||||
|
@ -86,12 +86,22 @@ const Search = (): JSX.Element => {
|
||||
!userIdentityData?.onboarded &&
|
||||
!!isUserDataFetched && (
|
||||
<div className={styles.onboarding_overlay}>
|
||||
<div className={styles.first_brain_button}>
|
||||
<div className={styles.main_message_wrapper}>
|
||||
<MessageInfoBox type="tutorial">
|
||||
<div className={styles.main_message}>
|
||||
<span>Welcome {userIdentityData?.username}!</span>
|
||||
<span>
|
||||
Press the following button to create your first brain
|
||||
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>
|
||||
</div>
|
||||
<div className={styles.first_brain_button}>
|
||||
<QuivrButton
|
||||
iconName="brain"
|
||||
label="Create Brain"
|
||||
|
@ -2,6 +2,7 @@ import { useAxios } from "@/lib/hooks";
|
||||
|
||||
import {
|
||||
getUser,
|
||||
getUserCredits,
|
||||
getUserIdentity,
|
||||
updateUserIdentity,
|
||||
UserIdentityUpdatableProperties,
|
||||
@ -17,5 +18,6 @@ export const useUserApi = () => {
|
||||
) => updateUserIdentity(userIdentityUpdatableProperties, axiosInstance),
|
||||
getUserIdentity: async () => getUserIdentity(axiosInstance),
|
||||
getUser: async () => getUser(axiosInstance),
|
||||
getUserCredits: async () => getUserCredits(axiosInstance),
|
||||
};
|
||||
};
|
||||
|
@ -53,3 +53,7 @@ export const getUserIdentity = async (
|
||||
export const getUser = async (
|
||||
axiosInstance: AxiosInstance
|
||||
): 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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
<div className={styles.page_header_wrapper}>
|
||||
<div className={`${styles.left} ${!isOpened ? styles.menu_closed : ""}`}>
|
||||
@ -49,6 +59,12 @@ export const PageHeader = ({
|
||||
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
|
||||
name={lightModeIconName}
|
||||
color="black"
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { createContext, useEffect, useState } from "react";
|
||||
|
||||
import { useUserApi } from "@/lib/api/user/useUserApi";
|
||||
import { parseBoolean } from "@/lib/helpers/parseBoolean";
|
||||
|
||||
type UserSettingsContextType = {
|
||||
isDarkMode: boolean;
|
||||
setIsDarkMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
remainingCredits: number | null;
|
||||
setRemainingCredits: React.Dispatch<React.SetStateAction<number | null>>;
|
||||
};
|
||||
|
||||
export const UserSettingsContext = createContext<
|
||||
@ -16,6 +19,8 @@ export const UserSettingsProvider = ({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => {
|
||||
const { getUserCredits } = useUserApi();
|
||||
const [remainingCredits, setRemainingCredits] = useState<number | null>(null);
|
||||
const [isDarkMode, setIsDarkMode] = useState<boolean>(() => {
|
||||
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}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user