|
| 1 | +'use client' |
| 2 | + |
| 3 | +import { useState, useEffect } from 'react' |
| 4 | +import { motion, AnimatePresence } from 'motion/react' |
| 5 | +import { |
| 6 | + TrendingUp, |
| 7 | + ListChecks, |
| 8 | + Coins, |
| 9 | + Users, |
| 10 | + Activity, |
| 11 | + Lightbulb, |
| 12 | + Check, |
| 13 | +} from 'lucide-react' |
| 14 | +import { cn } from '@/lib/utils' |
| 15 | +import { CountUp } from './shared' |
| 16 | + |
| 17 | +// ─── Timeline stages ──────────────────────────────────────────────────────── |
| 18 | + |
| 19 | +type Stage = 'init' | 'metrics' | 'chart' | 'activity' | 'insight' | 'settled' |
| 20 | + |
| 21 | +const TIMELINE: Record<Stage, number> = { |
| 22 | + 'init': 0, |
| 23 | + 'metrics': 500, |
| 24 | + 'chart': 3000, |
| 25 | + 'activity': 5000, |
| 26 | + 'insight': 7500, |
| 27 | + 'settled': 9500, |
| 28 | +} |
| 29 | + |
| 30 | +const STAGES: Stage[] = Object.keys(TIMELINE) as Stage[] |
| 31 | + |
| 32 | +// ─── Metric definitions ───────────────────────────────────────────────────── |
| 33 | + |
| 34 | +const METRICS = [ |
| 35 | + { label: 'Success Rate', value: 97.2, suffix: '%', decimals: 1, icon: TrendingUp, color: 'text-emerald-400' }, |
| 36 | + { label: 'Total Tasks', value: 1247, suffix: '', decimals: 0, icon: ListChecks, color: 'text-cyan-400' }, |
| 37 | + { label: 'Token Usage', value: 842, suffix: 'K', decimals: 0, icon: Coins, color: 'text-amber-400' }, |
| 38 | + { label: 'Active Sessions', value: 23, suffix: '', decimals: 0, icon: Users, color: 'text-violet-400' }, |
| 39 | +] |
| 40 | + |
| 41 | +// ─── Chart data ───────────────────────────────────────────────────────────── |
| 42 | + |
| 43 | +const CHART_DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] |
| 44 | +const CHART_VALUES = [65, 82, 55, 90, 78, 45, 88] // percentages of max height |
| 45 | + |
| 46 | +// ─── Activity feed ────────────────────────────────────────────────────────── |
| 47 | + |
| 48 | +const ACTIVITY_ITEMS = [ |
| 49 | + { text: 'Session #1247 completed', time: '2m ago', color: 'bg-emerald-500' }, |
| 50 | + { text: 'Skill "api-deploy" executed', time: '5m ago', color: 'bg-violet-500' }, |
| 51 | + { text: '3 tasks extracted from #1245', time: '8m ago', color: 'bg-cyan-500' }, |
| 52 | + { text: 'New session by alice@acme.com', time: '12m ago', color: 'bg-blue-500' }, |
| 53 | +] |
| 54 | + |
| 55 | +// ─── Main Dashboard Demo ──────────────────────────────────────────────────── |
| 56 | + |
| 57 | +export function DashboardDemo() { |
| 58 | + const [stage, setStage] = useState<Stage>('init') |
| 59 | + |
| 60 | + useEffect(() => { |
| 61 | + setStage('init') |
| 62 | + |
| 63 | + const timers: ReturnType<typeof setTimeout>[] = [] |
| 64 | + for (const [s, delay] of Object.entries(TIMELINE)) { |
| 65 | + if (delay > 0) { |
| 66 | + timers.push(setTimeout(() => setStage(s as Stage), delay)) |
| 67 | + } |
| 68 | + } |
| 69 | + return () => timers.forEach(clearTimeout) |
| 70 | + }, []) |
| 71 | + |
| 72 | + const si = STAGES.indexOf(stage) |
| 73 | + const showMetrics = si >= STAGES.indexOf('metrics') |
| 74 | + const showChart = si >= STAGES.indexOf('chart') |
| 75 | + const showActivity = si >= STAGES.indexOf('chart') |
| 76 | + const showInsight = si >= STAGES.indexOf('insight') |
| 77 | + |
| 78 | + return ( |
| 79 | + <div className="h-full flex items-center justify-center p-3 sm:p-4 lg:p-6"> |
| 80 | + <div className="w-full max-w-4xl space-y-3 sm:space-y-4"> |
| 81 | + {/* Metrics row */} |
| 82 | + <div className="grid grid-cols-2 lg:grid-cols-4 gap-2 sm:gap-3"> |
| 83 | + {METRICS.map((metric, i) => { |
| 84 | + const Icon = metric.icon |
| 85 | + return ( |
| 86 | + <motion.div |
| 87 | + key={metric.label} |
| 88 | + initial={{ opacity: 0, y: 16 }} |
| 89 | + animate={showMetrics ? { opacity: 1, y: 0 } : {}} |
| 90 | + transition={{ delay: i * 0.1, type: 'spring', stiffness: 300, damping: 25 }} |
| 91 | + className="border border-zinc-200 dark:border-zinc-700 bg-zinc-50/50 dark:bg-zinc-900/50 p-2.5 sm:p-4 rounded-lg" |
| 92 | + > |
| 93 | + <div className="flex items-center gap-1.5 mb-1 sm:mb-2"> |
| 94 | + <Icon className={cn('w-3.5 h-3.5 sm:w-4 sm:h-4', metric.color)} /> |
| 95 | + <span className="text-[10px] sm:text-xs text-zinc-400 dark:text-zinc-500">{metric.label}</span> |
| 96 | + </div> |
| 97 | + <div className="text-lg sm:text-2xl font-bold text-zinc-800 dark:text-zinc-200 font-mono"> |
| 98 | + {showMetrics ? ( |
| 99 | + <CountUp |
| 100 | + end={metric.value} |
| 101 | + suffix={metric.suffix} |
| 102 | + decimals={metric.decimals} |
| 103 | + duration={2000} |
| 104 | + /> |
| 105 | + ) : ( |
| 106 | + <span className="text-zinc-300 dark:text-zinc-700">--</span> |
| 107 | + )} |
| 108 | + </div> |
| 109 | + </motion.div> |
| 110 | + ) |
| 111 | + })} |
| 112 | + </div> |
| 113 | + |
| 114 | + {/* Chart + Activity row */} |
| 115 | + <div className="grid grid-cols-1 lg:grid-cols-5 gap-2 sm:gap-3"> |
| 116 | + {/* Bar chart */} |
| 117 | + <motion.div |
| 118 | + initial={{ opacity: 0, y: 16 }} |
| 119 | + animate={showChart ? { opacity: 1, y: 0 } : {}} |
| 120 | + transition={{ type: 'spring', stiffness: 300, damping: 25 }} |
| 121 | + className="lg:col-span-3 border border-zinc-200 dark:border-zinc-700 bg-zinc-50/50 dark:bg-zinc-900/50 p-3 sm:p-4 rounded-lg flex flex-col" |
| 122 | + > |
| 123 | + <div className="flex items-center justify-between mb-3"> |
| 124 | + <span className="text-xs sm:text-sm text-zinc-600 dark:text-zinc-400 font-medium">Weekly Activity</span> |
| 125 | + <Activity className="w-3.5 h-3.5 text-zinc-400 dark:text-zinc-500" /> |
| 126 | + </div> |
| 127 | + <div className="flex items-stretch gap-1.5 sm:gap-2 flex-1 min-h-[80px] sm:min-h-[120px]"> |
| 128 | + {CHART_DAYS.map((day, i) => ( |
| 129 | + <div key={day} className="flex-1 flex flex-col items-center gap-1"> |
| 130 | + <div className="w-full flex-1 bg-zinc-200 dark:bg-zinc-800 rounded-sm overflow-hidden relative"> |
| 131 | + <motion.div |
| 132 | + className="absolute bottom-0 left-0 right-0 bg-emerald-500/80 dark:bg-emerald-500/60 rounded-sm" |
| 133 | + initial={{ height: 0 }} |
| 134 | + animate={showChart ? { height: `${CHART_VALUES[i]}%` } : { height: 0 }} |
| 135 | + transition={{ duration: 0.8, delay: i * 0.1, ease: 'easeOut' }} |
| 136 | + /> |
| 137 | + </div> |
| 138 | + <span className="text-[9px] sm:text-[10px] text-zinc-400 dark:text-zinc-600">{day}</span> |
| 139 | + </div> |
| 140 | + ))} |
| 141 | + </div> |
| 142 | + </motion.div> |
| 143 | + |
| 144 | + {/* Activity feed */} |
| 145 | + <motion.div |
| 146 | + initial={{ opacity: 0, y: 16 }} |
| 147 | + animate={showActivity ? { opacity: 1, y: 0 } : {}} |
| 148 | + transition={{ type: 'spring', stiffness: 300, damping: 25 }} |
| 149 | + className="lg:col-span-2 border border-zinc-200 dark:border-zinc-700 bg-zinc-50/50 dark:bg-zinc-900/50 p-3 sm:p-4 rounded-lg" |
| 150 | + > |
| 151 | + <div className="flex items-center justify-between mb-3"> |
| 152 | + <span className="text-xs sm:text-sm text-zinc-600 dark:text-zinc-400 font-medium">Recent</span> |
| 153 | + </div> |
| 154 | + <div className="space-y-2"> |
| 155 | + <AnimatePresence> |
| 156 | + {showActivity && |
| 157 | + ACTIVITY_ITEMS.map((item, i) => ( |
| 158 | + <motion.div |
| 159 | + key={i} |
| 160 | + initial={{ opacity: 0, x: 12 }} |
| 161 | + animate={{ opacity: 1, x: 0 }} |
| 162 | + transition={{ delay: i * 0.2, type: 'spring', stiffness: 300, damping: 25 }} |
| 163 | + className="flex items-start gap-2" |
| 164 | + > |
| 165 | + <div className={cn('w-1.5 h-1.5 rounded-full mt-1.5 shrink-0', item.color)} /> |
| 166 | + <div className="min-w-0"> |
| 167 | + <p className="text-[10px] sm:text-xs text-zinc-700 dark:text-zinc-300 truncate">{item.text}</p> |
| 168 | + <p className="text-[9px] sm:text-[10px] text-zinc-400 dark:text-zinc-600">{item.time}</p> |
| 169 | + </div> |
| 170 | + </motion.div> |
| 171 | + ))} |
| 172 | + </AnimatePresence> |
| 173 | + </div> |
| 174 | + </motion.div> |
| 175 | + </div> |
| 176 | + |
| 177 | + {/* Insight notification */} |
| 178 | + <AnimatePresence> |
| 179 | + {showInsight && ( |
| 180 | + <motion.div |
| 181 | + initial={{ opacity: 0, y: 12 }} |
| 182 | + animate={{ opacity: 1, y: 0 }} |
| 183 | + exit={{ opacity: 0, y: -8 }} |
| 184 | + transition={{ type: 'spring', stiffness: 300, damping: 25 }} |
| 185 | + className="border border-amber-300/50 dark:border-amber-700/50 bg-amber-100/30 dark:bg-amber-950/20 rounded-lg p-2.5 sm:p-3 flex items-center gap-2 sm:gap-3" |
| 186 | + > |
| 187 | + <Lightbulb className="w-4 h-4 sm:w-5 sm:h-5 text-amber-500 dark:text-amber-400 shrink-0" /> |
| 188 | + <div className="flex-1 min-w-0"> |
| 189 | + <p className="text-xs sm:text-sm text-amber-700 dark:text-amber-300 font-medium"> |
| 190 | + Task completion rate up 12% this week |
| 191 | + </p> |
| 192 | + <p className="text-[10px] sm:text-xs text-amber-600/70 dark:text-amber-500/70"> |
| 193 | + Driven by improved skill reuse across 8 agents |
| 194 | + </p> |
| 195 | + </div> |
| 196 | + <Check className="w-4 h-4 text-amber-500/50 dark:text-amber-500/50 shrink-0" /> |
| 197 | + </motion.div> |
| 198 | + )} |
| 199 | + </AnimatePresence> |
| 200 | + </div> |
| 201 | + </div> |
| 202 | + ) |
| 203 | +} |
0 commit comments