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",