From 11dfdbb7a3afa580cb0071ee2d6bffa15a46f5d7 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Mon, 30 Mar 2026 16:37:49 -0300 Subject: [PATCH] feat(analytics): add diversity score card UI and diversity API route Implement DiversityScoreCard component to fetch and display provider diversity score with loading state and conditional styling, integrate it into AnalyticsPage overview, and add a new API route at src/app/api/analytics/diversity/route.ts to return the diversity report using getDiversityReport --- .../components/DiversityScoreCard.tsx | 136 ++++++++++++++++++ .../(dashboard)/dashboard/analytics/page.tsx | 12 +- src/app/api/analytics/diversity/route.ts | 13 ++ 3 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/analytics/components/DiversityScoreCard.tsx create mode 100644 src/app/api/analytics/diversity/route.ts diff --git a/src/app/(dashboard)/dashboard/analytics/components/DiversityScoreCard.tsx b/src/app/(dashboard)/dashboard/analytics/components/DiversityScoreCard.tsx new file mode 100644 index 0000000000..de4aa724c6 --- /dev/null +++ b/src/app/(dashboard)/dashboard/analytics/components/DiversityScoreCard.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Card } from "@/shared/components"; +import { useTranslations } from "next-intl"; + +export default function DiversityScoreCard() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const t = useTranslations("analytics"); + + useEffect(() => { + fetch("/api/analytics/diversity") + .then((res) => res.json()) + .then((json) => { + setData(json); + setLoading(false); + }) + .catch((err) => { + console.error(err); + setLoading(false); + }); + }, []); + + if (loading || !data) { + return ( + +
+
+ ); + } + + const scorePercentage = Math.round((data.score || 0) * 100); + + let riskColor = "text-green-500"; + let gaugeColor = "bg-green-500"; + let riskLabel = "Healthy Distribution"; + + if (scorePercentage < 40) { + riskColor = "text-red-500"; + gaugeColor = "bg-red-500"; + riskLabel = "High Vendor Lock-in Risk"; + } else if (scorePercentage < 70) { + riskColor = "text-amber-500"; + gaugeColor = "bg-amber-500"; + riskLabel = "Moderate Distribution"; + } + + return ( + +
+ pie_chart +

+ Provider Diversity Score +

+ + Shannon Entropy + +
+ +
+
+ + {scorePercentage}% + + {riskLabel} +
+ + {/* Simple CSS Donut */} +
+ + + + +
+
+ +
+

+ Provider Share +

+ + {Object.keys(data.providers || {}).length === 0 ? ( +
+ No recent usage data available. +
+ ) : ( +
+ {Object.entries(data.providers) + .sort(([, a]: any, [, b]: any) => b.share - a.share) + .slice(0, 4) // Top 4 providers + .map(([provider, stat]: [string, any]) => ( +
+
+ + {provider} + + + {Math.round(stat.share * 100)}% + +
+
+
+
+
+ ))} +
+ )} +
+ +
+ Window: {data.windowSize} reqs + Based on Last {Math.round(data.ttlMs / 60000)} mins +
+ + ); +} diff --git a/src/app/(dashboard)/dashboard/analytics/page.tsx b/src/app/(dashboard)/dashboard/analytics/page.tsx index 3f97518466..87aa0f69e6 100644 --- a/src/app/(dashboard)/dashboard/analytics/page.tsx +++ b/src/app/(dashboard)/dashboard/analytics/page.tsx @@ -4,6 +4,7 @@ import { useState, Suspense } from "react"; import { UsageAnalytics, CardSkeleton, SegmentedControl } from "@/shared/components"; import EvalsTab from "../usage/components/EvalsTab"; import SearchAnalyticsTab from "./SearchAnalyticsTab"; +import DiversityScoreCard from "./components/DiversityScoreCard"; import { useTranslations } from "next-intl"; export default function AnalyticsPage() { @@ -38,9 +39,14 @@ export default function AnalyticsPage() { /> {activeTab === "overview" && ( - }> - - +
+
+ +
+ }> + + +
)} {activeTab === "evals" && } {activeTab === "search" && } diff --git a/src/app/api/analytics/diversity/route.ts b/src/app/api/analytics/diversity/route.ts new file mode 100644 index 0000000000..6e00929eec --- /dev/null +++ b/src/app/api/analytics/diversity/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; +import { getDiversityReport } from "../../../../../open-sse/services/autoCombo/providerDiversity"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + try { + const report = getDiversityReport(); + return NextResponse.json(report); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +}