Files
AI-VideoAssistant/web/pages/Dashboard.tsx
2026-02-07 23:57:16 +08:00

474 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};