Skip to content

Commit 48e79f2

Browse files
authored
Merge pull request #40 from pattern-tech/feat/accounting
feat: add query usage box
2 parents 90784f8 + bde97dd commit 48e79f2

File tree

8 files changed

+198
-10
lines changed

8 files changed

+198
-10
lines changed

app/(chat)/adapter.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
ApiSendMessageStreamedResponse,
99
ApiGetAllConversationsResponse,
1010
ApiRenameConversationResponse,
11+
ApiQueryUsageResponse,
1112
} from '@/app/(chat)/types';
1213
import config from '@/config';
1314
import { extractErrorMessageOrDefault } from '@/lib/utils';
@@ -359,3 +360,37 @@ export const renameConversation = async (
359360
);
360361
}
361362
};
363+
364+
/**
365+
* Get query usage
366+
* @param accessToken
367+
* @returns result containing query usage
368+
*/
369+
export const getQueryUsage = async (
370+
accessToken: string,
371+
): Promise<Result<ApiQueryUsageResponse, string>> => {
372+
try {
373+
const response = await fetch(`${patternCoreEndpoint}/query-usage`, {
374+
headers: {
375+
Authorization: `Bearer ${accessToken}`,
376+
'Content-Type': 'application/json',
377+
},
378+
});
379+
380+
if (response.ok) {
381+
const queryUsage: ApiQueryUsageResponse = (await response.json()).data;
382+
return Ok(queryUsage);
383+
}
384+
385+
return Err(
386+
`Fetching query usage failed with error code ${response.status}`,
387+
);
388+
} catch (error) {
389+
return Err(
390+
extractErrorMessageOrDefault(
391+
error,
392+
'Unknown error while fetching query usage',
393+
),
394+
);
395+
}
396+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { auth } from '@/app/(auth)/auth';
2+
3+
import { getQueryUsage } from '../../service';
4+
5+
export async function GET() {
6+
const session = await auth();
7+
8+
if (
9+
!session ||
10+
!session.chainId ||
11+
!session.address ||
12+
!session.accessToken
13+
) {
14+
return Response.json('Unauthorized!', { status: 401 });
15+
}
16+
17+
const queryUsageResult = await getQueryUsage(session.accessToken);
18+
19+
if (queryUsageResult.isErr()) {
20+
return new Response(queryUsageResult.error, { status: 400 });
21+
}
22+
23+
const queryUsage = queryUsageResult.value;
24+
25+
return Response.json(queryUsage);
26+
}

app/(chat)/service.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
renameConversation,
88
getConversation,
99
getAllConversations,
10+
getQueryUsage as getQueryUsageAdapter,
1011
} from './adapter';
1112
import type { Conversation } from './types';
1213

@@ -95,6 +96,45 @@ export const getAllChats = async (
9596
return Ok(history);
9697
};
9798

99+
/**
100+
* Get query usage
101+
* @param accessToken
102+
* @returns result containing query usage
103+
*/
104+
export const getQueryUsage = async (
105+
accessToken: string,
106+
): Promise<
107+
Result<
108+
{
109+
todayQueryCount: number;
110+
remainingQueriesToday: number;
111+
maxQueryAllowancePerDay: number;
112+
nextResetTime: string;
113+
},
114+
string
115+
>
116+
> => {
117+
const result = await getQueryUsageAdapter(accessToken);
118+
119+
if (result.isErr()) {
120+
return Err(result.error);
121+
}
122+
123+
const {
124+
today_query_count,
125+
remaining_queries_today,
126+
max_query_allowance_per_day,
127+
next_reset_time,
128+
} = result.value;
129+
130+
return Ok({
131+
todayQueryCount: today_query_count,
132+
remainingQueriesToday: remaining_queries_today,
133+
maxQueryAllowancePerDay: max_query_allowance_per_day,
134+
nextResetTime: next_reset_time,
135+
});
136+
};
137+
98138
export {
99139
sendMessage,
100140
sendMessageStreamed,

app/(chat)/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ export interface Message {
1111
content: string;
1212
}
1313

14+
export interface QueryUsage {
15+
todayQueryCount: number;
16+
remainingQueriesToday: number;
17+
maxQueryAllowancePerDay: number;
18+
nextResetTime: string;
19+
}
20+
1421
export type ApiGetConversationResponse = Conversation | null;
1522
export type ApiGetConversationMessagesResponse = Message[];
1623
export type ApiCreateConversationResponse = Conversation;
@@ -20,3 +27,9 @@ export type ApiGetAllConversationsResponse = Conversation[];
2027
export interface ApiRenameConversationResponse {
2128
title: string;
2229
}
30+
export interface ApiQueryUsageResponse {
31+
today_query_count: number;
32+
remaining_queries_today: number;
33+
max_query_allowance_per_day: number;
34+
next_reset_time: string;
35+
}

app/layout.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Metadata } from 'next';
2+
import { SessionProvider } from 'next-auth/react';
23
import { headers } from 'next/headers';
34
import { Toaster } from 'sonner';
45
import { cookieToInitialState } from 'wagmi';
@@ -59,15 +60,17 @@ export default async function RootLayout({
5960
</head>
6061
<body className="antialiased">
6162
<ContextProvider initialState={initialState}>
62-
<ThemeProvider
63-
attribute="class"
64-
defaultTheme="system"
65-
enableSystem
66-
disableTransitionOnChange
67-
>
68-
<Toaster position="top-center" />
69-
{children}
70-
</ThemeProvider>
63+
<SessionProvider>
64+
<ThemeProvider
65+
attribute="class"
66+
defaultTheme="system"
67+
enableSystem
68+
disableTransitionOnChange
69+
>
70+
<Toaster position="top-center" />
71+
{children}
72+
</ThemeProvider>
73+
</SessionProvider>
7174
</ContextProvider>
7275
</body>
7376
</html>

components/chat-header.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Button } from '@/components/ui/button';
99

1010
import AppkitButton from './appkit-button';
1111
import { PlusIcon } from './icons';
12+
import { QueryUsage } from './query-usage';
1213
import { useSidebar } from './ui/sidebar';
1314
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
1415

@@ -41,7 +42,8 @@ function PureChatHeader() {
4142
</Tooltip>
4243
)}
4344

