add metrics page

This commit is contained in:
2025-05-04 17:12:42 +00:00
parent 43161635d0
commit b5a0f60110
2 changed files with 319 additions and 1 deletions

View File

@@ -0,0 +1,220 @@
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell,
LineChart,
Line,
} from 'recharts';
import { useMetrics } from '../api/reviews';
import { LoadingSpinner } from '../components/common/LoadingSpinner';
const VERDICT_COLORS = {
approve: '#22c55e',
request_changes: '#ef4444',
comment: '#3b82f6',
};
const SEVERITY_COLORS = {
critical: '#dc2626',
high: '#f97316',
medium: '#eab308',
low: '#3b82f6',
info: '#9ca3af',
};
const AGENT_COLORS = {
security: '#ef4444',
style: '#3b82f6',
complexity: '#a855f7',
};
interface StatCardProps {
label: string;
value: string | number;
subtext?: string;
}
function StatCard({ label, value, subtext }: StatCardProps) {
return (
<div className="bg-white border border-gray-200 rounded-lg p-4">
<p className="text-sm text-gray-500">{label}</p>
<p className="text-2xl font-bold text-gray-900 mt-1">{value}</p>
{subtext && <p className="text-xs text-gray-500 mt-1">{subtext}</p>}
</div>
);
}
export function MetricsPage() {
const { data: metrics, isLoading, error } = useMetrics();
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<LoadingSpinner size="lg" />
</div>
);
}
if (error || !metrics) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-center">
<p className="text-red-800">Failed to load metrics</p>
<p className="text-sm text-red-600 mt-1">{error?.message}</p>
</div>
);
}
const verdictData = Object.entries(metrics.verdict_counts).map(
([name, value]) => ({
name: name === 'request_changes' ? 'Changes' : name,
value,
}),
);
const severityData = Object.entries(metrics.severity_counts).map(
([name, value]) => ({
name: name.charAt(0).toUpperCase() + name.slice(1),
value,
fill: SEVERITY_COLORS[name as keyof typeof SEVERITY_COLORS],
}),
);
const costByAgentData = Object.entries(metrics.cost_by_agent).map(
([name, value]) => ({
name: name.charAt(0).toUpperCase() + name.slice(1),
cost: value,
fill: AGENT_COLORS[name as keyof typeof AGENT_COLORS],
}),
);
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900">Metrics</h1>
<p className="text-sm text-gray-600 mt-1">
Review statistics and trends
</p>
</div>
{/* Stat Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<StatCard label="Total Reviews" value={metrics.total_reviews} />
<StatCard
label="Completed"
value={metrics.completed_reviews}
subtext={`${Math.round((metrics.completed_reviews / metrics.total_reviews) * 100)}% completion rate`}
/>
<StatCard
label="Average Cost"
value={`$${metrics.average_cost_usd.toFixed(4)}`}
subtext="Per review"
/>
<StatCard
label="Total Cost"
value={`$${(metrics.average_cost_usd * metrics.completed_reviews).toFixed(2)}`}
subtext="All reviews"
/>
</div>
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* Verdict Distribution */}
<div className="bg-white border border-gray-200 rounded-lg p-4">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Verdict Distribution
</h2>
<ResponsiveContainer width="100%" height={250}>
<PieChart>
<Pie
data={verdictData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={5}
dataKey="value"
label={({ name, percent }) =>
`${name} (${((percent ?? 0) * 100).toFixed(0)}%)`
}
>
{verdictData.map((_, index) => (
<Cell
key={`cell-${index}`}
fill={
Object.values(VERDICT_COLORS)[
index % Object.values(VERDICT_COLORS).length
]
}
/>
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</div>
{/* Severity Distribution */}
<div className="bg-white border border-gray-200 rounded-lg p-4">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Findings by Severity
</h2>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={severityData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Bar dataKey="value" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Reviews Over Time */}
<div className="bg-white border border-gray-200 rounded-lg p-4">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Reviews Over Time
</h2>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={metrics.reviews_by_day}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line
type="monotone"
dataKey="count"
stroke="#6366f1"
strokeWidth={2}
/>
</LineChart>
</ResponsiveContainer>
</div>
{/* Cost by Agent */}
<div className="bg-white border border-gray-200 rounded-lg p-4">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Cost by Agent
</h2>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={costByAgentData} layout="vertical">
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" tickFormatter={(v) => `$${v.toFixed(2)}`} />
<YAxis type="category" dataKey="name" width={80} />
<Tooltip formatter={(value) => `$${Number(value).toFixed(4)}`} />
<Bar dataKey="cost" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
);
}