modcomms/frontend/components/Analytics.tsx
Vadym Samoilenko aeab7d3b18 Rename Legal Agent to Risk & Control Agent across frontend and backend
Updates all display labels (PDF report, campaign page, Knowledge Base card, analytics, status dashboard, checks overview) and aligns internal agent name in backend. Adds migration 010 to update the knowledge base display_name in production DB.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 15:10:32 +01:00

219 lines
No EOL
14 KiB
TypeScript
Executable file

import React, { useEffect, useState } from 'react';
import { UploadIcon } from './icons/UploadIcon';
import { TrendingUpIcon } from './icons/TrendingUpIcon';
import { BugIcon } from './icons/BugIcon';
import { ClockIcon } from './icons/ClockIcon';
import { LightbulbIcon } from './icons/LightbulbIcon';
import { ExclamationTriangleIcon } from './icons/StatusIcons';
import apiService, { AnalyticsResponse, AgencyAnalyticsItem } from '../services/apiService';
// Agent performance is still static for now - would need separate API
const agentPerformance = [
{ name: 'Risk & Control Agent', passRate: 85, avgIssues: 1.2, trend: 'up' },
{ name: 'Brand Agent', passRate: 68, avgIssues: 2.5, trend: 'down' },
{ name: 'Channel Best Practices Agent', passRate: 92, avgIssues: 0.8, trend: 'up' },
{ name: 'Channel Tech Specs Agent', passRate: 71, avgIssues: 1.9, trend: 'stable' },
];
const UpArrow = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={3} stroke="currentColor" className="h-4 w-4"><path strokeLinecap="round" strokeLinejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" /></svg>;
const DownArrow = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={3} stroke="currentColor" className="h-4 w-4"><path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" /></svg>;
const StableLine = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={3} stroke="currentColor" className="h-4 w-4"><path strokeLinecap="round" strokeLinejoin="round" d="M3.75 12h16.5" /></svg>;
const TrendIndicator: React.FC<{ trend: 'up' | 'down' | 'stable' }> = ({ trend }) => {
if (trend === 'up') {
return <div className="flex items-center gap-1.5 text-success"><UpArrow/> Improving</div>;
}
if (trend === 'down') {
return <div className="flex items-center gap-1.5 text-oliver-orange"><DownArrow/> Declining</div>;
}
return <div className="flex items-center gap-1.5 text-oliver-black"><StableLine/> Stable</div>;
};
export const Analytics: React.FC<{ agencyId?: string; isAdmin?: boolean }> = ({ agencyId, isAdmin }) => {
const [analytics, setAnalytics] = useState<AnalyticsResponse | null>(null);
const [agencyAnalytics, setAgencyAnalytics] = useState<AgencyAnalyticsItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadAnalytics = async () => {
setIsLoading(true);
try {
const data = await apiService.getAnalytics(agencyId);
setAnalytics(data);
} catch (error) {
console.error('Failed to load analytics:', error);
} finally {
setIsLoading(false);
}
};
loadAnalytics();
}, [agencyId]);
useEffect(() => {
if (!isAdmin) return;
const loadAgencyAnalytics = async () => {
try {
const data = await apiService.getAnalyticsByAgency();
setAgencyAnalytics(data.agencies);
} catch (error) {
console.error('Failed to load agency analytics:', error);
}
};
loadAgencyAnalytics();
}, [isAdmin]);
// Calculate stats from API data
// Exclude errors from denominator since they weren't successfully reviewed
const reviewedCount = analytics
? (analytics.passed || 0) + (analytics.failed || 0) + (analytics.legal_review || 0)
: 0;
const passRate = reviewedCount > 0
? Math.round((analytics!.passed / reviewedCount) * 100)
: 0;
const stats = [
{ name: 'Proofs Reviewed', value: analytics?.total_reviews?.toString() || '0', icon: UploadIcon },
{ name: 'Pass Rate', value: `${passRate}%`, icon: TrendingUpIcon },
{ name: 'Failed Reviews', value: analytics?.failed?.toString() || '0', icon: BugIcon },
{ name: 'Analysis Errors', value: analytics?.errors?.toString() || '0', icon: ExclamationTriangleIcon },
{ name: 'Legal Review Required', value: analytics?.legal_review?.toString() || '0', icon: ClockIcon },
];
return (
<div className="p-4 sm:p-6 lg:p-8 h-full bg-white">
<header className="mb-8">
<h1 className="text-3xl lg:text-4xl font-semibold text-oliver-black">Performance analytics</h1>
<p className="text-base lg:text-lg text-oliver-black mt-1">Overall usage and performance statistics for the tool.</p>
</header>
{/* Stats Cards */}
<section>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-6">
{stats.map((stat) => {
const Icon = stat.icon;
return (
<div key={stat.name} className="bg-oliver-grey rounded-[10px] p-6 flex items-start border-2 border-grey-300 transition-all hover:shadow-lg">
<div className="p-3 rounded-full bg-oliver-grey text-oliver-black mr-4 flex-shrink-0">
<Icon className="h-9 w-9" />
</div>
<div>
<p className="text-sm font-medium text-oliver-black">{stat.name}</p>
<p className="text-3xl font-bold text-oliver-black mt-1">{stat.value}</p>
</div>
</div>
);
})}
</div>
</section>
{/* Per-Agency Breakdown (admin only) */}
{isAdmin && agencyAnalytics.length > 0 && (
<section className="mt-10">
<h2 className="text-2xl font-semibold text-oliver-black mb-4">Agency performance</h2>
<div className="bg-white rounded-[10px] shadow-md overflow-hidden border border-grey-300">
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-oliver-sky">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-oliver-black uppercase tracking-wider">Agency</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-oliver-black uppercase tracking-wider">Proofs Reviewed</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-oliver-black uppercase tracking-wider">Pass Rate</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-oliver-black uppercase tracking-wider">Failed</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-oliver-black uppercase tracking-wider">Errors</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-oliver-black uppercase tracking-wider">Legal Review</th>
</tr>
</thead>
<tbody className="divide-y divide-grey-300">
{agencyAnalytics.map((agency, index) => {
const reviewed = agency.passed + agency.failed + agency.legal_review;
const rate = reviewed > 0 ? Math.round((agency.passed / reviewed) * 100) : 0;
const isSelected = agencyId === agency.agency_id;
return (
<tr
key={agency.agency_id}
className={
isSelected
? 'bg-blue-50'
: index % 2 === 0
? 'bg-white'
: 'bg-oliver-grey'
}
>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-oliver-black">
{agency.agency_name}
{isSelected && <span className="ml-2 text-xs text-oliver-black font-normal">(selected)</span>}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-oliver-black">{agency.total_reviews}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-semibold text-oliver-black">
<div className="flex items-center">
<span className={`h-2.5 w-2.5 rounded-full mr-3 ${rate >= 80 ? 'bg-success' : rate < 70 ? 'bg-error' : 'bg-warning'}`}></span>
{rate}%
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-oliver-black">{agency.failed}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-oliver-black">{agency.errors}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-oliver-black">{agency.legal_review}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</section>
)}
{/* AI Performance Summary */}
<section className="mt-10">
<h2 className="text-2xl font-semibold text-oliver-black mb-4">AI performance summary</h2>
<div className="bg-white border border-oliver-sky border-l-8 text-oliver-black p-6 rounded-[10px] shadow-md flex items-start gap-4">
<div className="flex-shrink-0">
<LightbulbIcon className="h-9 w-9 text-oliver-orange" />
</div>
<div>
<p className="font-semibold text-oliver-black">Key Insight (Last 7 Days):</p>
<p className="mt-1 text-oliver-black">
A sharp decline in Best Practice adherence has been noted, primarily driven by proofs from the <strong>Barclays Q4 Social</strong> campaign. The Brand Guardian agent also shows a declining performance trend, suggesting a potential need for updated brand guideline training or proof review.
</p>
</div>
</div>
</section>
{/* Agent Performance Table */}
<section className="mt-10">
<h2 className="text-2xl font-semibold text-oliver-black mb-4">Agent performance (last 7 days)</h2>
<div className="bg-white rounded-[10px] shadow-md overflow-hidden border border-grey-300">
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-oliver-sky">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-oliver-black uppercase tracking-wider">Agent Name</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-oliver-black uppercase tracking-wider">Pass Rate</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-oliver-black uppercase tracking-wider">Avg. Issues per Proof</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-oliver-black uppercase tracking-wider">Performance Trend</th>
</tr>
</thead>
<tbody className="divide-y divide-grey-300">
{agentPerformance.map((agent, index) => (
<tr key={agent.name} className={index % 2 === 0 ? 'bg-white' : 'bg-oliver-grey'}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-oliver-black">{agent.name}</td>
<td className={`px-6 py-4 whitespace-nowrap text-sm font-semibold text-oliver-black`}>
<div className="flex items-center">
<span className={`h-2.5 w-2.5 rounded-full mr-3 ${agent.passRate >= 80 ? 'bg-oliver-green' : agent.passRate < 70 ? 'bg-oliver-gold' : 'bg-oliver-orange'}`}></span>
{agent.passRate}%
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-oliver-black">{agent.avgIssues}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<TrendIndicator trend={agent.trend as 'up' | 'down' | 'stable'} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</section>
</div>
);
};