1014 lines
40 KiB
TypeScript
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>
|
|
);
|
|
}
|