diff --git a/frontend/src/components/ui/Badge.tsx b/frontend/src/components/ui/Badge.tsx new file mode 100644 index 0000000..f674e0d --- /dev/null +++ b/frontend/src/components/ui/Badge.tsx @@ -0,0 +1,53 @@ +import { FC, ReactNode } from 'react'; +import { cn } from '../../lib/utils'; + +interface BadgeProps { + children: ReactNode; + variant?: 'default' | 'success' | 'warning' | 'error' | 'info' | 'neutral'; + size?: 'sm' | 'md' | 'lg'; + className?: string; + animated?: boolean; +} + +/** + * Badge component for displaying status indicators + * + * Note: The animated prop uses the 'animate-ui-pulse-glow' CSS class + * which is defined in src/index.css + */ +export const Badge: FC = ({ + children, + variant = 'default', + size = 'md', + className, + animated = false, +}) => { + const variants = { + default: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300', + success: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300', + warning: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300', + error: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300', + info: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-300', + neutral: 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300', + }; + + const sizes = { + sm: 'px-2 py-0.5 text-xs', + md: 'px-2.5 py-1 text-sm', + lg: 'px-3 py-1.5 text-base', + }; + + return ( + + {children} + + ); +}; diff --git a/frontend/src/components/ui/Breadcrumbs.tsx b/frontend/src/components/ui/Breadcrumbs.tsx new file mode 100644 index 0000000..6cf742f --- /dev/null +++ b/frontend/src/components/ui/Breadcrumbs.tsx @@ -0,0 +1,46 @@ +import { FC } from 'react'; +import { Link } from 'react-router-dom'; +import { ChevronRight, Home } from 'lucide-react'; +import { cn } from '../../lib/utils'; + +interface BreadcrumbItem { + label: string; + href?: string; +} + +interface BreadcrumbsProps { + items: BreadcrumbItem[]; + className?: string; +} + +export const Breadcrumbs: FC = ({ items, className }) => { + return ( + + ); +}; diff --git a/frontend/src/components/ui/EmptyState.tsx b/frontend/src/components/ui/EmptyState.tsx new file mode 100644 index 0000000..90f91f2 --- /dev/null +++ b/frontend/src/components/ui/EmptyState.tsx @@ -0,0 +1,41 @@ +import { FC, ReactNode } from 'react'; +import { FileQuestion } from 'lucide-react'; +import { cn } from '../../lib/utils'; + +interface EmptyStateProps { + icon?: ReactNode; + title: string; + description?: string; + action?: ReactNode; + className?: string; +} + +export const EmptyState: FC = ({ + icon, + title, + description, + action, + className, +}) => { + return ( +
+
+ {icon || } +
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} + {action &&
{action}
} +
+ ); +}; diff --git a/frontend/src/components/ui/LoadingSkeleton.tsx b/frontend/src/components/ui/LoadingSkeleton.tsx new file mode 100644 index 0000000..4269796 --- /dev/null +++ b/frontend/src/components/ui/LoadingSkeleton.tsx @@ -0,0 +1,63 @@ +import { FC } from 'react'; +import { cn } from '../../lib/utils'; + +interface LoadingSkeletonProps { + variant?: 'text' | 'circular' | 'rectangular' | 'card'; + width?: string | number; + height?: string | number; + className?: string; + count?: number; +} + +/** + * LoadingSkeleton component for showing loading states + * + * Note: Uses the 'animate-ui-shimmer' CSS class defined in src/index.css + */ +export const LoadingSkeleton: FC = ({ + variant = 'rectangular', + width, + height, + className, + count = 1, +}) => { + const baseClasses = [ + 'animate-ui-shimmer', + 'bg-gradient-to-r', + 'from-slate-200 via-slate-300 to-slate-200', + 'dark:from-slate-700 dark:via-slate-600 dark:to-slate-700' + ].join(' '); + + const variants = { + text: 'rounded h-4', + circular: 'rounded-full', + rectangular: 'rounded-md', + card: 'rounded-xl h-32', + }; + + const style = { + width: width || (variant === 'text' ? '100%' : variant === 'circular' ? '48px' : '100%'), + height: height || (variant === 'circular' ? '48px' : variant === 'card' ? '128px' : '16px'), + }; + + if (count > 1) { + return ( +
+ {Array.from({ length: count }).map((_, index) => ( +
+ ))} +
+ ); + } + + return ( +
+ ); +}; diff --git a/frontend/src/components/ui/Tooltip.tsx b/frontend/src/components/ui/Tooltip.tsx new file mode 100644 index 0000000..0bb5f6a --- /dev/null +++ b/frontend/src/components/ui/Tooltip.tsx @@ -0,0 +1,76 @@ +import { FC, ReactNode, useState, KeyboardEvent } from 'react'; +import { cn } from '../../lib/utils'; + +interface TooltipProps { + content: string | ReactNode; + children: ReactNode; + position?: 'top' | 'bottom' | 'left' | 'right'; + className?: string; +} + +/** + * Tooltip component for displaying contextual help + * + * Keyboard navigation: + * - Press Enter or Space to toggle tooltip visibility + * - Press Escape to close the tooltip + */ +export const Tooltip: FC = ({ + content, + children, + position = 'top', + className, +}) => { + const [isVisible, setIsVisible] = useState(false); + + const positionClasses = { + top: 'bottom-full left-1/2 -translate-x-1/2 mb-2', + bottom: 'top-full left-1/2 -translate-x-1/2 mt-2', + left: 'right-full top-1/2 -translate-y-1/2 mr-2', + right: 'left-full top-1/2 -translate-y-1/2 ml-2', + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + setIsVisible(false); + } else if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setIsVisible(prev => !prev); + } + }; + + return ( +
+
setIsVisible(true)} + onMouseLeave={() => setIsVisible(false)} + onFocus={() => setIsVisible(true)} + onBlur={() => setIsVisible(false)} + onKeyDown={handleKeyDown} + > + {children} +
+ {isVisible && ( +
+ {content} +
+
+ )} +
+ ); +}; diff --git a/frontend/src/index.css b/frontend/src/index.css index 2e81f44..8435093 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -23,3 +23,33 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + +/* Custom Animations */ +@keyframes shimmer { + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; + } +} + +@keyframes pulse-glow { + 0%, 100% { + opacity: 1; + box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); + } + 50% { + opacity: 0.9; + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1); + } +} + +.animate-ui-shimmer { + animation: shimmer 2s infinite linear; + background-size: 2000px 100%; +} + +.animate-ui-pulse-glow { + animation: pulse-glow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} diff --git a/frontend/src/pages/FinalSummary.tsx b/frontend/src/pages/FinalSummary.tsx index 7f309c2..789e8b3 100644 --- a/frontend/src/pages/FinalSummary.tsx +++ b/frontend/src/pages/FinalSummary.tsx @@ -1,7 +1,7 @@ import { FC, useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { motion } from 'framer-motion'; -import { FileInput, GitMerge, Gavel, ArrowLeft } from 'lucide-react'; +import { FileInput, GitMerge, Gavel, TrendingUp, Clock } from 'lucide-react'; import { toast } from 'react-hot-toast'; import { SuccessBanner } from '../components/summary/SuccessBanner'; @@ -10,7 +10,10 @@ import { KeyFindings, Finding } from '../components/summary/KeyFindings'; import { ChartEmbed } from '../components/summary/ChartEmbed'; import { PDFGenerator } from '../components/summary/PDFGenerator'; import { ActionButtons } from '../components/summary/ActionButtons'; -import { Button } from '../components/ui/Button'; +import { Breadcrumbs } from '../components/ui/Breadcrumbs'; +import { Badge } from '../components/ui/Badge'; +import { Tooltip } from '../components/ui/Tooltip'; +import { LoadingSkeleton } from '../components/ui/LoadingSkeleton'; import { apiRequest } from '../lib/api'; @@ -106,10 +109,31 @@ export const FinalSummary: FC = () => { if (loading) { return ( -
-
-
-

Generating Case Summary...

+
+
+ {/* Header Skeleton */} +
+ +
+ + {/* Success Banner Skeleton */} + + + {/* Summary Cards Skeleton */} +
+
+
+ +
+ +
+ +
+
+
+ +
+
); @@ -121,15 +145,42 @@ export const FinalSummary: FC = () => {
+ {/* Breadcrumbs */} + + {/* Header */} -
- -

- Case Summary / {caseId} +
+
+

+ Case Summary

+ + {caseId} + + + = 90 ? 'success' : data && data.dataQuality >= 70 ? 'info' : 'warning'} + size="sm" + > + {data ? `${data.dataQuality}% Quality` : 'Loading...'} + + +
+
+ +
+ + {data ? `${data.daysToResolution} days` : 'Loading...'} +
+
+
{/* Success Banner */} @@ -214,11 +265,53 @@ export const FinalSummary: FC = () => { - {/* Additional context or quick stats could go here */} -
-

Analyst Note

-

This case exceeds the threshold for automatic reporting. Please review all findings before generating the final compliance PDF.

-
+ {/* Case Metrics Summary */} + +

+ + Case Metrics +

+
+
+ Status + + {data?.status || 'Unknown'} + +
+
+ Data Quality + {data?.dataQuality || 0}% +
+
+ Time to Resolve + {data?.daysToResolution || 0} days +
+
+
+ + {/* Analyst Note */} + +

+ + Analyst Note +

+

+ This case exceeds the threshold for automatic reporting. Please review all findings before generating the final compliance PDF. +

+

diff --git a/frontend/src/pages/Visualization.tsx b/frontend/src/pages/Visualization.tsx index 35f969a..dc33b7c 100644 --- a/frontend/src/pages/Visualization.tsx +++ b/frontend/src/pages/Visualization.tsx @@ -8,11 +8,18 @@ import { Calendar, Download, RefreshCw, - Share2 + Share2, + Info, + ArrowUpRight, + ArrowDownRight } from 'lucide-react'; import { motion } from 'framer-motion'; import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card'; import { Button } from '../components/ui/Button'; +import { Breadcrumbs } from '../components/ui/Breadcrumbs'; +import { Tooltip } from '../components/ui/Tooltip'; +import { Badge } from '../components/ui/Badge'; +import { LoadingSkeleton } from '../components/ui/LoadingSkeleton'; import { MilestoneTracker } from '../components/visualization/MilestoneTracker'; import { FraudDetectionPanel } from '../components/visualization/FraudDetectionPanel'; import { VisualizationDashboard } from '../components/visualization/VisualizationDashboard'; @@ -170,10 +177,28 @@ export function Visualization() { if (isLoading) { return ( -
-
- -

Loading visualization...

+
+
+ {/* Header Skeleton */} +
+
+ + +
+
+ + + +
+
+ + {/* KPI Cards Skeleton */} +
+ +
+ + {/* Content Skeleton */} +
); @@ -201,85 +226,151 @@ export function Visualization() { return (
+ {/* Breadcrumbs */} + + {/* Header */}
-

- Financial Visualization -

+
+

+ Financial Visualization +

+ + Live Data + +

Case {caseId} - Interactive financial analysis and fraud detection

- - - + + + + + + + + +
{/* KPI Cards */}
- + - Total Inflow - + + Total Inflow + + + + +
+ +
${(data?.total_inflow || 0).toLocaleString()}
-

All incoming transactions

+
+ +

All incoming transactions

+
- + - Total Outflow - + + Total Outflow + + + + +
+ +
${(data?.total_outflow || 0).toLocaleString()}
-

All outgoing transactions

+
+ +

All outgoing transactions

+
- + - Net Cashflow - + + Net Cashflow + + + + +
+ +
= 0 ? 'text-blue-600 dark:text-blue-400' : 'text-red-600 dark:text-red-400'}`}> ${Math.abs(data?.net_cashflow || 0).toLocaleString()}
-

- {(data?.net_cashflow || 0) >= 0 ? 'Surplus' : 'Deficit'} -

+
+ = 0 ? 'success' : 'warning'} size="sm"> + {(data?.net_cashflow || 0) >= 0 ? 'Surplus' : 'Deficit'} + +
- + - Suspect Items - + + Suspect Items + + + + +
+ +
{data?.suspect_transactions || 0}
-

Flagged transactions

+
+ {(data?.suspect_transactions || 0) > 0 ? ( + + Needs Review + + ) : ( + + All Clear + + )} +