Files
LexiChain/app/(dashboard)/dashboard/page.tsx
2026-05-03 13:26:31 +01:00

1014 lines
40 KiB
TypeScript

"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { motion, AnimatePresence } from "motion/react";
import {
Activity,
AlertTriangle,
ArrowRight,
BarChart3,
Brain,
CheckCircle2,
Clock3,
Database,
FileText,
RefreshCw,
Sparkles,
TrendingUp,
Zap,
Shield,
Fingerprint,
ChevronRight,
Tag,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { getStatsAction } from "@/features/analytics/api/stats.action";
import { checkDeadlineNotifications } from "@/features/notifications/api/notification.action";
import dynamic from "next/dynamic";
// Dynamically import heavy charting libraries to dramatically improve initial load and rendering time
const ContractStatusChart = dynamic(
() =>
import("@/features/analytics/components/charts").then(
(mod) => mod.ContractStatusChart,
),
{
ssr: false,
loading: () => (
<div className="h-full w-full animate-pulse bg-muted/30 rounded-lg"></div>
),
},
);
const ContractTypeChart = dynamic(
() =>
import("@/features/analytics/components/charts").then(
(mod) => mod.ContractTypeChart,
),
{
ssr: false,
loading: () => (
<div className="h-full w-full animate-pulse bg-muted/30 rounded-lg"></div>
),
},
);
const TrendChart = dynamic(
() =>
import("@/features/analytics/components/charts").then(
(mod) => mod.TrendChart,
),
{
ssr: false,
loading: () => (
<div className="h-full w-full animate-pulse bg-muted/30 rounded-lg"></div>
),
},
);
interface DashboardStats {
totalContracts: number;
analyzedContracts: number;
processingContracts: number;
uploadedContracts: number;
failedContracts: number;
analysisRate: number;
}
interface ChartData {
byType: Array<{ type: string; count: number }>;
byStatus: Array<{ status: string; count: number }>;
trends: Array<{ date: string; count: number }>;
}
interface PremiumInfo {
averagePremium: number;
totalPremium: number;
count: number;
}
interface RecentContract {
id: string;
title: string | null;
type: string | null;
createdAt: string;
premium: number | null;
}
interface AILearningTelemetry {
completedSamples: number;
completedLast7Days: number;
avgSummaryLength: number;
avgExtractedTextLength: number;
avgKeyPointsPerContract: number;
learningScore: number;
improvementHint: string;
}
interface StatsActionResult {
success: boolean;
stats?: DashboardStats;
chartData?: ChartData;
premiumInfo?: PremiumInfo;
aiLearningTelemetry?: AILearningTelemetry;
recentContracts?: RecentContract[];
error?: string;
}
const numberFormatter = new Intl.NumberFormat("en-US");
const currencyFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 2,
});
const defaultStats: DashboardStats = {
totalContracts: 0,
analyzedContracts: 0,
processingContracts: 0,
uploadedContracts: 0,
failedContracts: 0,
analysisRate: 0,
};
const formatLastUpdated = (date: Date | null): string => {
if (!date) {
return "Just now";
}
const seconds = Math.max(1, Math.floor((Date.now() - date.getTime()) / 1000));
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
return `${hours}h ago`;
};
const clampPercent = (value: number): number =>
Math.max(0, Math.min(100, value));
export default function DashboardPage() {
const [stats, setStats] = useState<DashboardStats>(defaultStats);
const [chartData, setChartData] = useState<ChartData | null>(null);
const [premiumInfo, setPremiumInfo] = useState<PremiumInfo>({
averagePremium: 0,
totalPremium: 0,
count: 0,
});
const [recentContracts, setRecentContracts] = useState<RecentContract[]>([]);
const [aiLearningTelemetry, setAiLearningTelemetry] =
useState<AILearningTelemetry>({
completedSamples: 0,
completedLast7Days: 0,
avgSummaryLength: 0,
avgExtractedTextLength: 0,
avgKeyPointsPerContract: 0,
learningScore: 0,
improvementHint: "Analyze contracts to build your AI quality profile.",
});
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const loadStats = useCallback(async (options?: { silent?: boolean }) => {
const isSilentRefresh = options?.silent ?? false;
if (isSilentRefresh) {
setIsRefreshing(true);
} else {
setIsLoading(true);
}
try {
const result = (await getStatsAction()) as StatsActionResult;
if (!result.success) {
return;
}
if (result.stats) setStats(result.stats);
if (result.chartData) setChartData(result.chartData);
if (result.premiumInfo) setPremiumInfo(result.premiumInfo);
if (result.aiLearningTelemetry) {
setAiLearningTelemetry(result.aiLearningTelemetry);
}
if (result.recentContracts) setRecentContracts(result.recentContracts);
setLastUpdated(new Date());
} finally {
if (isSilentRefresh) {
setIsRefreshing(false);
} else {
setIsLoading(false);
}
}
}, []);
useEffect(() => {
void loadStats();
// Check for upcoming contract deadlines and create notifications
void checkDeadlineNotifications();
}, [loadStats]);
useEffect(() => {
if (stats.processingContracts === 0 && stats.uploadedContracts === 0) {
return;
}
const intervalId = window.setInterval(() => {
void loadStats({ silent: true });
}, 10000);
return () => window.clearInterval(intervalId);
}, [loadStats, stats.processingContracts, stats.uploadedContracts]);
const hasChartData = useMemo(() => {
if (!chartData) return false;
const trendsCount = chartData.trends.reduce(
(total, entry) => total + entry.count,
0,
);
const byStatusCount = chartData.byStatus.reduce(
(total, entry) => total + entry.count,
0,
);
const byTypeCount = chartData.byType.reduce(
(total, entry) => total + entry.count,
0,
);
return trendsCount + byStatusCount + byTypeCount > 0;
}, [chartData]);
const pendingContracts = stats.processingContracts + stats.uploadedContracts;
const analyzedPercent =
stats.totalContracts > 0
? clampPercent((stats.analyzedContracts / stats.totalContracts) * 100)
: 0;
const pendingPercent =
stats.totalContracts > 0
? clampPercent((pendingContracts / stats.totalContracts) * 100)
: 0;
const failedPercent =
stats.totalContracts > 0
? clampPercent((stats.failedContracts / stats.totalContracts) * 100)
: 0;
const statusRows = [
{
label: "Uploaded",
value: stats.uploadedContracts,
colorClass: "bg-amber-500",
icon: <Database className="w-3.5 h-3.5" />,
},
{
label: "Processing",
value: stats.processingContracts,
colorClass: "bg-blue-500",
icon: <Zap className="w-3.5 h-3.5" />,
},
{
label: "Analyzed",
value: stats.analyzedContracts,
colorClass: "bg-emerald-500",
icon: <CheckCircle2 className="w-3.5 h-3.5" />,
},
{
label: "Failed",
value: stats.failedContracts,
colorClass: "bg-red-500",
icon: <AlertTriangle className="w-3.5 h-3.5" />,
},
];
if (isLoading) {
return (
<div className="min-h-screen bg-background relative overflow-hidden">
<div className="fixed inset-0 pointer-events-none">
<div className="absolute top-1/4 left-1/3 w-[500px] h-[500px] bg-primary/10 rounded-full blur-[120px] animate-pulse" />
<div className="absolute bottom-1/4 right-1/3 w-[400px] h-[400px] bg-violet-500/10 rounded-full blur-[100px] animate-pulse delay-700" />
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.03)_1px,transparent_1px)] bg-[size:64px_64px] [mask-image:radial-gradient(ellipse_60%_60%_at_50%_50%,#000_30%,transparent_100%)]" />
</div>
<div className="relative z-10 mx-auto max-w-7xl px-6 py-24">
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div
key={i}
className="rounded-2xl border border-border/40 bg-background/40 backdrop-blur-xl p-6 animate-pulse"
>
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-muted" />
<div className="flex-1 space-y-2">
<div className="h-4 bg-muted rounded-lg w-1/4" />
<div className="h-3 bg-muted rounded-lg w-1/3" />
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-background relative overflow-hidden">
{/* Ambient Background */}
<div className="fixed inset-0 pointer-events-none">
<div className="absolute top-[-10%] left-[-5%] w-[600px] h-[600px] bg-primary/5 rounded-full blur-[120px]" />
<div className="absolute top-[20%] right-[-10%] w-[500px] h-[500px] bg-violet-500/5 rounded-full blur-[100px]" />
<div className="absolute bottom-[-10%] left-[20%] w-[400px] h-[400px] bg-emerald-500/5 rounded-full blur-[100px]" />
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.02)_1px,transparent_1px)] bg-[size:64px_64px] [mask-image:radial-gradient(ellipse_80%_80%_at_50%_50%,#000_20%,transparent_100%)]" />
</div>
{/* Hero Section */}
<div className="relative border-b border-border/30">
<div className="absolute inset-0 bg-gradient-to-b from-primary/5 via-transparent to-transparent" />
<div className="relative mx-auto max-w-7xl px-6 lg:px-8 py-12 lg:py-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="grid gap-8 lg:grid-cols-[1.45fr,0.95fr] lg:items-end"
>
<div className="space-y-5">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.1 }}
className="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-primary/10 px-4 py-1.5 text-xs font-bold uppercase tracking-widest text-primary"
>
<Sparkles className="h-3.5 w-3.5" />
Performance Overview
</motion.div>
<h1 className="text-4xl lg:text-5xl font-bold tracking-tight bg-gradient-to-r from-foreground via-foreground to-muted-foreground bg-clip-text text-transparent">
Financial Contracts Analytics
</h1>
<p className="max-w-2xl text-muted-foreground leading-relaxed">
A reliable command center for uploaded documents, AI analysis
throughput, and portfolio quality across your BFSI workflow.
</p>
<div className="flex flex-wrap items-center gap-2 pt-2">
<span className="inline-flex items-center gap-1.5 rounded-full border border-emerald-500/20 bg-emerald-500/10 px-3 py-1.5 text-[11px] font-bold uppercase tracking-wider text-emerald-600 dark:text-emerald-400">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
</span>
Live metrics
</span>
<span className="inline-flex items-center gap-1.5 rounded-full border border-border/60 bg-background/60 backdrop-blur-xl px-3 py-1.5 text-[11px] font-medium text-muted-foreground">
<Clock3 className="h-3.5 w-3.5" />
{isRefreshing
? "Syncing..."
: `Updated ${formatLastUpdated(lastUpdated)}`}
</span>
</div>
</div>
{/* Pipeline Snapshot Card */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="relative group"
>
<div className="absolute -inset-0.5 bg-gradient-to-r from-primary/20 via-violet-500/20 to-primary/20 rounded-2xl blur opacity-40 group-hover:opacity-60 transition duration-500" />
<Card className="relative rounded-2xl border-border/40 bg-background/60 backdrop-blur-2xl p-6 shadow-2xl shadow-black/5">
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-primary/30 to-transparent" />
<div className="flex items-center justify-between">
<div>
<p className="text-[10px] font-bold uppercase tracking-[0.2em] text-muted-foreground">
Pipeline Snapshot
</p>
<p className="mt-1 text-3xl font-bold tracking-tight">
{numberFormatter.format(stats.totalContracts)}{" "}
<span className="text-lg font-medium text-muted-foreground">
files
</span>
</p>
</div>
<div className="rounded-xl border border-primary/20 bg-primary/10 p-3">
<Database className="h-6 w-6 text-primary" />
</div>
</div>
<div className="mt-5 rounded-xl border border-border/40 bg-muted/20 p-4 backdrop-blur-md">
<svg
viewBox="0 0 260 90"
fill="none"
aria-hidden="true"
className="h-[92px] w-full"
>
<path
d="M4 72H256"
stroke="hsl(var(--border))"
strokeWidth="1"
/>
<rect
x="24"
y="32"
width="28"
height="40"
rx="8"
fill="hsl(var(--primary) / 0.85)"
/>
<rect
x="76"
y="22"
width="28"
height="50"
rx="8"
fill="hsl(var(--secondary) / 0.8)"
/>
<rect
x="128"
y="14"
width="28"
height="58"
rx="8"
fill="hsl(var(--accent) / 0.78)"
/>
<rect
x="180"
y="44"
width="28"
height="28"
rx="8"
fill="hsl(var(--destructive) / 0.8)"
/>
<path
d="M24 30C64 8 98 10 142 18C176 24 214 34 240 22"
stroke="hsl(var(--foreground) / 0.35)"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</div>
<div className="mt-4 grid grid-cols-2 gap-3">
<div className="rounded-xl border border-border/40 bg-background/40 px-4 py-3 backdrop-blur-md">
<p className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
Analyzed
</p>
<p className="mt-1 text-lg font-bold text-foreground">
{numberFormatter.format(stats.analyzedContracts)}
</p>
</div>
<div className="rounded-xl border border-border/40 bg-background/40 px-4 py-3 backdrop-blur-md">
<p className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
Pending
</p>
<p className="mt-1 text-lg font-bold text-foreground">
{numberFormatter.format(pendingContracts)}
</p>
</div>
</div>
<div className="mt-4 flex gap-2">
<Button
variant="outline"
onClick={() => void loadStats()}
className="w-full rounded-xl border-border/60 bg-background/40 backdrop-blur-xl hover:bg-background/60"
>
<RefreshCw
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
/>
Refresh
</Button>
<Button
asChild
className="w-full rounded-xl shadow-lg shadow-primary/20"
>
<Link href="/contacts">
Manage
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</Card>
</motion.div>
</motion.div>
</div>
</div>
<div className="relative z-10 mx-auto max-w-7xl px-6 lg:px-8 py-10 space-y-8">
{/* Bento Stats Grid */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"
>
<BentoStat
icon={<FileText className="w-5 h-5" />}
label="Total Files"
value={numberFormatter.format(stats.totalContracts)}
subtitle="Uploaded into your workspace"
gradient="from-primary/20 to-primary/5"
border="border-primary/20"
iconColor="text-primary"
delay={0}
/>
<BentoStat
icon={<CheckCircle2 className="w-5 h-5" />}
label="Analyzed"
value={numberFormatter.format(stats.analyzedContracts)}
subtitle="Completed by AI pipeline"
gradient="from-emerald-500/20 to-emerald-500/5"
border="border-emerald-500/20"
iconColor="text-emerald-500"
progress={analyzedPercent}
progressColor="bg-emerald-500"
delay={0.05}
/>
<BentoStat
icon={<Clock3 className="w-5 h-5" />}
label="Pending Queue"
value={numberFormatter.format(pendingContracts)}
subtitle="Uploaded and processing files"
gradient="from-amber-500/20 to-amber-500/5"
border="border-amber-500/20"
iconColor="text-amber-500"
progress={pendingPercent}
progressColor="bg-amber-500"
delay={0.1}
/>
<BentoStat
icon={<AlertTriangle className="w-5 h-5" />}
label="Failed"
value={numberFormatter.format(stats.failedContracts)}
subtitle="Items needing re-analysis"
gradient="from-red-500/20 to-red-500/5"
border="border-red-500/20"
iconColor="text-red-500"
progress={failedPercent}
progressColor="bg-red-500"
delay={0.15}
/>
</motion.div>
{/* Pipeline Pulse + Premium Info */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="relative group"
>
<div className="absolute -inset-0.5 bg-gradient-to-r from-primary/10 via-transparent to-violet-500/10 rounded-2xl blur opacity-30" />
<Card className="relative rounded-2xl border-border/40 bg-background/40 backdrop-blur-2xl p-6 shadow-xl shadow-black/5">
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-primary/20 to-transparent" />
<div className="grid gap-6 lg:grid-cols-[1.2fr,0.8fr]">
<div>
<div className="mb-5 flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-primary/10 border border-primary/20">
<BarChart3 className="h-4 w-4 text-primary" />
</div>
<h2 className="text-sm font-bold uppercase tracking-wider">
Pipeline Pulse
</h2>
</div>
<div className="space-y-3">
{statusRows.map((row) => {
const rowPercent =
stats.totalContracts > 0
? clampPercent((row.value / stats.totalContracts) * 100)
: 0;
return (
<div
key={row.label}
className="group/row rounded-xl border border-border/40 bg-background/40 px-4 py-3 backdrop-blur-md hover:bg-background/60 transition-all"
>
<div className="mb-2 flex items-center justify-between text-xs">
<div className="flex items-center gap-2 text-muted-foreground">
<span
className={`${row.colorClass.replace("bg-", "text-")}`}
>
{row.icon}
</span>
{row.label}
</div>
<p className="font-bold text-foreground">
{numberFormatter.format(row.value)}
</p>
</div>
<div className="h-2 w-full rounded-full bg-muted/50 overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${rowPercent}%` }}
transition={{ duration: 0.8, delay: 0.3 }}
className={`h-full rounded-full ${row.colorClass}`}
/>
</div>
</div>
);
})}
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-1">
<div className="rounded-xl border border-border/40 bg-gradient-to-br from-emerald-500/10 to-transparent px-5 py-4 backdrop-blur-md hover:border-emerald-500/30 transition-all">
<p className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
Success Rate
</p>
<p className="mt-1 text-3xl font-bold text-foreground">
{stats.analysisRate}%
</p>
<p className="mt-1 text-xs text-muted-foreground">
Completed vs total files
</p>
</div>
<div className="rounded-xl border border-border/40 bg-gradient-to-br from-primary/10 to-transparent px-5 py-4 backdrop-blur-md hover:border-primary/30 transition-all">
<p className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
Avg Premium
</p>
<p className="mt-1 text-3xl font-bold text-foreground">
{currencyFormatter.format(premiumInfo.averagePremium)}
</p>
<p className="mt-1 text-xs text-muted-foreground">
Across {numberFormatter.format(premiumInfo.count)} analyzed
files
</p>
</div>
<div className="rounded-xl border border-border/40 bg-gradient-to-br from-violet-500/10 to-transparent px-5 py-4 backdrop-blur-md hover:border-violet-500/30 transition-all">
<p className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
Total Premium
</p>
<p className="mt-1 text-3xl font-bold text-foreground">
{currencyFormatter.format(premiumInfo.totalPremium)}
</p>
<p className="mt-1 text-xs text-muted-foreground">
Portfolio value captured by AI
</p>
</div>
</div>
</div>
</Card>
</motion.div>
{/* AI Learning Telemetry */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="relative group"
>
<div className="absolute -inset-0.5 bg-gradient-to-r from-violet-500/10 via-transparent to-primary/10 rounded-2xl blur opacity-30" />
<Card className="relative rounded-2xl border-border/40 bg-background/40 backdrop-blur-2xl p-6 shadow-xl shadow-black/5">
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-violet-500/30 to-transparent" />
<div className="mb-5 flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-violet-500/10 border border-violet-500/20">
<Brain className="h-4 w-4 text-violet-500" />
</div>
<h2 className="text-sm font-bold uppercase tracking-wider">
AI Learning Telemetry
</h2>
</div>
<span className="inline-flex items-center gap-1.5 rounded-full border border-violet-500/20 bg-violet-500/10 px-3 py-1.5 text-xs font-bold text-violet-600 dark:text-violet-400">
<TrendingUp className="h-3.5 w-3.5" />
Score {aiLearningTelemetry.learningScore}/100
</span>
</div>
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-4">
{[
{
label: "Completed Samples",
value: numberFormatter.format(
aiLearningTelemetry.completedSamples,
),
sub: `${numberFormatter.format(aiLearningTelemetry.completedLast7Days)} in last 7 days`,
icon: <CheckCircle2 className="w-4 h-4" />,
color: "emerald",
},
{
label: "Avg Summary Length",
value: numberFormatter.format(
aiLearningTelemetry.avgSummaryLength,
),
sub: "characters",
icon: <FileText className="w-4 h-4" />,
color: "primary",
},
{
label: "Avg Extracted Text",
value: numberFormatter.format(
aiLearningTelemetry.avgExtractedTextLength,
),
sub: "characters",
icon: <Fingerprint className="w-4 h-4" />,
color: "blue",
},
{
label: "Avg Key Points",
value: aiLearningTelemetry.avgKeyPointsPerContract.toFixed(1),
sub: "items per analysis",
icon: <Sparkles className="w-4 h-4" />,
color: "violet",
},
].map((item) => (
<div
key={item.label}
className="rounded-xl border border-border/40 bg-background/40 px-4 py-4 backdrop-blur-md hover:bg-background/60 transition-all group/card"
>
<div className="flex items-center gap-2 mb-3">
<span className={`text-${item.color}-500`}>
{item.icon}
</span>
<p className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
{item.label}
</p>
</div>
<p className="text-2xl font-bold text-foreground">
{item.value}
</p>
<p className="mt-1 text-xs text-muted-foreground">
{item.sub}
</p>
</div>
))}
</div>
<div className="mt-5 rounded-xl border border-border/40 bg-background/40 p-4 backdrop-blur-md">
<div className="mb-2 flex items-center justify-between text-xs">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
Learning quality index
</span>
<span className="font-bold text-foreground">
{aiLearningTelemetry.learningScore}%
</span>
</div>
<div className="h-2 w-full rounded-full bg-muted/50 overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{
width: `${Math.max(0, Math.min(100, aiLearningTelemetry.learningScore))}%`,
}}
transition={{ duration: 1, delay: 0.5 }}
className="h-full rounded-full bg-gradient-to-r from-primary via-violet-500 to-accent"
/>
</div>
<p className="mt-2 text-xs text-muted-foreground leading-relaxed">
{aiLearningTelemetry.improvementHint}
</p>
</div>
</Card>
</motion.div>
{hasChartData ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="grid grid-cols-1 gap-5 lg:grid-cols-12"
>
{chartData && chartData.trends.length > 0 && (
<Card className="rounded-2xl border-border/40 bg-background/40 backdrop-blur-2xl p-6 shadow-xl shadow-black/5 lg:col-span-8 overflow-hidden">
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-primary/20 to-transparent" />
<div className="mb-5 flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-primary/10 border border-primary/20">
<BarChart3 className="h-4 w-4 text-primary" />
</div>
<h2 className="text-sm font-bold uppercase tracking-wider">
Upload Trend (30 days)
</h2>
</div>
<div className="h-[320px]">
<TrendChart data={chartData.trends} />
</div>
</Card>
)}
{chartData && chartData.byStatus.length > 0 && (
<Card className="rounded-2xl border-border/40 bg-background/40 backdrop-blur-2xl p-6 shadow-xl shadow-black/5 lg:col-span-4 overflow-hidden">
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-emerald-500/20 to-transparent" />
<div className="mb-5 flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-emerald-500/10 border border-emerald-500/20">
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
</div>
<h2 className="text-sm font-bold uppercase tracking-wider">
Processing Status
</h2>
</div>
<div className="h-[320px]">
<ContractStatusChart
data={chartData.byStatus.map((s) => ({
...s,
name: s.status,
}))}
/>
</div>
</Card>
)}
{chartData && chartData.byType.length > 0 && (
<Card className="rounded-2xl border-border/40 bg-background/40 backdrop-blur-2xl p-6 shadow-xl shadow-black/5 lg:col-span-7 overflow-hidden">
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-violet-500/20 to-transparent" />
<div className="mb-5 flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-violet-500/10 border border-violet-500/20">
<FileText className="h-4 w-4 text-violet-500" />
</div>
<h2 className="text-sm font-bold uppercase tracking-wider">
Contract Type Distribution
</h2>
</div>
<div className="h-[300px]">
<ContractTypeChart data={chartData.byType} />
</div>
</Card>
)}
<Card className="rounded-2xl border-border/40 bg-background/40 backdrop-blur-2xl p-6 shadow-xl shadow-black/5 lg:col-span-5 overflow-hidden">
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-amber-500/20 to-transparent" />
<div className="mb-5 flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-amber-500/10 border border-amber-500/20">
<Sparkles className="h-4 w-4 text-amber-500" />
</div>
<h2 className="text-sm font-bold uppercase tracking-wider">
Recent Analyses
</h2>
</div>
{recentContracts.length > 0 ? (
<div className="space-y-3">
<AnimatePresence>
{recentContracts.map((contract, idx) => (
<motion.div
key={contract.id}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.05 }}
className="group rounded-xl border border-border/40 bg-background/40 px-4 py-3 backdrop-blur-md hover:bg-background/60 hover:border-primary/20 transition-all cursor-pointer"
>
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-foreground truncate flex-1">
{contract.title || "Untitled contract"}
</p>
<ChevronRight className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 group-hover:translate-x-0.5 transition-all shrink-0" />
</div>
<div className="mt-1.5 flex items-center justify-between text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Tag className="w-3 h-3" />
{contract.type || "Unknown type"}
</span>
<span className="flex items-center gap-1">
<Clock3 className="w-3 h-3" />
{new Date(contract.createdAt).toLocaleDateString(
"en-US",
{
month: "short",
day: "numeric",
},
)}
</span>
</div>
<p className="mt-2 text-xs font-bold text-foreground">
Premium:{" "}
{contract.premium !== null
? currencyFormatter.format(contract.premium)
: "Not detected"}
</p>
</motion.div>
))}
</AnimatePresence>
</div>
) : (
<div className="flex h-[300px] items-center justify-center rounded-xl border border-dashed border-border/40 bg-background/20 text-center">
<div>
<div className="relative inline-flex mb-3">
<div className="absolute inset-0 bg-primary/20 blur-xl rounded-full" />
<Sparkles className="w-8 h-8 text-muted-foreground relative z-10" />
</div>
<p className="text-sm font-semibold text-foreground">
No recent analyses yet
</p>
<p className="mt-1 text-xs text-muted-foreground">
Analyze a contract to populate this activity feed.
</p>
</div>
</div>
)}
</Card>
</motion.div>
) : (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
<Card className="rounded-2xl border-border/40 bg-background/40 backdrop-blur-2xl p-12 shadow-xl shadow-black/5">
<div className="mx-auto flex max-w-2xl flex-col items-center text-center">
<div className="relative inline-flex mb-6">
<div className="absolute inset-0 bg-primary/20 blur-2xl rounded-full" />
<BarChart3 className="w-16 h-16 text-muted-foreground relative z-10" />
</div>
<h3 className="text-2xl font-bold text-foreground">
Your analytics will appear here
</h3>
<p className="mt-2 text-sm text-muted-foreground max-w-md">
Upload and analyze contracts to unlock trend and distribution
charts.
</p>
<Button
asChild
className="mt-6 rounded-xl shadow-lg shadow-primary/20"
>
<Link href="/contacts">
Upload first contract
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</Card>
</motion.div>
)}
</div>
</div>
);
}
// ─────────────────────────────────────────────────
// Bento Stat Sub-Component
// ─────────────────────────────────────────────────
function BentoStat({
icon,
label,
value,
subtitle,
gradient,
border,
iconColor,
progress,
progressColor,
delay,
}: {
icon: React.ReactNode;
label: string;
value: string;
subtitle: string;
gradient: string;
border: string;
iconColor: string;
progress?: number;
progressColor?: string;
delay: number;
}) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 + delay }}
className={`group relative rounded-2xl border ${border} bg-gradient-to-br ${gradient} p-5 backdrop-blur-xl overflow-hidden hover:scale-[1.02] transition-transform duration-300`}
>
<div className="absolute inset-0 bg-background/40" />
<div className="absolute -right-4 -top-4 w-24 h-24 bg-gradient-to-br from-white/10 to-transparent rounded-full blur-2xl group-hover:opacity-100 opacity-0 transition-opacity" />
<div className="relative z-10">
<div className="flex items-center justify-between mb-4">
<div
className={`p-2.5 rounded-xl bg-background/60 border border-border/40 shadow-sm ${iconColor}`}
>
{icon}
</div>
</div>
<p className="text-[10px] font-bold text-muted-foreground uppercase tracking-[0.2em]">
{label}
</p>
<p className="text-3xl font-bold text-foreground mt-1 truncate tracking-tight">
{value}
</p>
<p className="text-[11px] text-muted-foreground mt-1">{subtitle}</p>
{progress !== undefined && (
<div className="mt-3 h-1.5 w-full rounded-full bg-muted/50 overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.8, delay: 0.3 + delay }}
className={`h-full rounded-full ${progressColor}`}
/>
</div>
)}
</div>
</motion.div>
);
}