diff --git a/app/app/(admin)/admin/page.tsx b/app/app/(admin)/admin/page.tsx index c32d89fa9..29c830745 100644 --- a/app/app/(admin)/admin/page.tsx +++ b/app/app/(admin)/admin/page.tsx @@ -1,19 +1,20 @@ // "use client"; import { Grid, Col, Card, Text, AreaChart, Metric } from "@tremor/react"; -import UsageAnalysis from "./usage-analysis"; +// import UsageAnalysis from "./usage-analysis"; +import UsageByModel from "./usage-by-model"; export default function AdminPage() { return ( - - - - - - - Title - KPI 2 - - - + <> + + + {/**/} + {/**/} + + + + + + ); } diff --git a/app/app/(admin)/admin/usage-analysis.tsx b/app/app/(admin)/admin/usage-analysis.tsx index 345d49652..2349f74de 100644 --- a/app/app/(admin)/admin/usage-analysis.tsx +++ b/app/app/(admin)/admin/usage-analysis.tsx @@ -54,105 +54,7 @@ export default async function UsageAnalysis() { // @ts-ignore const log_data = HandleLogData(todayLog); - // console.log("======", log_data); - // const data = [ - // { - // name: "Twitter", - // value: 456, - // href: "https://twitter.com/tremorlabs", - // icon: function TwitterIcon() { - // return ( - // - // - // - // - // ); - // }, - // }, - // { - // name: "Google", - // value: 351, - // href: "https://google.com", - // icon: function GoogleIcon() { - // return ( - // - // - // - // - // ); - // }, - // }, - // { - // name: "GitHub", - // value: 271, - // href: "https://github.com/tremorlabs/tremor", - // icon: function GitHubIcon() { - // return ( - // - // - // - // - // ); - // }, - // }, - // { - // name: "Reddit", - // value: 191, - // href: "https://reddit.com", - // icon: function RedditIcon() { - // return ( - // - // - // - // - // ); - // }, - // }, - // { - // name: "Youtube", - // value: 91, - // href: "https://www.youtube.com/@tremorlabs3079", - // icon: function YouTubeIcon() { - // return ( - // - // - // - // - // ); - // }, - // }, - // ]; // @ts-ignore return ( diff --git a/app/app/(admin)/admin/usage-by-model-chart.tsx b/app/app/(admin)/admin/usage-by-model-chart.tsx new file mode 100644 index 000000000..ba41dc53f --- /dev/null +++ b/app/app/(admin)/admin/usage-by-model-chart.tsx @@ -0,0 +1,22 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import * as echarts from "echarts"; // 导入 echarts + +export default function UsageByModelChart({ + option, +}: { + option: echarts.EChartsOption; +}) { + useEffect(() => { + var chartDom = document.getElementById("usage-by-model-chart"); + var myChart = echarts.init(chartDom); + option && myChart.setOption(option); + }, [option]); // 空数组作为第二个参数,表示仅在组件挂载和卸载时执行 + + return ( +
+ ); +} diff --git a/app/app/(admin)/admin/usage-by-model.tsx b/app/app/(admin)/admin/usage-by-model.tsx new file mode 100644 index 000000000..fbc17e74c --- /dev/null +++ b/app/app/(admin)/admin/usage-by-model.tsx @@ -0,0 +1,207 @@ +import * as echarts from "echarts"; +import { EChartsOption } from "echarts"; +import dynamic from "next/dynamic"; +import prisma from "@/lib/prisma"; +import { estimateTokenLength } from "@/app/utils/token"; + +const UsageByModelChart = dynamic(() => import("./usage-by-model-chart"), { + ssr: false, +}); + +function HandleLogData(todayLog: [{ userName: string; logEntry: string }]) { + const data1 = todayLog.map((log) => { + return { + name: log.userName ?? "unknown", + value: estimateTokenLength(log.logEntry ?? ""), + }; + }); + + type Accumulator = { + [key: string]: number; + }; + const data2 = data1.reduce((acc, item) => { + if (acc[item.name]) { + acc[item.name] += item.value; + } else { + acc[item.name] = item.value; + } + return acc; + }, {}); + const data3 = Object.entries(data2) + .map(([name, value]) => ({ + name, + value, + })) + .sort((a, b) => { + return a.value - b.value; + }); + + const data4 = data3.reduce( + (acc, cur) => { + // @ts-ignore + acc.name.push(cur.name); + // @ts-ignore + acc.value.push(cur.value); + return acc; + }, + { name: [], value: [] }, + ); + return data4; +} + +export default async function UsageByModel() { + // 今天日期的开始和结束 + const startDate = new Date(); + startDate.setHours(0, 0, 0, 0); + + const endDate = new Date(); + endDate.setHours(23, 59, 59, 999); + const todayLog = await prisma.logEntry.findMany({ + where: { + createdAt: { + gte: startDate, // gte 表示 '大于等于' + lte: endDate, // lte 表示 '小于等于' + }, + }, + include: { + user: true, + }, + }); + + // @ts-ignore + const log_data = HandleLogData(todayLog); + + // console.log('[log_data]====---==', log_data) + + const usageByModelOption: EChartsOption = { + tooltip: { + trigger: "axis", + axisPointer: { + type: "shadow", + }, + }, + title: { + show: true, + text: "token使用分析", + }, + legend: {}, + grid: { + left: "3%", + right: "4%", + bottom: "3%", + containLabel: true, + }, + xAxis: [ + { + type: "value", + }, + ], + yAxis: [ + { + type: "category", + data: log_data.name, + }, + ], + series: [ + { + name: "总量", + type: "bar", + emphasis: { + focus: "series", + }, + label: { + show: true, + position: "right", + }, + colorBy: "data", + // progress: { + // show: true + // }, + data: log_data.value, + }, + // { + // name: 'Email', + // type: 'bar', + // stack: 'Ad', + // emphasis: { + // focus: 'series' + // }, + // data: [120, 132, 101, 134, 90, 230, 210] + // }, + // { + // name: 'Union Ads', + // type: 'bar', + // stack: 'Ad', + // emphasis: { + // focus: 'series' + // }, + // data: [220, 182, 191, 234, 290, 330, 310] + // }, + // { + // name: 'Video Ads', + // type: 'bar', + // stack: 'Ad', + // emphasis: { + // focus: 'series' + // }, + // data: [150, 232, 201, 154, 190, 330, 410] + // }, + // { + // name: 'Search Engine', + // type: 'bar', + // data: [862, 1018, 964, 1026, 1679, 1600, 1570], + // emphasis: { + // focus: 'series' + // }, + // // markLine: { + // // lineStyle: { + // // type: 'dashed' + // // }, + // // data: [[{ type: 'min' }, { type: 'max' }]] + // // } + // }, + // { + // name: 'Baidu', + // type: 'bar', + // barWidth: 5, + // stack: 'Search Engine', + // emphasis: { + // focus: 'series' + // }, + // data: [620, 732, 701, 734, 1090, 1130, 1120] + // }, + // { + // name: 'Google', + // type: 'bar', + // stack: 'Search Engine', + // emphasis: { + // focus: 'series' + // }, + // data: [120, 132, 101, 134, 290, 230, 220] + // }, + // { + // name: 'Bing', + // type: 'bar', + // stack: 'Search Engine', + // emphasis: { + // focus: 'series' + // }, + // data: [60, 72, 71, 74, 190, 130, 110] + // }, + // { + // name: 'Others', + // type: 'bar', + // stack: 'Search Engine', + // emphasis: { + // focus: 'series' + // }, + // data: [62, 82, 91, 84, 109, 110, 120] + // } + ], + }; + return ( + <> + + + ); +} diff --git a/app/app/(admin)/layout.tsx b/app/app/(admin)/layout.tsx index 8b7376bc6..240c0e232 100644 --- a/app/app/(admin)/layout.tsx +++ b/app/app/(admin)/layout.tsx @@ -12,12 +12,14 @@ export default async function AdminLayout({ children: ReactNode; }) { return ( -
-
-

+
+
+

Admin Page

- {children} +
+ {children} +
); diff --git a/package.json b/package.json index e03b839e4..b344344db 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@svgr/webpack": "^8.1.0", "@tremor/react": "^3.12.1", "@vercel/analytics": "^1.1.1", + "echarts": "^5.4.3", "emoji-picker-react": "^4.5.15", "fuse.js": "^7.0.0", "html-to-image": "^1.11.11",