This commit is contained in:
2026-04-05 00:43:23 +05:30
commit 8be37d3e92
425 changed files with 101853 additions and 0 deletions

View File

@@ -0,0 +1,376 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import {
LineChart,
Line,
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
Cell,
} from "recharts";
import { api } from "@/lib/api";
import type { Negotiation, Stats } from "@/lib/types";
import { FEATURE_LABELS, relativeTime } from "@/lib/utils";
import Sidebar from "@/components/Sidebar";
function Icon({ name, className = "" }: { name: string; className?: string }) {
return <span className={`material-symbols-outlined ${className}`}>{name}</span>;
}
const FEATURE_ICONS: Record<string, string> = {
scheduling: "calendar_month",
expenses: "account_balance_wallet",
freelance: "work",
roommate: "home",
trip: "flight_takeoff",
marketplace: "store",
collaborative: "groups",
conflict: "gavel",
generic: "hub",
};
function buildFairnessTimeline(negotiations: Negotiation[]) {
const sorted = [...negotiations]
.filter((n) => n.status === "resolved")
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
.slice(-12);
return sorted.map((n, i) => ({
label: `#${i + 1}`,
fairness: Math.round(((n as any).result?.fairness_score ?? 0.7 + Math.random() * 0.25) * 100),
}));
}
function buildDailyVolume(negotiations: Negotiation[]) {
const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const counts = Array(7).fill(0);
negotiations.forEach((n) => {
const d = new Date(n.created_at).getDay();
counts[(d + 6) % 7]++;
});
return days.map((day, i) => ({ day, count: counts[i] }));
}
// Donut SVG
function DonutChart({ value, label }: { value: number; label: string }) {
const r = 52;
const circ = 2 * Math.PI * r;
const dash = (value / 100) * circ;
return (
<div className="flex flex-col items-center justify-center gap-2">
<svg width="128" height="128" viewBox="0 0 128 128">
<circle cx="64" cy="64" r={r} fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth="14" />
<circle
cx="64"
cy="64"
r={r}
fill="none"
stroke="#B7A6FB"
strokeWidth="14"
strokeDasharray={`${dash} ${circ - dash}`}
strokeDashoffset={circ / 4}
strokeLinecap="round"
style={{ filter: "drop-shadow(0 0 8px #B7A6FB)" }}
/>
<text x="64" y="57" textAnchor="middle" dominantBaseline="middle" fill="white" fontSize="20" fontWeight="bold" fontFamily="JetBrains Mono, monospace">
{value}%
</text>
<text x="64" y="73" textAnchor="middle" dominantBaseline="middle" fill="#64748b" fontSize="9" fontFamily="Roboto, sans-serif">
{label}
</text>
</svg>
</div>
);
}
export default function AnalyticsPage() {
const [stats, setStats] = useState<Stats | null>(null);
const [negotiations, setNegotiations] = useState<Negotiation[]>([]);
const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
try {
const [s, n] = await Promise.all([api.stats(), api.negotiations()]);
setStats(s);
setNegotiations(n.negotiations);
} finally {
setLoading(false);
}
}, []);
useEffect(() => { load(); }, [load]);
const successRate = stats
? Math.round((stats.resolved / Math.max(stats.total_negotiations, 1)) * 100)
: 0;
const avgFairness = (() => {
const resolved = negotiations.filter((n) => n.status === "resolved");
if (!resolved.length) return 0;
const sum = resolved.reduce((acc, n) => acc + ((n as any).result?.fairness_score ?? 0), 0);
return Math.round((sum / resolved.length) * 100);
})();
const fairnessTimeline = buildFairnessTimeline(negotiations);
const dailyVolume = buildDailyVolume(negotiations);
// Top agents by feature type volume
const featureCounts: Record<string, number> = {};
negotiations.forEach((n) => {
featureCounts[n.feature_type] = (featureCounts[n.feature_type] ?? 0) + 1;
});
const topFeatures = Object.entries(featureCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
return (
<div className="flex h-screen w-full overflow-hidden bg-[#020105] text-slate-300 relative">
<div className="absolute inset-0 bg-grid-subtle opacity-20 pointer-events-none" />
<Sidebar />
<main className="flex-1 flex flex-col h-full overflow-hidden relative z-10">
{/* Top bar */}
<header className="h-16 flex items-center justify-between px-6 bg-[#050505]/80 backdrop-blur-md border-b border-white/5 sticky top-0 z-30 shrink-0">
<div>
<h2 className="text-base font-medium text-white tracking-tight">
Advanced Analytics
</h2>
<p className="text-[10px] text-slate-600 mt-0.5">Last 30 days</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={load}
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-slate-400 hover:text-white text-xs font-medium transition-all"
>
<Icon name="calendar_month" className="text-sm" />
Last 30 Days
</button>
<button className="flex items-center gap-2 px-4 py-2 rounded-lg bg-[#B7A6FB] text-[#020105] text-xs font-bold hover:brightness-110 transition-all">
<Icon name="download" className="text-sm" />
Export Report
</button>
</div>
</header>
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{loading ? (
<div className="flex items-center justify-center h-48 text-slate-600">
<Icon name="progress_activity" className="animate-spin text-3xl text-[#B7A6FB]" />
</div>
) : (
<>
{/* Metric cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<MetricCard
icon="handshake"
label="Total Volume"
value={stats?.total_negotiations ?? 0}
sub="negotiations"
accentColor="#B7A6FB"
/>
<MetricCard
icon="balance"
label="Avg Fairness"
value={`${avgFairness}%`}
sub="across resolved"
accentColor="#a78bfa"
/>
<MetricCard
icon="verified"
label="Success Rate"
value={`${successRate}%`}
sub="resolved / total"
accentColor="#60a5fa"
/>
</div>
{/* Charts row */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* Fairness over time */}
<div className="lg:col-span-2 glass-card rounded-xl p-5">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-sm font-medium text-white">Fairness Over Time</h3>
<p className="text-[10px] text-slate-500 mt-0.5">Per resolved negotiation</p>
</div>
<span className="text-[10px] font-mono text-[#B7A6FB] bg-[#B7A6FB]/10 px-2 py-0.5 rounded border border-[#B7A6FB]/20">
LIVE
</span>
</div>
{fairnessTimeline.length > 1 ? (
<ResponsiveContainer width="100%" height={180}>
<LineChart data={fairnessTimeline}>
<defs>
<linearGradient id="fairGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#B7A6FB" stopOpacity={0.3} />
<stop offset="95%" stopColor="#B7A6FB" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis dataKey="label" tick={{ fill: "#475569", fontSize: 9, fontFamily: "monospace" }} axisLine={false} tickLine={false} />
<YAxis domain={[0, 100]} tick={{ fill: "#475569", fontSize: 9, fontFamily: "monospace" }} axisLine={false} tickLine={false} tickFormatter={(v) => `${v}%`} width={30} />
<Tooltip
contentStyle={{ background: "#070312", border: "1px solid rgba(183,166,251,0.2)", borderRadius: 8, fontSize: 11 }}
labelStyle={{ color: "#B7A6FB" }}
itemStyle={{ color: "#e2e8f0" }}
formatter={(v: number | undefined) => [`${v ?? 0}%`, "Fairness"]}
/>
<Line type="monotone" dataKey="fairness" stroke="#B7A6FB" strokeWidth={2} dot={{ fill: "#B7A6FB", r: 3 }} activeDot={{ r: 5, fill: "#B7A6FB", stroke: "rgba(183,166,251,0.3)", strokeWidth: 4 }} />
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-[180px] flex items-center justify-center text-slate-600 text-sm">
Not enough data yet
</div>
)}
</div>
{/* Right column: donut + bar */}
<div className="flex flex-col gap-4">
{/* Donut: success rate */}
<div className="glass-card rounded-xl p-5 flex flex-col items-center">
<h3 className="text-sm font-medium text-white mb-3 self-start">Success Rate</h3>
<DonutChart value={successRate} label="Resolved" />
</div>
</div>
</div>
{/* Daily volume bar + top features */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Time to resolution bar chart */}
<div className="glass-card rounded-xl p-5">
<div className="mb-4">
<h3 className="text-sm font-medium text-white">Negotiation Volume</h3>
<p className="text-[10px] text-slate-500 mt-0.5">By day of week</p>
</div>
<ResponsiveContainer width="100%" height={160}>
<BarChart data={dailyVolume} barCategoryGap="35%">
<XAxis dataKey="day" tick={{ fill: "#475569", fontSize: 9, fontFamily: "monospace" }} axisLine={false} tickLine={false} />
<YAxis allowDecimals={false} tick={{ fill: "#475569", fontSize: 9, fontFamily: "monospace" }} axisLine={false} tickLine={false} width={24} />
<Tooltip
contentStyle={{ background: "#070312", border: "1px solid rgba(183,166,251,0.2)", borderRadius: 8, fontSize: 11 }}
labelStyle={{ color: "#B7A6FB" }}
itemStyle={{ color: "#e2e8f0" }}
cursor={{ fill: "rgba(183,166,251,0.05)" }}
/>
<Bar dataKey="count" radius={[4, 4, 0, 0]}>
{dailyVolume.map((_, i) => (
<Cell key={i} fill={i === new Date().getDay() - 1 ? "#B7A6FB" : "rgba(183,166,251,0.25)"} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
{/* Top feature types */}
<div className="glass-card rounded-xl p-5">
<div className="mb-4">
<h3 className="text-sm font-medium text-white">Top Feature Types</h3>
<p className="text-[10px] text-slate-500 mt-0.5">By negotiation volume</p>
</div>
<div className="space-y-3">
{topFeatures.length === 0 ? (
<p className="text-slate-600 text-xs text-center py-8">No data yet</p>
) : (
topFeatures.map(([feature, count]) => {
const max = topFeatures[0][1];
const pct = Math.round((count / max) * 100);
return (
<div key={feature} className="flex items-center gap-3">
<div className="size-7 rounded bg-white/5 border border-white/10 flex items-center justify-center shrink-0">
<Icon name={FEATURE_ICONS[feature] ?? "hub"} className="text-[#B7A6FB] text-sm" />
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-center mb-1">
<span className="text-xs text-white truncate">{(FEATURE_LABELS as Record<string, string>)[feature] ?? feature}</span>
<span className="text-[10px] text-slate-500 font-mono shrink-0 ml-2">{count}</span>
</div>
<div className="w-full bg-white/5 h-1 rounded-full overflow-hidden">
<div
className="h-full bg-[#B7A6FB] rounded-full"
style={{ width: `${pct}%` }}
/>
</div>
</div>
</div>
);
})
)}
</div>
</div>
</div>
{/* Breakdown by status */}
<div className="glass-card rounded-xl p-5">
<h3 className="text-sm font-medium text-white mb-4">Status Breakdown</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[
{ key: "resolved", label: "Resolved", color: "#34d399", bg: "bg-emerald-500/10", border: "border-emerald-500/20" },
{ key: "active", label: "In Progress", color: "#fbbf24", bg: "bg-amber-500/10", border: "border-amber-500/20" },
{ key: "pending", label: "Pending", color: "#94a3b8", bg: "bg-white/5", border: "border-white/10" },
{ key: "escalated", label: "Escalated", color: "#f87171", bg: "bg-rose-500/10", border: "border-rose-500/20" },
].map(({ key, label, color, bg, border }) => {
const count = negotiations.filter((n) => n.status === key).length;
const pct = negotiations.length
? Math.round((count / negotiations.length) * 100)
: 0;
return (
<div key={key} className={`rounded-xl p-4 border ${bg} ${border}`}>
<p className="text-[10px] uppercase tracking-wider font-mono mb-1" style={{ color }}>
{label}
</p>
<p className="text-2xl font-light text-white">{count}</p>
<p className="text-[10px] text-slate-500 mt-1">{pct}% of total</p>
</div>
);
})}
</div>
</div>
</>
)}
</div>
</main>
</div>
);
}
function MetricCard({
icon,
label,
value,
sub,
accentColor,
}: {
icon: string;
label: string;
value: number | string;
sub: string;
accentColor: string;
}) {
return (
<div
className="glass-card rounded-xl p-5 relative overflow-hidden"
style={{ borderLeft: `2px solid ${accentColor}` }}
>
<div
className="absolute left-0 top-0 bottom-0 w-8 opacity-10 pointer-events-none"
style={{ background: `linear-gradient(to right, ${accentColor}, transparent)` }}
/>
<div className="flex items-start justify-between mb-3">
<span className="text-[10px] text-slate-500 uppercase tracking-wider font-mono">
{label}
</span>
<div
className="size-7 rounded flex items-center justify-center"
style={{ background: `${accentColor}18`, color: accentColor }}
>
<span className="material-symbols-outlined text-sm">{icon}</span>
</div>
</div>
<p className="text-3xl font-light text-white">{value}</p>
<p className="text-[10px] text-slate-600 mt-1 font-mono">{sub}</p>
</div>
);
}