From 76731dfbb86905219d908f518f5ed40771f581c9 Mon Sep 17 00:00:00 2001 From: Mohammad Kermani Date: Wed, 9 Apr 2025 09:38:11 +0000 Subject: [PATCH 1/2] feat: add query usage box --- app/(chat)/adapter.ts | 35 ++++++++++++++++ app/(chat)/api/query-usage/route.ts | 26 ++++++++++++ app/(chat)/service.ts | 40 ++++++++++++++++++ app/(chat)/types.ts | 13 ++++++ components/chat-header.tsx | 4 +- components/chat.tsx | 1 + components/query-usage.tsx | 63 +++++++++++++++++++++++++++++ 7 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 app/(chat)/api/query-usage/route.ts create mode 100644 components/query-usage.tsx diff --git a/app/(chat)/adapter.ts b/app/(chat)/adapter.ts index 38e04ae..d60f226 100644 --- a/app/(chat)/adapter.ts +++ b/app/(chat)/adapter.ts @@ -8,6 +8,7 @@ import type { ApiSendMessageStreamedResponse, ApiGetAllConversationsResponse, ApiRenameConversationResponse, + ApiQueryUsageResponse, } from '@/app/(chat)/types'; import config from '@/config'; import { extractErrorMessageOrDefault } from '@/lib/utils'; @@ -359,3 +360,37 @@ export const renameConversation = async ( ); } }; + +/** + * Get query usage + * @param accessToken + * @returns result containing query usage + */ +export const getQueryUsage = async ( + accessToken: string, +): Promise> => { + try { + const response = await fetch(`${patternCoreEndpoint}/query-usage`, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + + if (response.ok) { + const queryUsage: ApiQueryUsageResponse = (await response.json()).data; + return Ok(queryUsage); + } + + return Err( + `Fetching query usage failed with error code ${response.status}`, + ); + } catch (error) { + return Err( + extractErrorMessageOrDefault( + error, + 'Unknown error while fetching query usage', + ), + ); + } +}; diff --git a/app/(chat)/api/query-usage/route.ts b/app/(chat)/api/query-usage/route.ts new file mode 100644 index 0000000..15e181e --- /dev/null +++ b/app/(chat)/api/query-usage/route.ts @@ -0,0 +1,26 @@ +import { auth } from '@/app/(auth)/auth'; + +import { getQueryUsage } from '../../service'; + +export async function GET() { + const session = await auth(); + + if ( + !session || + !session.chainId || + !session.address || + !session.accessToken + ) { + return Response.json('Unauthorized!', { status: 401 }); + } + + const queryUsageResult = await getQueryUsage(session.accessToken); + + if (queryUsageResult.isErr()) { + return new Response(queryUsageResult.error, { status: 400 }); + } + + const queryUsage = queryUsageResult.value; + + return Response.json(queryUsage); +} diff --git a/app/(chat)/service.ts b/app/(chat)/service.ts index 959890a..8f15c40 100644 --- a/app/(chat)/service.ts +++ b/app/(chat)/service.ts @@ -7,6 +7,7 @@ import { renameConversation, getConversation, getAllConversations, + getQueryUsage as getQueryUsageAdapter, } from './adapter'; import type { Conversation } from './types'; @@ -95,6 +96,45 @@ export const getAllChats = async ( return Ok(history); }; +/** + * Get query usage + * @param accessToken + * @returns result containing query usage + */ +export const getQueryUsage = async ( + accessToken: string, +): Promise< + Result< + { + todayQueryCount: number; + remainingQueriesToday: number; + maxQueryAllowancePerDay: number; + nextResetTime: string; + }, + string + > +> => { + const result = await getQueryUsageAdapter(accessToken); + + if (result.isErr()) { + return Err(result.error); + } + + const { + today_query_count, + remaining_queries_today, + max_query_allowance_per_day, + next_reset_time, + } = result.value; + + return Ok({ + todayQueryCount: today_query_count, + remainingQueriesToday: remaining_queries_today, + maxQueryAllowancePerDay: max_query_allowance_per_day, + nextResetTime: next_reset_time, + }); +}; + export { sendMessage, sendMessageStreamed, diff --git a/app/(chat)/types.ts b/app/(chat)/types.ts index e0eab5f..3b85cd8 100644 --- a/app/(chat)/types.ts +++ b/app/(chat)/types.ts @@ -11,6 +11,13 @@ export interface Message { content: string; } +export interface QueryUsage { + todayQueryCount: number; + remainingQueriesToday: number; + maxQueryAllowancePerDay: number; + nextResetTime: string; +} + export type ApiGetConversationResponse = Conversation | null; export type ApiGetConversationMessagesResponse = Message[]; export type ApiCreateConversationResponse = Conversation; @@ -20,3 +27,9 @@ export type ApiGetAllConversationsResponse = Conversation[]; export interface ApiRenameConversationResponse { title: string; } +export interface ApiQueryUsageResponse { + today_query_count: number; + remaining_queries_today: number; + max_query_allowance_per_day: number; + next_reset_time: string; +} diff --git a/components/chat-header.tsx b/components/chat-header.tsx index 20504da..8b1795a 100644 --- a/components/chat-header.tsx +++ b/components/chat-header.tsx @@ -9,6 +9,7 @@ import { Button } from '@/components/ui/button'; import AppkitButton from './appkit-button'; import { PlusIcon } from './icons'; +import { QueryUsage } from './query-usage'; import { useSidebar } from './ui/sidebar'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; @@ -41,7 +42,8 @@ function PureChatHeader() { )} -
+
+
diff --git a/components/chat.tsx b/components/chat.tsx index fd89296..f202b4c 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -46,6 +46,7 @@ export function Chat({ generateId: generateUUID, onFinish: () => { mutate('/api/history'); + mutate('/api/query-usage'); }, onError: (error) => { toast.error(error.message); diff --git a/components/query-usage.tsx b/components/query-usage.tsx new file mode 100644 index 0000000..acc1131 --- /dev/null +++ b/components/query-usage.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { formatDistanceToNow } from 'date-fns'; +import useSWR from 'swr'; + +import type { QueryUsage as QueryUsageType } from '@/app/(chat)/types'; +import { fetcher } from '@/lib/utils'; + +import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; + +export function QueryUsage() { + const { data, isLoading } = useSWR( + '/api/query-usage', + fetcher, + ); + + if (isLoading) { + return ( +
+
+ Loading usage... +
+ ); + } + + if (!data) return null; + + const { + todayQueryCount, + remainingQueriesToday, + maxQueryAllowancePerDay, + nextResetTime, + } = data; + + const resetTime = formatDistanceToNow(new Date(nextResetTime), { + addSuffix: true, + }); + + return ( + + +
+ Credits + + {remainingQueriesToday} / {maxQueryAllowancePerDay} + +
+
+ +
+
+ Used today: + {todayQueryCount} +
+
+ Resets: + {resetTime} +
+
+
+
+ ); +} From bde97ddfe079f2d1e8b33a925e5da96ef2d6c0bb Mon Sep 17 00:00:00 2001 From: Mohammad Kermani Date: Wed, 9 Apr 2025 09:53:58 +0000 Subject: [PATCH 2/2] fix: hide query usage box if user is not authenticated --- app/layout.tsx | 21 ++++++++++++--------- components/query-usage.tsx | 7 ++++++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index a1679bf..658fd63 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,5 @@ import type { Metadata } from 'next'; +import { SessionProvider } from 'next-auth/react'; import { headers } from 'next/headers'; import { Toaster } from 'sonner'; import { cookieToInitialState } from 'wagmi'; @@ -59,15 +60,17 @@ export default async function RootLayout({ - - - {children} - + + + + {children} + + diff --git a/components/query-usage.tsx b/components/query-usage.tsx index acc1131..8931d14 100644 --- a/components/query-usage.tsx +++ b/components/query-usage.tsx @@ -1,6 +1,7 @@ 'use client'; import { formatDistanceToNow } from 'date-fns'; +import { useSession } from 'next-auth/react'; import useSWR from 'swr'; import type { QueryUsage as QueryUsageType } from '@/app/(chat)/types'; @@ -9,11 +10,15 @@ import { fetcher } from '@/lib/utils'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; export function QueryUsage() { + const { status } = useSession(); const { data, isLoading } = useSWR( - '/api/query-usage', + status === 'authenticated' ? '/api/query-usage' : null, fetcher, + { revalidateOnFocus: false, revalidateOnReconnect: false }, ); + if (status !== 'authenticated') return null; + if (isLoading) { return (