Files
tool-evaluator/artifacts/toolrate/src/pages/analytics.tsx
T
opencode 8f2fd89847
Build & Push Docker Image / build (push) Successful in 2m49s
feat(import): bulk tool import (CSV/JSON/YAML) for admins; NetBox-style help button
- Add POST /admin/tools/import with format auto-detect, CSV delimiters
  (comma/semicolon/tab), per-row validation via CreateToolBody, bulk insert,
  audit log entries per imported tool; gated by new 'tool-import' feature
  flag (premium/enterprise; admins always pass)
- Add tool-import-dialog UI (format tabs, delimiter select, textarea, file
  upload, result/error list) behind hasFeature('tool-import')
- Replace FieldHelp question marks and bare GuideHelp links with a NetBox-style
  'Hilfe/Help' outline button (HelpCircle + text) in form headers only
- Sync locales to 482 keys per language (de/en), update handbook docs
  (administration import section, index/plaene feature tables), regenerate
  API client + zod schemas, add yaml dependency
2026-08-04 23:09:11 +02:00

214 lines
10 KiB
TypeScript

import {
useGetAnalyticsSummary,
useGetTopTools,
useGetAnalyticsByCategory,
useGetRatingDistribution,
GetTopToolsMetric
} from "@workspace/api-client-react";
import { useTranslation } from "react-i18next";
import { Layout } from "@/components/layout";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer,
RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, Legend,
LineChart, Line
} from "recharts";
import { BarChart3, TrendingUp, Layers, Activity } from "lucide-react";
export default function Analytics() {
const { t } = useTranslation();
const { data: summary, isLoading: loadingSummary } = useGetAnalyticsSummary();
const { data: topTools, isLoading: loadingTopTools } = useGetTopTools({
limit: 8,
metric: GetTopToolsMetric.combined
});
const { data: categoryStats, isLoading: loadingCategories } = useGetAnalyticsByCategory();
const { data: distribution, isLoading: loadingDistribution } = useGetRatingDistribution();
const topToolsChartData = topTools?.map(t => ({
name: t.tool.name,
score: Number((t.score).toFixed(2)),
ratings: t.ratingCount
})) || [];
const categoryChartData = categoryStats?.map(c => ({
category: c.category,
tools: c.toolCount,
avgScore: (c.avgUsefulness != null && c.avgUsability != null)
? Number(((c.avgUsefulness + c.avgUsability) / 2).toFixed(2))
: 0
})) || [];
const usefulnessData = distribution?.usefulness.map(b => ({ score: `${b.score} Star`, count: b.count })) || [];
const usabilityData = distribution?.usability.map(b => ({ score: `${b.score} Star`, count: b.count })) || [];
return (
<Layout>
<div className="space-y-6 pb-10">
<div>
<h1 className="text-3xl font-bold tracking-tight mb-2">
{t("analytics.title")}
</h1>
<p className="text-muted-foreground">{t("analytics.subtitle")}</p>
</div>
{/* Top KPI Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-muted-foreground">{t("analytics.totalToolsIndexed")}</span>
<Layers className="w-4 h-4 text-muted-foreground" />
</div>
{loadingSummary ? <Skeleton className="h-8 w-16" /> : (
<div className="text-3xl font-bold">{summary?.totalTools || 0}</div>
)}
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-muted-foreground">{t("analytics.totalRatingsCast")}</span>
<Activity className="w-4 h-4 text-muted-foreground" />
</div>
{loadingSummary ? <Skeleton className="h-8 w-16" /> : (
<div className="text-3xl font-bold">{summary?.totalRatings || 0}</div>
)}
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-muted-foreground">{t("analytics.activeCategories")}</span>
<BarChart3 className="w-4 h-4 text-muted-foreground" />
</div>
{loadingSummary ? <Skeleton className="h-8 w-16" /> : (
<div className="text-3xl font-bold">{summary?.categoriesCount || 0}</div>
)}
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-muted-foreground">{t("analytics.avgGlobalScore")}</span>
<TrendingUp className="w-4 h-4 text-muted-foreground" />
</div>
{loadingSummary ? <Skeleton className="h-8 w-16" /> : (
<div className="text-3xl font-bold">{summary?.avgCombined?.toFixed(2) || "0.00"}</div>
)}
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top Tools Chart */}
<Card className="col-span-1 lg:col-span-2">
<CardHeader>
<CardTitle>{t("analytics.top8Tools")}</CardTitle>
<CardDescription>{t("analytics.top8ToolsSub")}</CardDescription>
</CardHeader>
<CardContent>
{loadingTopTools ? (
<Skeleton className="h-[300px] w-full" />
) : (
<div className="h-[300px] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={topToolsChartData} margin={{ top: 20, right: 30, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
<XAxis dataKey="name" tickLine={false} axisLine={false} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
<YAxis domain={[0, 5]} tickLine={false} axisLine={false} tickFormatter={(v) => `${v}`} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
<RechartsTooltip
cursor={{ fill: 'hsl(var(--muted))' }}
contentStyle={{ borderRadius: '8px', border: '1px solid hsl(var(--border))' }}
/>
<Bar dataKey="score" fill="hsl(var(--primary))" radius={[4, 4, 0, 0]} maxBarSize={50} />
</BarChart>
</ResponsiveContainer>
</div>
)}
</CardContent>
</Card>
{/* Category Breakdown */}
<Card>
<CardHeader>
<CardTitle>{t("analytics.toolsByCategory")}</CardTitle>
<CardDescription>{t("analytics.toolsByCategorySub")}</CardDescription>
</CardHeader>
<CardContent>
{loadingCategories ? (
<Skeleton className="h-[300px] w-full" />
) : (
<div className="h-[300px] w-full">
<ResponsiveContainer width="100%" height="100%">
<RadarChart cx="50%" cy="50%" outerRadius="70%" data={categoryChartData}>
<PolarGrid stroke="hsl(var(--border))" />
<PolarAngleAxis dataKey="category" tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
<PolarRadiusAxis angle={30} domain={[0, 'auto']} tick={false} axisLine={false} />
<Radar name={t("analytics.radarTools")} dataKey="tools" stroke="hsl(var(--primary))" fill="hsl(var(--primary))" fillOpacity={0.3} />
<RechartsTooltip />
</RadarChart>
</ResponsiveContainer>
</div>
)}
</CardContent>
</Card>
{/* Rating Distributions */}
<Card>
<CardHeader>
<CardTitle>{t("analytics.globalRatingDistribution")}</CardTitle>
<CardDescription>{t("analytics.globalRatingDistributionSub")}</CardDescription>
</CardHeader>
<CardContent>
{loadingDistribution ? (
<Skeleton className="h-[280px] w-full" />
) : (
<div className="space-y-5">
<div>
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2 block">{t("detail.usefulness")}</span>
<div className="h-[110px] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={usefulnessData} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
<XAxis dataKey="score" tickLine={false} axisLine={false} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
<YAxis hide />
<RechartsTooltip cursor={{ fill: 'hsl(var(--muted))' }} contentStyle={{ borderRadius: '6px', border: '1px solid hsl(var(--border))' }} />
<Bar dataKey="count" fill="hsl(var(--primary))" radius={[4, 4, 0, 0]} maxBarSize={40} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
<div>
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2 block">{t("detail.usability")}</span>
<div className="h-[110px] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={usabilityData} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
<XAxis dataKey="score" tickLine={false} axisLine={false} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
<YAxis hide />
<RechartsTooltip cursor={{ fill: 'hsl(var(--muted))' }} contentStyle={{ borderRadius: '6px', border: '1px solid hsl(var(--border))' }} />
<Bar dataKey="count" fill="hsl(var(--chart-3))" radius={[4, 4, 0, 0]} maxBarSize={40} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</div>
</Layout>
);
}