"use client"; import { useRef, useEffect } from "react"; import ReactECharts from "echarts-for-react"; import type { ECharts } from "echarts"; import { Card as ShadcnCard } from "@/components/ui/card"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Label } from "@/components/ui/label"; import { Skeleton } from "@/components/ui/skeleton"; import { MetricToggle } from "@/components/ui/metric-toggle"; import { useTranslation } from "react-i18next"; import { PieChartOutlined } from "@ant-design/icons"; import { motion } from "framer-motion"; interface ModelUsage { model_name: string; total_cost: number; total_count: number; } interface ModelDistributionChartProps { loading: boolean; models: ModelUsage[]; metric: "cost" | "count"; onMetricChange: (metric: "cost" | "count") => void; } const getPieOption = ( models: ModelUsage[], metric: "cost" | "count", t: (key: string) => string ) => { const pieData = models .map((item) => ({ type: item.model_name, value: metric === "cost" ? Number(item.total_cost) : item.total_count, })) .filter((item) => item.value > 0); const total = pieData.reduce((sum, item) => sum + item.value, 0); const sortedData = [...pieData] .sort((a, b) => b.value - a.value) .reduce((acc, curr) => { const percentage = (curr.value / total) * 100; if (percentage < 5) { const otherIndex = acc.findIndex( (item) => item.name === t("panel.modelUsage.others") ); if (otherIndex >= 0) { acc[otherIndex].value += curr.value; } else { acc.push({ name: t("panel.modelUsage.others"), value: curr.value, }); } } else { acc.push({ name: curr.type, value: curr.value, }); } return acc; }, [] as { name: string; value: number }[]); const isSmallScreen = window.innerWidth < 640; return { tooltip: { show: true, trigger: "item", backgroundColor: "rgba(255, 255, 255, 0.98)", borderColor: "rgba(0, 0, 0, 0.05)", borderWidth: 1, padding: [14, 18], textStyle: { color: "#333", fontSize: 13, lineHeight: 20, }, formatter: (params: any) => { const percentage = ((params.value / total) * 100).toFixed(1); return `
${params.name}
${metric === "cost" ? t("panel.byAmount") : t("panel.byCount")} ${ metric === "cost" ? `${t("common.currency")}${params.value.toFixed(4)}` : `${params.value} ${t("common.count")}` }
${percentage}%
`; }, extraCssText: "box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); border-radius: 8px;", }, legend: { show: true, orient: "horizontal", bottom: isSmallScreen ? 20 : 10, type: "scroll", itemWidth: 16, itemHeight: 16, itemGap: 20, textStyle: { fontSize: 13, color: "#555", padding: [0, 0, 0, 4], }, pageIconSize: 12, pageTextStyle: { color: "#666", }, }, series: [ { name: metric === "cost" ? t("panel.byAmount") : t("panel.byCount"), type: "pie", radius: isSmallScreen ? ["35%", "65%"] : ["45%", "75%"], center: ["50%", "45%"], avoidLabelOverlap: false, itemStyle: { borderRadius: 6, borderWidth: 2, borderColor: "#fff", shadowBlur: 8, shadowColor: "rgba(0, 0, 0, 0.1)", }, label: { show: !isSmallScreen, position: "outside", alignTo: "labelLine", margin: 6, formatter: (params: any) => { const percentage = ((params.value / total) * 100).toFixed(1); return [ `{name|${params.name}}`, `{value|${ metric === "cost" ? `${t("common.currency")}${params.value.toFixed(4)}` : `${params.value} ${t("common.count")}` }}`, `{per|${percentage}%}`, ].join("\n"); }, rich: { name: { fontSize: 13, color: "#444", padding: [0, 0, 3, 0], fontWeight: 500, width: 120, overflow: "break", }, value: { fontSize: 12, color: "#666", padding: [3, 0], fontFamily: "monospace", }, per: { fontSize: 12, color: "#888", padding: [2, 0, 0, 0], }, }, lineHeight: 16, }, labelLayout: { hideOverlap: true, moveOverlap: "shiftY", }, labelLine: { show: !isSmallScreen, length: 20, length2: 20, minTurnAngle: 90, maxSurfaceAngle: 90, smooth: true, }, data: sortedData, zlevel: 0, padAngle: 2, emphasis: { scale: true, scaleSize: 8, focus: "self", itemStyle: { shadowBlur: 16, shadowOffsetX: 0, shadowColor: "rgba(0, 0, 0, 0.15)", }, label: { show: !isSmallScreen, }, labelLine: { show: !isSmallScreen, lineStyle: { width: 2, }, }, }, select: { disabled: true, }, }, ], graphic: [ { type: "text", left: "center", top: "40%", style: { text: metric === "cost" ? `${t("common.total")}\n${t("common.currency")}${total.toFixed( 2 )}` : `${t("common.total")}\n${total}${t("common.count")}`, textAlign: "center", fontSize: 15, fontWeight: "500", lineHeight: 22, fill: "#333", }, zlevel: 2, }, ], animation: true, animationDuration: 500, universalTransition: true, }; }; export default function ModelDistributionChart({ loading, models, metric, onMetricChange, }: ModelDistributionChartProps) { const chartRef = useRef(); const { t } = useTranslation("common"); useEffect(() => { const handleResize = () => { if (chartRef.current) { chartRef.current.resize(); chartRef.current.setOption(getPieOption(models, metric, t)); } }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [metric, models, t]); return (

{t("panel.modelUsage.title")}

{loading ? (
) : (
(chartRef.current = instance)} />
)}
); }