474 lines
25 KiB
TypeScript
474 lines
25 KiB
TypeScript
|
||
import React, { useState, useMemo, useRef } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { Phone, CheckCircle, Clock, UserCheck, Activity, Filter, ChevronDown, BarChart3, HelpCircle, Mail, Sparkles, ArrowDown, Bot, Zap, Rocket, LineChart, Layers, Fingerprint, Network, MonitorPlay, Plus } from 'lucide-react';
|
||
import { Card, Button } from '../components/UI';
|
||
import { mockAssistants, getDashboardStats } from '../services/mockData';
|
||
|
||
export const DashboardPage: React.FC = () => {
|
||
const navigate = useNavigate();
|
||
const [timeRange, setTimeRange] = useState<'week' | 'month' | 'year'>('week');
|
||
const [selectedAssistantId, setSelectedAssistantId] = useState<string>('all');
|
||
|
||
const workflowRef = useRef<HTMLDivElement>(null);
|
||
const aboutRef = useRef<HTMLDivElement>(null);
|
||
|
||
const stats = useMemo(() => {
|
||
return getDashboardStats(timeRange, selectedAssistantId);
|
||
}, [timeRange, selectedAssistantId]);
|
||
|
||
const scrollToNext = (ref: React.RefObject<HTMLDivElement>) => {
|
||
ref.current?.scrollIntoView({ behavior: 'smooth' });
|
||
};
|
||
|
||
return (
|
||
<div className="h-[calc(100vh-5rem)] overflow-y-auto snap-y snap-mandatory custom-scrollbar relative scroll-smooth bg-background">
|
||
{/* SECTION 1: METRICS & CHARTS */}
|
||
<section className="min-h-full snap-start flex flex-col py-6 relative">
|
||
<div className="w-full max-w-[1600px] mx-auto px-6 lg:px-12 flex-1 flex flex-col">
|
||
|
||
{/* 1. Top Utility Row (Separated) */}
|
||
<div className="flex justify-end items-center gap-3 mb-4 animate-in fade-in slide-in-from-top-2">
|
||
<Button variant="ghost" size="sm" className="h-8 px-4 text-xs font-medium border border-white/5 bg-white/5 hover:bg-white/10 hover:text-white transition-all text-white/60 rounded-full">
|
||
<HelpCircle className="w-3.5 h-3.5 mr-2 opacity-70" /> 开发文档
|
||
</Button>
|
||
<Button variant="ghost" size="sm" className="h-8 px-4 text-xs font-medium border border-white/5 bg-white/5 hover:bg-white/10 hover:text-white transition-all text-white/60 rounded-full">
|
||
<Mail className="w-3.5 h-3.5 mr-2 opacity-70" /> 问题反馈
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 2. Main Dashboard Container */}
|
||
<div className="flex-1 bg-card/20 backdrop-blur-sm border border-white/5 rounded-[2rem] p-8 shadow-2xl flex flex-col gap-8 relative overflow-hidden group/dash">
|
||
{/* Decorative Background for Container */}
|
||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-primary/5 blur-[120px] rounded-full pointer-events-none -mr-32 -mt-32"></div>
|
||
|
||
{/* Header Area */}
|
||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 relative z-10">
|
||
<div className="space-y-1">
|
||
<div className="flex items-center gap-4">
|
||
<h1 className="text-3xl font-black tracking-tight text-white flex items-center gap-3">
|
||
<div className="h-3 w-3 rounded-full bg-primary shadow-[0_0_15px_rgba(6,182,212,0.8)] animate-pulse"></div>
|
||
控制台
|
||
</h1>
|
||
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-primary/10 text-primary border border-primary/20 tracking-wider">LIVE</span>
|
||
</div>
|
||
<p className="text-muted-foreground text-sm font-medium tracking-wide pl-7 opacity-80">
|
||
实时监控智能体运行状态与业务核心指标
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-4">
|
||
{/* Create Assistant CTA */}
|
||
<Button
|
||
onClick={() => navigate('/assistants')}
|
||
className="h-10 px-6 rounded-full bg-gradient-to-r from-primary to-blue-600 hover:from-primary/90 hover:to-blue-600/90 shadow-[0_0_20px_rgba(6,182,212,0.3)] border-0 font-bold tracking-wide"
|
||
>
|
||
<Plus className="w-4 h-4 mr-2 stroke-[3]" /> 创建助手
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Filters Row */}
|
||
<div className="flex items-center justify-between pt-2 pb-2 border-b border-white/5 relative z-10">
|
||
<div className="flex items-center gap-2 text-xs font-bold text-muted-foreground uppercase tracking-widest">
|
||
<MonitorPlay className="w-4 h-4 text-primary" />
|
||
<span>数据总览</span>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3 bg-black/20 p-1 rounded-xl border border-white/10">
|
||
<div className="relative group min-w-[140px]">
|
||
<select
|
||
className="w-full bg-transparent border-0 rounded-lg pl-3 pr-8 py-1.5 text-xs font-bold focus:outline-none appearance-none cursor-pointer transition-all text-white hover:text-primary"
|
||
value={selectedAssistantId}
|
||
onChange={(e) => setSelectedAssistantId(e.target.value)}
|
||
>
|
||
<option value="all" className="bg-background">全平台概览</option>
|
||
{mockAssistants.map(a => (
|
||
<option key={a.id} value={a.id} className="bg-background">{a.name}</option>
|
||
))}
|
||
</select>
|
||
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground pointer-events-none" />
|
||
</div>
|
||
<div className="w-px h-4 bg-white/10"></div>
|
||
<div className="flex gap-1">
|
||
{(['week', 'month', 'year'] as const).map((r) => (
|
||
<button
|
||
key={r}
|
||
onClick={() => setTimeRange(r)}
|
||
className={`px-3 py-1 text-[10px] font-bold uppercase tracking-wider rounded-lg transition-all ${timeRange === r ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:bg-white/5 hover:text-white'}`}
|
||
>
|
||
{r === 'week' ? '本周' : r === 'month' ? '本月' : '全年'}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Metrics Grid */}
|
||
<div className="grid gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 relative z-10">
|
||
<StatCard
|
||
title="总通话数量"
|
||
subtitle="累计呼叫"
|
||
value={stats.totalCalls.toString()}
|
||
icon={<Phone />}
|
||
trend="+12.5%"
|
||
color="cyan"
|
||
/>
|
||
<StatCard
|
||
title="平均接通率"
|
||
subtitle="接听占比"
|
||
value={`${stats.answerRate}%`}
|
||
icon={<CheckCircle />}
|
||
trend="+2.1%"
|
||
color="emerald"
|
||
/>
|
||
<StatCard
|
||
title="平均通话时长"
|
||
subtitle="持续时间"
|
||
value={stats.avgDuration}
|
||
icon={<Clock />}
|
||
trend="-0.5%"
|
||
color="blue"
|
||
/>
|
||
<StatCard
|
||
title="转人工服务"
|
||
subtitle="人工介入"
|
||
value={stats.humanTransferCount.toString()}
|
||
icon={<UserCheck />}
|
||
trend="+5%"
|
||
color="violet"
|
||
/>
|
||
</div>
|
||
|
||
{/* Chart Section */}
|
||
<div className="flex-1 min-h-[300px] relative z-10">
|
||
<Card className="h-full p-6 border-white/5 bg-black/20 shadow-inner relative overflow-hidden group rounded-2xl">
|
||
<div className="flex flex-col h-full relative z-10">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div className="space-y-1">
|
||
<h3 className="text-lg font-bold text-white flex items-center gap-2">
|
||
<BarChart3 className="w-4 h-4 text-primary" />
|
||
业务趋势分析
|
||
</h3>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<span className="relative flex h-2 w-2">
|
||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
||
</span>
|
||
<span className="text-[10px] font-bold text-green-400 font-mono tracking-widest">实时</span>
|
||
</div>
|
||
</div>
|
||
<div className="flex-1 w-full relative">
|
||
<SimpleAreaChart data={stats.trend} />
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Scroll Indicator */}
|
||
<div
|
||
onClick={() => scrollToNext(workflowRef)}
|
||
className="absolute bottom-6 left-1/2 -translate-x-1/2 flex flex-col items-center gap-3 cursor-pointer group animate-bounce-slow opacity-40 hover:opacity-100 transition-all duration-500 hover:scale-110"
|
||
>
|
||
<span className="text-[10px] font-bold text-primary tracking-[0.3em] uppercase group-hover:tracking-[0.5em] transition-all">核心流程</span>
|
||
<div className="p-2 rounded-full bg-primary/10 border border-primary/20 backdrop-blur-sm group-hover:bg-primary group-hover:text-black transition-colors">
|
||
<ArrowDown className="w-5 h-5" />
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* SECTION 2: WORKFLOW LOGIC */}
|
||
<section
|
||
ref={workflowRef}
|
||
className="min-h-full snap-start flex flex-col items-center justify-center py-20 px-6 relative bg-gradient-to-b from-background via-black/40 to-background border-t border-white/5"
|
||
>
|
||
{/* Background Grid */}
|
||
<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:40px_40px] opacity-50 pointer-events-none"></div>
|
||
|
||
<div className="w-full max-w-[1400px] text-center space-y-20 relative z-10">
|
||
<div className="space-y-6">
|
||
<h2 className="text-4xl md:text-5xl font-black text-white tracking-tight">
|
||
全链路智能体生命周期
|
||
</h2>
|
||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
||
从构建到优化,为您提供一站式 <span className="text-primary font-bold">AI 视频通话</span> 解决方案闭环
|
||
</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 relative px-4 md:px-10">
|
||
{/* Connecting Line (Desktop) */}
|
||
<div className="hidden md:block absolute top-14 left-[12%] right-[12%] h-0.5 bg-gradient-to-r from-transparent via-white/10 to-transparent z-0">
|
||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-primary/50 to-transparent w-1/2 animate-shimmer"></div>
|
||
</div>
|
||
|
||
<WorkflowStep
|
||
step="01"
|
||
title="创建智能体"
|
||
icon={<Bot className="w-8 h-8" />}
|
||
desc="配置人设、知识库与工具链"
|
||
color="blue"
|
||
/>
|
||
<WorkflowStep
|
||
step="02"
|
||
title="调试与测试"
|
||
icon={<Zap className="w-8 h-8" />}
|
||
desc="在线调试、自动化压力测试"
|
||
color="yellow"
|
||
/>
|
||
<WorkflowStep
|
||
step="03"
|
||
title="发布上线"
|
||
icon={<Rocket className="w-8 h-8" />}
|
||
desc="多端分发、API 极速接入"
|
||
color="cyan"
|
||
/>
|
||
<WorkflowStep
|
||
step="04"
|
||
title="监控运营"
|
||
icon={<LineChart className="w-8 h-8" />}
|
||
desc="全量日志、核心指标监控"
|
||
color="purple"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Scroll Indicator */}
|
||
<div
|
||
onClick={() => scrollToNext(aboutRef)}
|
||
className="absolute bottom-10 left-1/2 -translate-x-1/2 cursor-pointer opacity-30 hover:opacity-100 transition-opacity p-4 flex flex-col items-center gap-2 hover:scale-110 duration-300"
|
||
>
|
||
<span className="text-[10px] text-muted-foreground tracking-widest font-bold">了解更多</span>
|
||
<ArrowDown className="w-6 h-6 text-white animate-bounce" />
|
||
</div>
|
||
</section>
|
||
|
||
{/* SECTION 3: ABOUT */}
|
||
<section
|
||
ref={aboutRef}
|
||
className="min-h-full snap-start flex flex-col items-center justify-center py-20 px-6 relative bg-gradient-to-t from-primary/5 to-transparent border-t border-white/5"
|
||
>
|
||
<div className="w-full max-w-[1200px] relative">
|
||
<div className="relative z-10 flex flex-col items-center text-center space-y-10">
|
||
|
||
<div className="space-y-4 mt-8">
|
||
<h2 className="text-5xl md:text-6xl font-black text-white tracking-tighter">
|
||
关于 <span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-blue-500">AI 视频助手</span>
|
||
</h2>
|
||
<p className="text-2xl text-muted-foreground max-w-3xl mx-auto leading-relaxed font-light">
|
||
致力打造下一代多模态实时交互体验,让 AI 像真人一样看、听、说。
|
||
</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 w-full pt-16 text-left">
|
||
<FeatureCard icon={<Layers className="text-blue-400" />} title="多模态融合" desc="文本、音频、视频流实时同步处理,毫秒级响应" />
|
||
<FeatureCard icon={<Fingerprint className="text-purple-400" />} title="私有化知识" desc="深度集成 RAG 技术,确保企业数据安全可控" />
|
||
<FeatureCard icon={<Network className="text-emerald-400" />} title="低延迟架构" desc="全球节点加速,WebRTC 端到端加密传输" />
|
||
<FeatureCard icon={<Zap className="text-yellow-400" />} title="极速集成" desc="提供完善的 SDK 与 API 文档,5分钟完成上线" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<footer className="absolute bottom-4 text-[10px] text-muted-foreground opacity-30 font-mono">
|
||
© 2024 AI 视频助手. 保留所有权利.
|
||
</footer>
|
||
</section>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// --- Sub Components ---
|
||
|
||
const StatCard: React.FC<{
|
||
title: string;
|
||
subtitle: string;
|
||
value: string;
|
||
icon: React.ReactNode;
|
||
trend?: string;
|
||
color?: 'cyan' | 'emerald' | 'blue' | 'violet'
|
||
}> = ({ title, subtitle, value, icon, trend, color = 'cyan' }) => {
|
||
const colors = {
|
||
cyan: {
|
||
icon: "text-cyan-400",
|
||
bg: "bg-cyan-500/10",
|
||
border: "border-cyan-500/20",
|
||
glow: "group-hover:shadow-[0_0_30px_-5px_rgba(34,211,238,0.3)]",
|
||
text: "group-hover:text-cyan-400",
|
||
trend: "text-cyan-400 bg-cyan-400/10 border-cyan-400/20"
|
||
},
|
||
emerald: {
|
||
icon: "text-emerald-400",
|
||
bg: "bg-emerald-500/10",
|
||
border: "border-emerald-500/20",
|
||
glow: "group-hover:shadow-[0_0_30px_-5px_rgba(52,211,153,0.3)]",
|
||
text: "group-hover:text-emerald-400",
|
||
trend: "text-emerald-400 bg-emerald-400/10 border-emerald-400/20"
|
||
},
|
||
blue: {
|
||
icon: "text-blue-400",
|
||
bg: "bg-blue-500/10",
|
||
border: "border-blue-500/20",
|
||
glow: "group-hover:shadow-[0_0_30px_-5px_rgba(96,165,250,0.3)]",
|
||
text: "group-hover:text-blue-400",
|
||
trend: "text-blue-400 bg-blue-400/10 border-blue-400/20"
|
||
},
|
||
violet: {
|
||
icon: "text-violet-400",
|
||
bg: "bg-violet-500/10",
|
||
border: "border-violet-500/20",
|
||
glow: "group-hover:shadow-[0_0_30px_-5px_rgba(167,139,250,0.3)]",
|
||
text: "group-hover:text-violet-400",
|
||
trend: "text-violet-400 bg-violet-400/10 border-violet-400/20"
|
||
}
|
||
};
|
||
|
||
const theme = colors[color];
|
||
|
||
return (
|
||
<div className={`relative overflow-hidden rounded-2xl border border-white/5 bg-gradient-to-br from-card/80 to-card/40 p-6 transition-all duration-500 group backdrop-blur-md hover:border-white/20 ${theme.glow}`}>
|
||
{/* Decorative Background Blob */}
|
||
<div className={`absolute -right-12 -top-12 h-40 w-40 rounded-full blur-[60px] opacity-0 group-hover:opacity-20 transition-opacity duration-700 ${theme.bg.replace('/10', '')}`} />
|
||
|
||
<div className="relative z-10 flex flex-col justify-between h-full gap-8">
|
||
<div className="flex justify-between items-start">
|
||
<div className={`p-3.5 rounded-xl border backdrop-blur-md transition-all duration-500 group-hover:scale-110 group-hover:-rotate-3 ${theme.bg} ${theme.border}`}>
|
||
{React.cloneElement(icon as React.ReactElement, { className: `w-7 h-7 ${theme.icon}` })}
|
||
</div>
|
||
{trend && (
|
||
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-bold font-mono border backdrop-blur-md ${theme.trend}`}>
|
||
<span>{trend}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<div className="flex items-center gap-2">
|
||
<h3 className="text-xs font-bold text-muted-foreground uppercase tracking-wider">{title}</h3>
|
||
<span className="text-[10px] text-muted-foreground/40 font-bold hidden group-hover:inline-block transition-all">{subtitle}</span>
|
||
</div>
|
||
<div className={`text-5xl font-black text-white tracking-tight tabular-nums transition-colors duration-300 ${theme.text}`}>
|
||
{value}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const WorkflowStep: React.FC<{ step: string; title: string; desc: string; icon: React.ReactNode; color: 'blue' | 'yellow' | 'cyan' | 'purple' }> = ({ step, title, desc, icon, color }) => {
|
||
const colors = {
|
||
blue: "text-blue-400 border-blue-500/30 hover:border-blue-400 hover:shadow-[0_0_30px_rgba(96,165,250,0.3)] bg-blue-500/10",
|
||
yellow: "text-yellow-400 border-yellow-500/30 hover:border-yellow-400 hover:shadow-[0_0_30px_rgba(250,204,21,0.3)] bg-yellow-500/10",
|
||
cyan: "text-cyan-400 border-cyan-500/30 hover:border-cyan-400 hover:shadow-[0_0_30px_rgba(34,211,238,0.3)] bg-cyan-500/10",
|
||
purple: "text-purple-400 border-purple-500/30 hover:border-purple-400 hover:shadow-[0_0_30px_rgba(192,132,252,0.3)] bg-purple-500/10"
|
||
};
|
||
|
||
return (
|
||
<div className="relative z-10 flex flex-col items-center text-center group">
|
||
<div className={`w-24 h-24 rounded-3xl flex items-center justify-center border-2 transition-all duration-500 mb-6 backdrop-blur-xl ${colors[color]} group-hover:scale-110 group-hover:-translate-y-2`}>
|
||
<div className="transform transition-transform duration-500 group-hover:scale-110 group-hover:rotate-6">
|
||
{icon}
|
||
</div>
|
||
<div className="absolute -top-3 -right-3 w-8 h-8 rounded-full bg-background border border-white/10 flex items-center justify-center text-[10px] font-black font-mono text-muted-foreground shadow-lg">
|
||
{step}
|
||
</div>
|
||
</div>
|
||
<h3 className="text-lg font-bold text-white mb-2 group-hover:text-white transition-colors">{title}</h3>
|
||
<p className="text-sm text-muted-foreground leading-snug max-w-[200px] group-hover:text-white/70 transition-colors">{desc}</p>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const FeatureCard: React.FC<{ icon: React.ReactNode; title: string; desc: string }> = ({ icon, title, desc }) => (
|
||
<div className="p-6 rounded-2xl bg-white/[0.03] border border-white/5 hover:bg-white/[0.06] hover:border-white/10 transition-all duration-300 group">
|
||
<div className="mb-4 p-3 bg-black/20 rounded-xl w-fit group-hover:scale-110 transition-transform duration-300 [&>svg]:w-6 [&>svg]:h-6">
|
||
{icon}
|
||
</div>
|
||
<h3 className="text-lg font-bold text-white mb-2">{title}</h3>
|
||
<p className="text-sm text-muted-foreground leading-relaxed">{desc}</p>
|
||
</div>
|
||
);
|
||
|
||
const SimpleAreaChart: React.FC<{ data: { label: string, value: number }[] }> = ({ data }) => {
|
||
if (!data || data.length === 0) return null;
|
||
|
||
const height = 300;
|
||
const width = 1400;
|
||
const padding = 30;
|
||
|
||
const maxValue = Math.max(...data.map(d => d.value)) * 1.2;
|
||
const points = data.map((d, i) => {
|
||
const x = (i / (data.length - 1)) * (width - padding * 2) + padding;
|
||
const y = height - (d.value / maxValue) * (height - padding * 2) - padding;
|
||
return `${x},${y}`;
|
||
}).join(' ');
|
||
|
||
const firstPoint = points.split(' ')[0];
|
||
const lastPoint = points.split(' ')[points.split(' ').length - 1];
|
||
const fillPath = `${points} ${lastPoint.split(',')[0]},${height} ${firstPoint.split(',')[0]},${height}`;
|
||
|
||
return (
|
||
<div className="w-full h-full relative">
|
||
<svg viewBox={`0 0 ${width} ${height}`} className="w-full h-full overflow-visible" preserveAspectRatio="none">
|
||
{/* Grid Lines */}
|
||
<line x1={padding} y1={height - padding} x2={width - padding} y2={height - padding} stroke="rgba(255,255,255,0.03)" strokeWidth="1" />
|
||
{[0.25, 0.5, 0.75].map(v => (
|
||
<line
|
||
key={v}
|
||
x1={padding} y1={height - padding - ((height - padding * 2) * v)}
|
||
x2={width - padding} y2={height - padding - ((height - padding * 2) * v)}
|
||
stroke="rgba(255,255,255,0.02)" strokeWidth="1" strokeDasharray="8 6"
|
||
/>
|
||
))}
|
||
|
||
{/* Area Fill Gradient */}
|
||
<defs>
|
||
<linearGradient id="chartGradient" x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="0%" stopColor="hsl(var(--primary))" stopOpacity="0.4" />
|
||
<stop offset="100%" stopColor="hsl(var(--primary))" stopOpacity="0" />
|
||
</linearGradient>
|
||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||
<feGaussianBlur stdDeviation="5" result="coloredBlur" />
|
||
<feMerge>
|
||
<feMergeNode in="coloredBlur" />
|
||
<feMergeNode in="SourceGraphic" />
|
||
</feMerge>
|
||
</filter>
|
||
</defs>
|
||
|
||
<polygon points={fillPath} fill="url(#chartGradient)" />
|
||
|
||
<polyline
|
||
points={points}
|
||
fill="none"
|
||
stroke="hsl(var(--primary))"
|
||
strokeWidth="3"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
filter="url(#glow)"
|
||
/>
|
||
|
||
{data.length < 32 && data.map((d, i) => {
|
||
const x = (i / (data.length - 1)) * (width - padding * 2) + padding;
|
||
const y = height - (d.value / maxValue) * (height - padding * 2) - padding;
|
||
return (
|
||
<g key={i} className="group/dot">
|
||
<circle cx={x} cy={y} r="3.5" fill="hsl(var(--background))" stroke="hsl(var(--primary))" strokeWidth="2" className="transition-all duration-300 group-hover/dot:r-5 group-hover/dot:stroke-white" />
|
||
<circle cx={x} cy={y} r="10" fill="hsl(var(--primary))" fillOpacity="0" className="cursor-pointer" />
|
||
</g>
|
||
);
|
||
})}
|
||
</svg>
|
||
|
||
{/* X-Axis Labels */}
|
||
<div className="absolute bottom-0 left-0 right-0 flex justify-between px-[3%] text-[9px] text-muted-foreground pointer-events-none font-mono opacity-50 mt-3">
|
||
{data.filter((_, i) => i % Math.ceil(data.length / 7) === 0).map((d, i) => (
|
||
<span key={i}>{d.label}</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|