44-
<div className="order-4 m-4 md:ml-auto">
45+
<div className="order-4 m-4 md:ml-auto flex items-center gap-4">
46+
<QueryUsage />
4547
<AppkitButton />
4648
</div>
4749
</header>

components/chat.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export function Chat({
4646
generateId: generateUUID,
4747
onFinish: () => {
4848
mutate('/api/history');
49+
mutate('/api/query-usage');
4950
},
5051
onError: (error) => {
5152
toast.error(error.message);

components/query-usage.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use client';
2+
3+
import { formatDistanceToNow } from 'date-fns';
4+
import { useSession } from 'next-auth/react';
5+
import useSWR from 'swr';
6+
7+
import type { QueryUsage as QueryUsageType } from '@/app/(chat)/types';
8+
import { fetcher } from '@/lib/utils';
9+
10+
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
11+
12+
export function QueryUsage() {
13+
const { status } = useSession();
14+
const { data, isLoading } = useSWR<QueryUsageType>(
15+
status === 'authenticated' ? '/api/query-usage' : null,
16+
fetcher,
17+
{ revalidateOnFocus: false, revalidateOnReconnect: false },
18+
);
19+
20+
if (status !== 'authenticated') return null;
21+
22+
if (isLoading) {
23+
return (
24+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
25+
<div className="size-4 animate-pulse rounded-full bg-muted" />
26+
<span>Loading usage...</span>
27+
</div>
28+
);
29+
}
30+
31+
if (!data) return null;
32+
33+
const {
34+
todayQueryCount,
35+
remainingQueriesToday,
36+
maxQueryAllowancePerDay,
37+
nextResetTime,
38+
} = data;
39+
40+
const resetTime = formatDistanceToNow(new Date(nextResetTime), {
41+
addSuffix: true,
42+
});
43+
44+
return (
45+
<Tooltip>
46+
<TooltipTrigger asChild>
47+
<div className="flex items-center justify-between text-sm cursor-pointer border rounded-md px-2 py-1 dark:border-zinc-700">
48+
<span className="text-muted-foreground mr-2">Credits</span>
49+
<span className="font-medium">
50+
{remainingQueriesToday} / {maxQueryAllowancePerDay}
51+
</span>
52+
</div>
53+
</TooltipTrigger>
54+
<TooltipContent>
55+
<div className="flex flex-col gap-2">
56+
<div className="flex items-center gap-2">
57+
<span className="text-muted-foreground">Used today:</span>
58+
<span className="font-medium">{todayQueryCount}</span>
59+
</div>
60+
<div className="flex items-center gap-2">
61+
<span className="text-muted-foreground">Resets:</span>
62+
<span className="font-medium">{resetTime}</span>
63+
</div>
64+
</div>
65+
</TooltipContent>
66+
</Tooltip>
67+
);
68+
}

0 commit comments

Comments
 (0)