Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions app/(chat)/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
ApiSendMessageStreamedResponse,
ApiGetAllConversationsResponse,
ApiRenameConversationResponse,
ApiQueryUsageResponse,
} from '@/app/(chat)/types';
import config from '@/config';
import { extractErrorMessageOrDefault } from '@/lib/utils';
Expand Down Expand Up @@ -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<Result<ApiQueryUsageResponse, string>> => {
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',
),
);
}
};
26 changes: 26 additions & 0 deletions app/(chat)/api/query-usage/route.ts
Original file line number Diff line number Diff line change
@@ -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);
}
40 changes: 40 additions & 0 deletions app/(chat)/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
renameConversation,
getConversation,
getAllConversations,
getQueryUsage as getQueryUsageAdapter,
} from './adapter';
import type { Conversation } from './types';

Expand Down Expand Up @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions app/(chat)/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
21 changes: 12 additions & 9 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -59,15 +60,17 @@ export default async function RootLayout({
</head>
<body className="antialiased">
<ContextProvider initialState={initialState}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<Toaster position="top-center" />
{children}
</ThemeProvider>
<SessionProvider>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<Toaster position="top-center" />
{children}
</ThemeProvider>
</SessionProvider>
</ContextProvider>
</body>
</html>
Expand Down
4 changes: 3 additions & 1 deletion components/chat-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -41,7 +42,8 @@ function PureChatHeader() {
</Tooltip>
)}

<div className="order-4 m-4 md:ml-auto">
<div className="order-4 m-4 md:ml-auto flex items-center gap-4">
<QueryUsage />
<AppkitButton />
</div>
</header>
Expand Down
1 change: 1 addition & 0 deletions components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function Chat({
generateId: generateUUID,
onFinish: () => {
mutate('/api/history');
mutate('/api/query-usage');
},
onError: (error) => {
toast.error(error.message);
Expand Down
68 changes: 68 additions & 0 deletions components/query-usage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'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';
import { fetcher } from '@/lib/utils';

import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';

export function QueryUsage() {
const { status } = useSession();
const { data, isLoading } = useSWR<QueryUsageType>(
status === 'authenticated' ? '/api/query-usage' : null,
fetcher,
{ revalidateOnFocus: false, revalidateOnReconnect: false },
);

if (status !== 'authenticated') return null;

if (isLoading) {
return (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="size-4 animate-pulse rounded-full bg-muted" />
<span>Loading usage...</span>
</div>
);
}

if (!data) return null;

const {
todayQueryCount,
remainingQueriesToday,
maxQueryAllowancePerDay,
nextResetTime,
} = data;

const resetTime = formatDistanceToNow(new Date(nextResetTime), {
addSuffix: true,
});

return (
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center justify-between text-sm cursor-pointer border rounded-md px-2 py-1 dark:border-zinc-700">
<span className="text-muted-foreground mr-2">Credits</span>
<span className="font-medium">
{remainingQueriesToday} / {maxQueryAllowancePerDay}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Used today:</span>
<span className="font-medium">{todayQueryCount}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Resets:</span>
<span className="font-medium">{resetTime}</span>
</div>
</div>
</TooltipContent>
</Tooltip>
);
}