forked from dotneet/smart-chatbot-ui
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fixes useMemo warning add error message to admin page
- Loading branch information
Showing
9 changed files
with
330 additions
and
1 deletion.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './api/admin'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
import { useEffect, useMemo, useReducer, useState } from "react"; | ||
import Link from 'next/link' | ||
import { trpc } from '@/utils/trpc'; | ||
import { UsageReport } from "@/types/admin"; | ||
|
||
import { | ||
ColumnDef, | ||
flexRender, | ||
getCoreRowModel, | ||
getSortedRowModel, | ||
SortingState, | ||
useReactTable, | ||
} from '@tanstack/react-table' | ||
import { useSession } from "next-auth/react"; | ||
import { UserRole } from "@/types/user"; | ||
|
||
function UsageReportTable({ start }: { start: Date }) { | ||
const end = new Date(start.getFullYear(), start.getMonth() + 1); | ||
const startTs = start.valueOf(); | ||
const endTs = end.valueOf(); | ||
const usageReportQuery = trpc.admin.usageReport.useQuery({ start: startTs, end: endTs }); | ||
const [data, setData] = useState<UsageReport[]>([]); | ||
const [sorting, setSorting] = useState<SortingState>([]) | ||
const columns = useMemo<ColumnDef<UsageReport>[]>( | ||
() => { | ||
const USDollar = new Intl.NumberFormat('en-US', { | ||
style: 'currency', | ||
currency: 'USD', | ||
}); | ||
return [ | ||
{ | ||
header: 'User', | ||
footer: props => props.column.id, | ||
columns: [ | ||
{ | ||
accessorFn: row => row.user.name, | ||
id: 'name', | ||
cell: info => info.getValue(), | ||
header: 'Name', | ||
footer: props => props.column.id, | ||
}, | ||
{ | ||
accessorFn: row => row.user.email, | ||
id: 'email', | ||
cell: info => info.getValue(), | ||
header: 'Email', | ||
footer: props => props.column.id, | ||
}, | ||
], | ||
}, | ||
{ | ||
header: 'Usage', | ||
footer: props => props.column.id, | ||
columns: [ | ||
{ | ||
accessorKey: 'totalPriceUSD', | ||
cell: cell => USDollar.format(cell.getValue()), | ||
header: 'Cost', | ||
footer: props => props.column.id, | ||
}, | ||
{ | ||
accessorKey: 'conversions', | ||
header: 'Conversions', | ||
footer: props => props.column.id, | ||
}, | ||
{ | ||
accessorKey: 'tokens', | ||
header: 'Tokens', | ||
footer: props => props.column.id, | ||
}, | ||
] | ||
} | ||
] | ||
}, | ||
[] | ||
) | ||
const table = useReactTable({ | ||
data, | ||
columns, | ||
state: { | ||
sorting, | ||
}, | ||
onSortingChange: setSorting, | ||
getCoreRowModel: getCoreRowModel(), | ||
getSortedRowModel: getSortedRowModel(), | ||
// debugTable: true, | ||
}) | ||
|
||
useEffect(() => { | ||
// console.info(apiResponse); | ||
setData(usageReportQuery.data || []); | ||
}, [usageReportQuery.data]); | ||
|
||
if (usageReportQuery.isLoading) { | ||
return <div>Loading...</div> | ||
} | ||
if (usageReportQuery.isError) { | ||
return <div>Error: {usageReportQuery.error.message}</div> | ||
} | ||
|
||
return ( | ||
<table className="w-full border-collapse border border-slate-500"> | ||
<thead> | ||
{table.getHeaderGroups().map(headerGroup => ( | ||
<tr key={headerGroup.id}> | ||
{headerGroup.headers.map(header => { | ||
return ( | ||
<th className="border border-slate-600" key={header.id} colSpan={header.colSpan}> | ||
{header.isPlaceholder ? null : ( | ||
<div | ||
{...{ | ||
className: header.column.getCanSort() | ||
? 'cursor-pointer select-none' | ||
: '', | ||
onClick: header.column.getToggleSortingHandler(), | ||
}} | ||
> | ||
{flexRender( | ||
header.column.columnDef.header, | ||
header.getContext() | ||
)} | ||
{{ | ||
asc: ' 🔼', | ||
desc: ' 🔽', | ||
}[header.column.getIsSorted() as string] ?? null} | ||
</div> | ||
)} | ||
</th> | ||
) | ||
})} | ||
</tr> | ||
))} | ||
</thead> | ||
<tbody> | ||
{table | ||
.getRowModel() | ||
.rows | ||
.map(row => { | ||
return ( | ||
<tr key={row.id}> | ||
{row.getVisibleCells().map(cell => { | ||
return ( | ||
<td className="border border-slate-700" key={cell.id}> | ||
{flexRender( | ||
cell.column.columnDef.cell, | ||
cell.getContext() | ||
)} | ||
</td> | ||
) | ||
})} | ||
</tr> | ||
) | ||
})} | ||
</tbody> | ||
</table> | ||
); | ||
} | ||
|
||
function AdminPage() { | ||
const session = useSession() | ||
const isAdminUser: boolean = session.data?.user?.role === UserRole.ADMIN; | ||
const now = new Date(); | ||
const [startDate, setStartDate] = useState(new Date(now.getFullYear(), now.getMonth())); | ||
const handlePreviousMonth = () => { | ||
let currentMonth = startDate.getMonth(); | ||
let currentYear = startDate.getFullYear(); | ||
if (currentMonth > 0) { | ||
currentMonth -= 1; | ||
} else { | ||
currentMonth = 11; | ||
currentYear -= 1; | ||
} | ||
setStartDate(new Date(currentYear, currentMonth)); | ||
}; | ||
|
||
const handleNextMonth = () => { | ||
let currentMonth = startDate.getMonth(); | ||
let currentYear = startDate.getFullYear(); | ||
if (currentMonth < 11) { | ||
currentMonth += 1; | ||
|
||
} else { | ||
currentMonth = 0; | ||
currentYear += 1; | ||
} | ||
setStartDate(new Date(currentYear, currentMonth)); | ||
}; | ||
if (!session.data?.user) { | ||
return <h1 className="text-white">You need to <Link className="underline text-blue-600 hover:text-blue-800 visited:text-purple-600" href="/api/auth/signin?callbackUrl=%2Fadmin">log in</Link> to view this page.</h1> | ||
} | ||
if (!isAdminUser) { | ||
return <h1 className="text-white">Admin permission required.</h1> | ||
} | ||
return ( | ||
<div className="container mx-auto text-white"> | ||
<div className="flex flex-row items-center justify-center"> | ||
<button className="flex w-1/4 cursor-pointer select-none items-center justify-start gap-3 rounded-md py-3 px-3 text-[14px] leading-3 text-white transition-colors duration-200 hover:bg-gray-500/10" | ||
onClick={handlePreviousMonth}> | ||
<svg className="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 8 14"> | ||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 1 1.3 6.326a.91.91 0 0 0 0 1.348L7 13" /> | ||
</svg> | ||
Previous Month | ||
</button> | ||
<h1 className="flex w-1/2 items-center justify-center">Monthly Cost Summary: {startDate.getFullYear()}/{startDate.getMonth() + 1}</h1> {/* Months are 0-indexed in JavaScript */} | ||
<button className="flex w-1/4 cursor-pointer select-none items-center justify-end gap-3 rounded-md py-3 px-3 text-[14px] leading-3 text-white transition-colors duration-200 hover:bg-gray-500/10" | ||
onClick={handleNextMonth}> | ||
Next Month | ||
<svg className="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 8 14"> | ||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 13 5.7-5.326a.909.909 0 0 0 0-1.348L1 1" /> | ||
</svg> | ||
</button> | ||
</div> | ||
<div className="flex flex-row items-center justify-center"> | ||
<UsageReportTable start={startDate} /> | ||
</div> | ||
</div> | ||
); | ||
} | ||
export default AdminPage; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './admin'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { validateAdminAccess } from '../context'; | ||
import { procedure, router } from '../trpc'; | ||
import { z } from 'zod'; | ||
import { getLlmUsageReport } from '@/utils/server/admin'; | ||
|
||
export const admin = router({ | ||
usageReport: procedure | ||
.input(z.object({ start: z.number(), end: z.number() })) | ||
.query(async ({ ctx, input }) => { | ||
await validateAdminAccess(ctx); | ||
let start = new Date(input.start); | ||
let end = new Date(input.end); | ||
return getLlmUsageReport(start, end); | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import * as z from 'zod'; | ||
import { UserSchema } from './user'; | ||
import { TokenUsageCountSchema } from './llmUsage'; | ||
|
||
export const UsageReportSchema = z.object({ | ||
user: UserSchema, | ||
totalPriceUSD: z.number(), | ||
conversions: z.number(), | ||
tokens: z.number(), | ||
}); | ||
|
||
export type UsageReport = z.infer<typeof UsageReportSchema>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { UsageReport } from "@/types/admin"; | ||
import { getDb } from "./storage"; | ||
|
||
export async function getLlmUsageReport(start: Date, end: Date): Promise<UsageReport[]> { | ||
console.info("getLlmUsageReport", start, end) | ||
const db = await getDb(); | ||
const result = await db.collection("userLlmUsage") | ||
.aggregate<UsageReport>([ | ||
{ | ||
$match: { | ||
date: { | ||
$gte: start, | ||
$lt: end, | ||
} | ||
} | ||
}, | ||
{ | ||
$group: { | ||
_id: "$userId", | ||
totalPriceUSD: { | ||
$sum: "$totalPriceUSD" | ||
}, | ||
conversions: { | ||
$count: {}, | ||
}, | ||
tokens: { | ||
$sum: "$tokens.total", | ||
}, | ||
} | ||
}, | ||
{ | ||
$lookup: { | ||
from: 'users', // join with users collection | ||
localField: '_id', | ||
foreignField: '_id', | ||
as: 'user' | ||
} | ||
}, | ||
{ | ||
$unwind: { | ||
path: "$user", | ||
} | ||
} | ||
]).toArray(); | ||
console.info("getLlmUsageReport result", result.length) | ||
return result; | ||
} |