Spaces:
Sleeping
Sleeping
Ananth Shyam
feat: enhance analytics API with freshness computation and coverage gaps endpoint
e69f4cf | import { useQuery } from '@tanstack/react-query' | |
| import { | |
| RadarChart, Radar, PolarGrid, PolarAngleAxis, ResponsiveContainer, Tooltip, | |
| } from 'recharts' | |
| import { apiFetch } from '@/lib/http' | |
| import { LoadingSkeleton } from '@/components/common/LoadingSkeleton' | |
| import { cn } from '@/lib/utils' | |
| interface DomainScore { | |
| domain: string | |
| coverage: number | |
| freshness: number | null | |
| accuracy: number | null | |
| score: number | |
| } | |
| interface HealthResponse { | |
| overall_score: number | |
| domains: DomainScore[] | |
| } | |
| async function fetchHealth(): Promise<HealthResponse> { | |
| const res = await apiFetch('/api/analytics/knowledge-health') | |
| return res.json() | |
| } | |
| function ScoreBadge({ value }: { value: number }) { | |
| const pct = Math.round(value * 100) | |
| const colour = | |
| pct >= 80 ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' : | |
| pct >= 60 ? 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400' : | |
| 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400' | |
| return ( | |
| <span className={cn('rounded-full px-2 py-0.5 text-xs font-semibold', colour)}> | |
| {pct}% | |
| </span> | |
| ) | |
| } | |
| function HeatCell({ value }: { value: number | null }) { | |
| if (value === null) { | |
| return ( | |
| <td className="px-2 py-2 text-center"> | |
| <div className="mx-auto h-8 w-16 rounded flex items-center justify-center text-xs font-medium bg-stone-100 text-stone-400 dark:bg-stone-800 dark:text-stone-500"> | |
| N/A | |
| </div> | |
| </td> | |
| ) | |
| } | |
| const pct = Math.round(value * 100) | |
| const bg = | |
| pct >= 80 ? 'bg-green-500' : | |
| pct >= 60 ? 'bg-amber-400' : | |
| pct >= 40 ? 'bg-orange-400' : 'bg-red-500' | |
| return ( | |
| <td className="px-2 py-2 text-center"> | |
| <div | |
| className={cn('mx-auto h-8 w-16 rounded flex items-center justify-center text-xs font-medium text-white', bg)} | |
| title={`${pct}%`} | |
| > | |
| {pct}% | |
| </div> | |
| </td> | |
| ) | |
| } | |
| export function KnowledgeHealthDashboard() { | |
| const { data, isLoading } = useQuery({ | |
| queryKey: ['analytics-knowledge-health'], | |
| queryFn: fetchHealth, | |
| staleTime: 300_000, | |
| }) | |
| const overall = data ? Math.round(data.overall_score * 100) : 0 | |
| const radarData = data?.domains.map((d) => ({ | |
| domain: d.domain, | |
| Coverage: Math.round(d.coverage * 100), | |
| Freshness: d.freshness !== null ? Math.round(d.freshness * 100) : 0, | |
| Accuracy: d.accuracy !== null ? Math.round(d.accuracy * 100) : 0, | |
| })) ?? [] | |
| return ( | |
| <div className="flex flex-col gap-6"> | |
| {/* Overall score */} | |
| <div className="rounded-xl border border-surface-subtle p-5"> | |
| <div className="flex items-center justify-between"> | |
| <div> | |
| <p className="text-sm font-medium text-stone-500">Overall Knowledge Health</p> | |
| {isLoading ? ( | |
| <div className="mt-1 h-8 w-20 animate-pulse rounded bg-stone-200 dark:bg-stone-700" /> | |
| ) : ( | |
| <p className={cn( | |
| 'text-4xl font-bold', | |
| overall >= 80 ? 'text-green-600' : | |
| overall >= 60 ? 'text-amber-500' : 'text-red-500', | |
| )}> | |
| {overall}% | |
| </p> | |
| )} | |
| </div> | |
| {!isLoading && ( | |
| <div className="text-right text-sm text-stone-400"> | |
| <p>{data?.domains.length ?? 0} domains tracked</p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| <div className="grid gap-6 lg:grid-cols-2"> | |
| {/* Radar chart */} | |
| <div className="rounded-xl border border-surface-subtle p-5"> | |
| <p className="mb-4 text-sm font-medium text-stone-500">Coverage / Freshness / Accuracy</p> | |
| {isLoading ? ( | |
| <LoadingSkeleton rows={4} /> | |
| ) : ( | |
| <ResponsiveContainer width="100%" height={240}> | |
| <RadarChart data={radarData}> | |
| <PolarGrid stroke="#e7e5e4" /> | |
| <PolarAngleAxis dataKey="domain" tick={{ fontSize: 11 }} /> | |
| <Tooltip /> | |
| <Radar name="Coverage" dataKey="Coverage" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.15} /> | |
| <Radar name="Freshness" dataKey="Freshness" stroke="#22c55e" fill="#22c55e" fillOpacity={0.15} /> | |
| <Radar name="Accuracy" dataKey="Accuracy" stroke="#f59e0b" fill="#f59e0b" fillOpacity={0.15} /> | |
| </RadarChart> | |
| </ResponsiveContainer> | |
| )} | |
| </div> | |
| {/* Heatmap table */} | |
| <div className="rounded-xl border border-surface-subtle p-5"> | |
| <p className="mb-4 text-sm font-medium text-stone-500">Domain Breakdown</p> | |
| {isLoading ? ( | |
| <LoadingSkeleton rows={4} /> | |
| ) : ( | |
| <div className="overflow-x-auto"> | |
| <table className="w-full text-sm"> | |
| <thead> | |
| <tr className="border-b border-surface-subtle text-xs text-stone-400"> | |
| <th className="py-2 text-left font-medium">Domain</th> | |
| <th className="px-2 py-2 text-center font-medium">Coverage</th> | |
| <th className="px-2 py-2 text-center font-medium">Freshness</th> | |
| <th className="px-2 py-2 text-center font-medium">Accuracy</th> | |
| <th className="px-2 py-2 text-center font-medium">Score</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {(data?.domains ?? []).map((d) => ( | |
| <tr key={d.domain} className="border-b border-surface-subtle/50 last:border-0"> | |
| <td className="py-2 font-medium">{d.domain}</td> | |
| <HeatCell value={d.coverage} /> | |
| <HeatCell value={d.freshness} /> | |
| <HeatCell value={d.accuracy} /> | |
| <td className="px-2 py-2 text-center"> | |
| <ScoreBadge value={d.score} /> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } | |