From e0a6ba2dc4e554e58599e0e9659d05fc2e09fda5 Mon Sep 17 00:00:00 2001 From: tangweijie <877588133@qq.com> Date: Mon, 5 Jan 2026 18:08:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=20React=20?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9C=8B=E6=9D=BF=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Vite + React 18 + TypeScript - TailwindCSS 暗色主题 - 仪表板、分析、事件浏览页面 - Recharts 图表组件 - Zustand 状态管理 - TanStack Query 数据请求 --- README.md | 114 ++++++++++++++++++++- index.html | 17 ++++ package.json | 43 ++++++++ postcss.config.js | 7 ++ public/vite.svg | 4 + src/App.tsx | 22 +++++ src/components/layout/Header.tsx | 48 +++++++++ src/components/layout/Layout.tsx | 18 ++++ src/components/layout/Sidebar.tsx | 68 +++++++++++++ src/hooks/useAnalytics.ts | 27 +++++ src/index.css | 74 ++++++++++++++ src/lib/utils.ts | 34 +++++++ src/main.tsx | 26 +++++ src/pages/Analytics/index.tsx | 48 +++++++++ src/pages/Dashboard/AcceptanceChart.tsx | 69 +++++++++++++ src/pages/Dashboard/ActivityChart.tsx | 60 ++++++++++++ src/pages/Dashboard/RecentEvents.tsx | 74 ++++++++++++++ src/pages/Dashboard/StatsCard.tsx | 84 ++++++++++++++++ src/pages/Dashboard/index.tsx | 103 +++++++++++++++++++ src/pages/Events/index.tsx | 120 +++++++++++++++++++++++ src/pages/Settings/index.tsx | 125 ++++++++++++++++++++++++ src/services/api.ts | 48 +++++++++ src/stores/appStore.ts | 26 +++++ src/types/index.ts | 59 +++++++++++ tailwind.config.js | 61 ++++++++++++ tsconfig.json | 32 ++++++ tsconfig.node.json | 12 +++ vite.config.ts | 23 +++++ 28 files changed, 1444 insertions(+), 2 deletions(-) create mode 100644 index.html create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 public/vite.svg create mode 100644 src/App.tsx create mode 100644 src/components/layout/Header.tsx create mode 100644 src/components/layout/Layout.tsx create mode 100644 src/components/layout/Sidebar.tsx create mode 100644 src/hooks/useAnalytics.ts create mode 100644 src/index.css create mode 100644 src/lib/utils.ts create mode 100644 src/main.tsx create mode 100644 src/pages/Analytics/index.tsx create mode 100644 src/pages/Dashboard/AcceptanceChart.tsx create mode 100644 src/pages/Dashboard/ActivityChart.tsx create mode 100644 src/pages/Dashboard/RecentEvents.tsx create mode 100644 src/pages/Dashboard/StatsCard.tsx create mode 100644 src/pages/Dashboard/index.tsx create mode 100644 src/pages/Events/index.tsx create mode 100644 src/pages/Settings/index.tsx create mode 100644 src/services/api.ts create mode 100644 src/stores/appStore.ts create mode 100644 src/types/index.ts create mode 100644 tailwind.config.js create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/README.md b/README.md index fbdfcd3..fe68a1c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,113 @@ -# collector-dashboard +# Collector Dashboard -数据分析看板 - React + TypeScript + Vite \ No newline at end of file +数据分析看板 - React + TypeScript + Vite + +## 项目介绍 + +Collector Dashboard 是 IDE 数据采集系统的可视化前端,提供数据分析、事件浏览和配置管理等功能。 + +## 技术栈 + +- **框架**: React 18 +- **语言**: TypeScript +- **构建工具**: Vite +- **样式**: TailwindCSS +- **状态管理**: Zustand +- **数据请求**: TanStack Query +- **图表**: Recharts +- **路由**: React Router + +## 功能特性 + +- 📊 数据概览仪表板 +- 📈 采纳率趋势分析 +- 📋 事件浏览和搜索 +- ⚙️ 配置管理界面 +- 🌙 暗色主题设计 + +## 快速开始 + +### 环境要求 + +- Node.js >= 18 +- pnpm >= 8 + +### 安装 + +```bash +# 安装依赖 +pnpm install + +# 启动开发服务器 +pnpm dev + +# 构建生产版本 +pnpm build +``` + +### 开发 + +```bash +# 启动开发服务器(端口 5173) +pnpm dev + +# 代码检查 +pnpm lint + +# 预览生产构建 +pnpm preview +``` + +## 项目结构 + +``` +collector-dashboard/ +├── src/ +│ ├── components/ +│ │ ├── layout/ # 布局组件 +│ │ ├── charts/ # 图表组件 +│ │ └── common/ # 通用组件 +│ ├── pages/ +│ │ ├── Dashboard/ # 仪表板页面 +│ │ ├── Analytics/ # 分析页面 +│ │ ├── Events/ # 事件页面 +│ │ └── Settings/ # 设置页面 +│ ├── hooks/ # 自定义 Hooks +│ ├── services/ # API 服务 +│ ├── stores/ # 状态管理 +│ ├── types/ # 类型定义 +│ ├── lib/ # 工具函数 +│ ├── App.tsx +│ └── main.tsx +├── public/ +├── index.html +├── package.json +├── tailwind.config.js +├── vite.config.ts +└── tsconfig.json +``` + +## API 代理 + +开发模式下,API 请求会代理到 `http://localhost:8000`: + +```typescript +// vite.config.ts +server: { + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + }, +} +``` + +## 相关仓库 + +- [ide-data-collector](../ide-data-collector) - IDE插件 Monorepo +- [collector-backend](../collector-backend) - 数据采集后端 + +## 许可证 + +MIT License diff --git a/index.html b/index.html new file mode 100644 index 0000000..56ede46 --- /dev/null +++ b/index.html @@ -0,0 +1,17 @@ + + + + + + + IDE Collector Dashboard + + + + + +
+ + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..1382bdc --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "collector-dashboard", + "version": "0.1.0", + "private": true, + "description": "数据分析看板 - React + TypeScript + Vite", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/react-query": "^5.17.0", + "axios": "^1.6.0", + "clsx": "^2.1.0", + "date-fns": "^3.0.0", + "lucide-react": "^0.303.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.0", + "recharts": "^2.10.0", + "tailwind-merge": "^2.2.0", + "zustand": "^4.4.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-react": "^4.2.0", + "autoprefixer": "^10.4.16", + "eslint": "^8.55.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.32", + "tailwindcss": "^3.4.0", + "typescript": "^5.3.0", + "vite": "^5.0.0" + } +} + diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..1d92651 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,7 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..d16605c --- /dev/null +++ b/public/vite.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..096d1ae --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,22 @@ +import { Routes, Route } from 'react-router-dom'; +import Layout from '@/components/layout/Layout'; +import Dashboard from '@/pages/Dashboard'; +import Analytics from '@/pages/Analytics'; +import Events from '@/pages/Events'; +import Settings from '@/pages/Settings'; + +function App() { + return ( + + }> + } /> + } /> + } /> + } /> + + + ); +} + +export default App; + diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 0000000..35f37fb --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,48 @@ +import { Bell, Search, RefreshCw } from 'lucide-react'; +import { useAppStore } from '@/stores/appStore'; + +export default function Header() { + const { lastRefresh, refreshData } = useAppStore(); + + return ( +
+ {/* Search */} +
+
+ + +
+
+ + {/* Actions */} +
+ {/* Last Refresh */} +
+ 上次刷新: {lastRefresh ? new Date(lastRefresh).toLocaleTimeString() : '-'} +
+ + {/* Refresh Button */} + + + {/* Notifications */} + + + {/* User */} +
+
+
+ ); +} + diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx new file mode 100644 index 0000000..07ef9df --- /dev/null +++ b/src/components/layout/Layout.tsx @@ -0,0 +1,18 @@ +import { Outlet } from 'react-router-dom'; +import Sidebar from './Sidebar'; +import Header from './Header'; + +export default function Layout() { + return ( +
+ +
+
+
+ +
+
+
+ ); +} + diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..9a7f1d4 --- /dev/null +++ b/src/components/layout/Sidebar.tsx @@ -0,0 +1,68 @@ +import { NavLink } from 'react-router-dom'; +import { + LayoutDashboard, + BarChart3, + FileText, + Settings, + Zap +} from 'lucide-react'; +import { cn } from '@/lib/utils'; + +const navItems = [ + { to: '/', icon: LayoutDashboard, label: '仪表板' }, + { to: '/analytics', icon: BarChart3, label: '数据分析' }, + { to: '/events', icon: FileText, label: '事件浏览' }, + { to: '/settings', icon: Settings, label: '设置' }, +]; + +export default function Sidebar() { + return ( + + ); +} + diff --git a/src/hooks/useAnalytics.ts b/src/hooks/useAnalytics.ts new file mode 100644 index 0000000..625b254 --- /dev/null +++ b/src/hooks/useAnalytics.ts @@ -0,0 +1,27 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '@/services/api'; + +export function useOverview() { + return useQuery({ + queryKey: ['overview'], + queryFn: api.getOverview, + staleTime: 1000 * 60 * 5, // 5 minutes + }); +} + +export function useAcceptanceRate() { + return useQuery({ + queryKey: ['acceptanceRate'], + queryFn: api.getAcceptanceRate, + staleTime: 1000 * 60 * 5, + }); +} + +export function useEvents(params: { skip?: number; limit?: number } = {}) { + return useQuery({ + queryKey: ['events', params], + queryFn: () => api.getEvents(params), + staleTime: 1000 * 60, // 1 minute + }); +} + diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..bd07191 --- /dev/null +++ b/src/index.css @@ -0,0 +1,74 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: #0a0a0f; + --foreground: #fafafa; + --card: #111118; + --card-foreground: #fafafa; + --border: #27272a; + --ring: #3b82f6; +} + +body { + @apply bg-[var(--background)] text-[var(--foreground)] antialiased; + font-feature-settings: 'liga' 1, 'calt' 1; +} + +/* 自定义滚动条 */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + @apply bg-zinc-900; +} + +::-webkit-scrollbar-thumb { + @apply bg-zinc-700 rounded-full; +} + +::-webkit-scrollbar-thumb:hover { + @apply bg-zinc-600; +} + +/* 图表渐变 */ +.chart-gradient { + background: linear-gradient(180deg, rgba(14, 165, 233, 0.2) 0%, rgba(14, 165, 233, 0) 100%); +} + +/* 卡片悬浮效果 */ +.card-hover { + @apply transition-all duration-300 ease-out; +} + +.card-hover:hover { + @apply transform -translate-y-1 shadow-lg shadow-primary-500/10; +} + +/* 数字动画 */ +@keyframes countUp { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-count-up { + animation: countUp 0.5s ease-out forwards; +} + +/* 网格背景 */ +.grid-background { + background-image: + linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px); + background-size: 50px 50px; +} + diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..41f092e --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,34 @@ +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function formatNumber(num: number): string { + return new Intl.NumberFormat('zh-CN').format(num); +} + +export function formatPercentage(value: number, decimals = 1): string { + return `${(value * 100).toFixed(decimals)}%`; +} + +export function formatDate(date: Date | string): string { + const d = typeof date === 'string' ? new Date(date) : date; + return d.toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }); +} + +export function formatDateTime(date: Date | string): string { + const d = typeof date === 'string' ? new Date(date) : date; + return d.toLocaleString('zh-CN'); +} + +export function truncate(str: string, length: number): string { + if (str.length <= length) return str; + return str.slice(0, length) + '...'; +} + diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..0fa0fb1 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App'; +import './index.css'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, // 5 minutes + refetchOnWindowFocus: false, + }, + }, +}); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + +); + diff --git a/src/pages/Analytics/index.tsx b/src/pages/Analytics/index.tsx new file mode 100644 index 0000000..5fd97cf --- /dev/null +++ b/src/pages/Analytics/index.tsx @@ -0,0 +1,48 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '@/services/api'; + +export default function Analytics() { + const { data: acceptanceRate } = useQuery({ + queryKey: ['acceptanceRate'], + queryFn: api.getAcceptanceRate, + }); + + return ( +
+
+

数据分析

+

深入分析AI编程工具使用数据

+
+ +
+ {/* Acceptance Rate Section */} +
+

采纳率分析

+
+ {((acceptanceRate?.overall ?? 0) * 100).toFixed(1)}% +
+

总体采纳率

+
+ + {/* Token Usage Section */} +
+

Token 使用统计

+

详细的 Token 消耗分析

+
+ + {/* Provider Comparison */} +
+

提供商对比

+

不同AI服务商的效果对比

+
+ + {/* Language Distribution */} +
+

语言分布

+

按编程语言统计使用情况

+
+
+
+ ); +} + diff --git a/src/pages/Dashboard/AcceptanceChart.tsx b/src/pages/Dashboard/AcceptanceChart.tsx new file mode 100644 index 0000000..c7ae273 --- /dev/null +++ b/src/pages/Dashboard/AcceptanceChart.tsx @@ -0,0 +1,69 @@ +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from 'recharts'; + +const data = [ + { date: '1/1', rate: 65 }, + { date: '1/2', rate: 72 }, + { date: '1/3', rate: 68 }, + { date: '1/4', rate: 75 }, + { date: '1/5', rate: 82 }, + { date: '1/6', rate: 78 }, + { date: '1/7', rate: 85 }, +]; + +export default function AcceptanceChart() { + return ( +
+ + + + + + + + + + + `${value}%`} + /> + [`${value}%`, '采纳率']} + /> + + + +
+ ); +} + diff --git a/src/pages/Dashboard/ActivityChart.tsx b/src/pages/Dashboard/ActivityChart.tsx new file mode 100644 index 0000000..a3dbfb4 --- /dev/null +++ b/src/pages/Dashboard/ActivityChart.tsx @@ -0,0 +1,60 @@ +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from 'recharts'; + +const data = [ + { hour: '00', events: 120 }, + { hour: '04', events: 80 }, + { hour: '08', events: 450 }, + { hour: '12', events: 380 }, + { hour: '16', events: 520 }, + { hour: '20', events: 280 }, +]; + +export default function ActivityChart() { + return ( +
+ + + + `${value}:00`} + /> + + [value, '事件数']} + /> + + + +
+ ); +} + diff --git a/src/pages/Dashboard/RecentEvents.tsx b/src/pages/Dashboard/RecentEvents.tsx new file mode 100644 index 0000000..91726c2 --- /dev/null +++ b/src/pages/Dashboard/RecentEvents.tsx @@ -0,0 +1,74 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '@/services/api'; +import { cn } from '@/lib/utils'; +import { CheckCircle2, XCircle, MessageSquare, Code } from 'lucide-react'; + +const eventTypeConfig: Record = { + code_completion_accepted: { + icon: CheckCircle2, + color: 'text-emerald-400', + label: '补全接受', + }, + code_completion_rejected: { + icon: XCircle, + color: 'text-rose-400', + label: '补全拒绝', + }, + chat_message_sent: { + icon: MessageSquare, + color: 'text-primary-400', + label: '聊天消息', + }, + code_completion_shown: { + icon: Code, + color: 'text-zinc-400', + label: '补全展示', + }, +}; + +export default function RecentEvents() { + const { data: events, isLoading } = useQuery({ + queryKey: ['recentEvents'], + queryFn: () => api.getEvents({ limit: 10 }), + }); + + if (isLoading) { + return ( +
+ {[...Array(5)].map((_, i) => ( +
+ ))} +
+ ); + } + + return ( +
+ {(events ?? []).map((event) => { + const config = eventTypeConfig[event.eventType] ?? eventTypeConfig.code_completion_shown; + const Icon = config.icon; + + return ( +
+
+ +
+
+

{config.label}

+

+ {event.language ?? 'unknown'} · {event.ideType} +

+
+
+ {new Date(event.timestamp).toLocaleTimeString()} +
+
+ ); + })} +
+ ); +} + diff --git a/src/pages/Dashboard/StatsCard.tsx b/src/pages/Dashboard/StatsCard.tsx new file mode 100644 index 0000000..ffee7a6 --- /dev/null +++ b/src/pages/Dashboard/StatsCard.tsx @@ -0,0 +1,84 @@ +import { LucideIcon } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface StatsCardProps { + title: string; + value: string | number; + change: string; + icon: LucideIcon; + color: 'primary' | 'emerald' | 'violet' | 'amber'; + delay?: number; + isLoading?: boolean; +} + +const colorMap = { + primary: { + bg: 'bg-primary-500/10', + text: 'text-primary-400', + border: 'border-primary-500/20', + }, + emerald: { + bg: 'bg-emerald-500/10', + text: 'text-emerald-400', + border: 'border-emerald-500/20', + }, + violet: { + bg: 'bg-violet-500/10', + text: 'text-violet-400', + border: 'border-violet-500/20', + }, + amber: { + bg: 'bg-amber-500/10', + text: 'text-amber-400', + border: 'border-amber-500/20', + }, +}; + +export default function StatsCard({ + title, + value, + change, + icon: Icon, + color, + delay = 0, + isLoading, +}: StatsCardProps) { + const colors = colorMap[color]; + const isPositive = change.startsWith('+'); + + return ( +
+
+
+

{title}

+ {isLoading ? ( +
+ ) : ( +

{value}

+ )} +
+
+ +
+
+
+ + {change} + + vs 上周 +
+
+ ); +} + diff --git a/src/pages/Dashboard/index.tsx b/src/pages/Dashboard/index.tsx new file mode 100644 index 0000000..e12c274 --- /dev/null +++ b/src/pages/Dashboard/index.tsx @@ -0,0 +1,103 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '@/services/api'; +import StatsCard from './StatsCard'; +import AcceptanceChart from './AcceptanceChart'; +import ActivityChart from './ActivityChart'; +import RecentEvents from './RecentEvents'; +import { + Activity, + Users, + CheckCircle2, + Zap, + TrendingUp, + Clock +} from 'lucide-react'; + +export default function Dashboard() { + const { data: overview, isLoading } = useQuery({ + queryKey: ['overview'], + queryFn: api.getOverview, + }); + + const stats = [ + { + title: '总事件数', + value: overview?.totalEvents ?? 0, + change: '+12%', + icon: Activity, + color: 'primary', + }, + { + title: '活跃用户', + value: overview?.activeUsers ?? 0, + change: '+8%', + icon: Users, + color: 'emerald', + }, + { + title: '采纳率', + value: `${((overview?.acceptanceRate ?? 0) * 100).toFixed(1)}%`, + change: '+5%', + icon: CheckCircle2, + color: 'violet', + }, + { + title: '平均延迟', + value: `${overview?.avgLatencyMs ?? 0}ms`, + change: '-15%', + icon: Clock, + color: 'amber', + }, + ]; + + return ( +
+ {/* Page Title */} +
+
+

仪表板

+

AI编程工具使用数据概览

+
+
+ 时间范围: + +
+
+ + {/* Stats Grid */} +
+ {stats.map((stat, index) => ( + + ))} +
+ + {/* Charts */} +
+
+

采纳率趋势

+ +
+
+

活动分布

+ +
+
+ + {/* Recent Events */} +
+

最近事件

+ +
+
+ ); +} + diff --git a/src/pages/Events/index.tsx b/src/pages/Events/index.tsx new file mode 100644 index 0000000..582d157 --- /dev/null +++ b/src/pages/Events/index.tsx @@ -0,0 +1,120 @@ +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { api } from '@/services/api'; +import { cn } from '@/lib/utils'; +import { Search, Filter, ChevronLeft, ChevronRight } from 'lucide-react'; + +export default function Events() { + const [page, setPage] = useState(1); + const [search, setSearch] = useState(''); + const limit = 20; + + const { data: events, isLoading } = useQuery({ + queryKey: ['events', page, limit], + queryFn: () => api.getEvents({ skip: (page - 1) * limit, limit }), + }); + + return ( +
+
+
+

事件浏览

+

查看所有采集的事件数据

+
+
+ + {/* Filters */} +
+
+ + setSearch(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-[var(--card)] border border-[var(--border)] rounded-lg text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-primary-500/50" + /> +
+ +
+ + {/* Events Table */} +
+ + + + + + + + + + + + {isLoading ? ( + [...Array(10)].map((_, i) => ( + + + + )) + ) : ( + (events ?? []).map((event) => ( + + + + + + + + )) + )} + +
事件ID类型语言IDE时间
+
+
+ {event.eventId.substring(0, 8)}... + + + {event.eventType.replace(/_/g, ' ')} + + {event.language ?? '-'}{event.ideType} + {new Date(event.timestamp).toLocaleString()} +
+ + {/* Pagination */} +
+

+ 显示 {(page - 1) * limit + 1} - {page * limit} 条 +

+
+ + 第 {page} 页 + +
+
+
+
+ ); +} + diff --git a/src/pages/Settings/index.tsx b/src/pages/Settings/index.tsx new file mode 100644 index 0000000..13d156b --- /dev/null +++ b/src/pages/Settings/index.tsx @@ -0,0 +1,125 @@ +import { useState } from 'react'; +import { Save, RefreshCw } from 'lucide-react'; + +export default function Settings() { + const [settings, setSettings] = useState({ + apiEndpoint: 'http://localhost:8000/api/v1', + samplingRate: 1.0, + batchSize: 50, + flushInterval: 60, + anonymizeUser: true, + obfuscateCode: true, + }); + + const handleSave = () => { + console.log('Saving settings:', settings); + // TODO: Save to backend + }; + + return ( +
+
+

设置

+

配置数据采集和系统参数

+
+ + {/* API Settings */} +
+

API 配置

+ +
+ + setSettings({ ...settings, apiEndpoint: e.target.value })} + className="w-full px-4 py-2 bg-zinc-800 border border-[var(--border)] rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500/50" + /> +
+
+ + {/* Collection Settings */} +
+

采集配置

+ +
+
+ + setSettings({ ...settings, samplingRate: parseFloat(e.target.value) })} + className="w-full px-4 py-2 bg-zinc-800 border border-[var(--border)] rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500/50" + /> +
+
+ + setSettings({ ...settings, batchSize: parseInt(e.target.value) })} + className="w-full px-4 py-2 bg-zinc-800 border border-[var(--border)] rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500/50" + /> +
+
+ + setSettings({ ...settings, flushInterval: parseInt(e.target.value) })} + className="w-full px-4 py-2 bg-zinc-800 border border-[var(--border)] rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500/50" + /> +
+
+
+ + {/* Privacy Settings */} +
+

隐私配置

+ +
+ + +
+
+ + {/* Actions */} +
+ + +
+
+ ); +} + diff --git a/src/services/api.ts b/src/services/api.ts new file mode 100644 index 0000000..1ed5a0e --- /dev/null +++ b/src/services/api.ts @@ -0,0 +1,48 @@ +import axios from 'axios'; +import type { OverviewStats, AcceptanceRateStats, EventResponse } from '@/types'; + +const client = axios.create({ + baseURL: '/api/v1', + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +export const api = { + // Health + async healthCheck() { + const { data } = await client.get('/health'); + return data; + }, + + // Overview + async getOverview(): Promise { + const { data } = await client.get('/analytics/overview'); + return data; + }, + + // Acceptance Rate + async getAcceptanceRate(): Promise { + const { data } = await client.get('/analytics/acceptance-rate'); + return data; + }, + + // Events + async getEvents(params: { skip?: number; limit?: number } = {}): Promise { + const { data } = await client.get('/events', { params }); + return data; + }, + + async getEvent(eventId: string): Promise { + const { data } = await client.get(`/events/${eventId}`); + return data; + }, + + // Config + async getConfig() { + const { data } = await client.get('/config'); + return data; + }, +}; + diff --git a/src/stores/appStore.ts b/src/stores/appStore.ts new file mode 100644 index 0000000..fee2c9f --- /dev/null +++ b/src/stores/appStore.ts @@ -0,0 +1,26 @@ +import { create } from 'zustand'; + +interface AppState { + lastRefresh: Date | null; + isLoading: boolean; + error: string | null; + refreshData: () => void; + setLoading: (loading: boolean) => void; + setError: (error: string | null) => void; +} + +export const useAppStore = create((set) => ({ + lastRefresh: null, + isLoading: false, + error: null, + + refreshData: () => { + set({ lastRefresh: new Date() }); + // Trigger refetch via React Query + window.dispatchEvent(new CustomEvent('app:refresh')); + }, + + setLoading: (loading) => set({ isLoading: loading }), + setError: (error) => set({ error }), +})); + diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..606cd71 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,59 @@ +export interface OverviewStats { + totalEvents: number; + totalUsers: number; + activeUsers: number; + totalCompletions: number; + totalAccepted: number; + acceptanceRate: number; + avgLatencyMs: number; + totalTokens: number; + periodStart: string; + periodEnd: string; +} + +export interface AcceptanceRateStats { + overall: number; + timeSeries: TimeSeriesPoint[]; + byProvider: Record; + byLanguage: Record; + byIDE: Record; +} + +export interface TimeSeriesPoint { + timestamp: string; + value: number; +} + +export interface TokenUsageStats { + totalPromptTokens: number; + totalCompletionTokens: number; + totalTokens: number; + avgTokensPerCompletion: number; + timeSeries: TimeSeriesPoint[]; + byProvider: Record; +} + +export interface EventResponse { + id: number; + eventId: string; + eventType: string; + timestamp: string; + userId: string; + ideType: string; + language: string | null; + aiProvider: string | null; + accepted: boolean | null; + createdAt: string; +} + +export interface UserActivityStats { + dailyActiveUsers: TimeSeriesPoint[]; + hourlyDistribution: Record; + topUsers: Array<{ + userId: string; + eventCount: number; + }>; + avgEventsPerUser: number; + avgSessionsPerUser: number; +} + diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..e00a1cc --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,61 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + darkMode: 'class', + theme: { + extend: { + colors: { + primary: { + 50: '#f0f9ff', + 100: '#e0f2fe', + 200: '#bae6fd', + 300: '#7dd3fc', + 400: '#38bdf8', + 500: '#0ea5e9', + 600: '#0284c7', + 700: '#0369a1', + 800: '#075985', + 900: '#0c4a6e', + 950: '#082f49', + }, + accent: { + 50: '#fdf4ff', + 100: '#fae8ff', + 200: '#f5d0fe', + 300: '#f0abfc', + 400: '#e879f9', + 500: '#d946ef', + 600: '#c026d3', + 700: '#a21caf', + 800: '#86198f', + 900: '#701a75', + 950: '#4a044e', + }, + }, + fontFamily: { + sans: ['JetBrains Mono', 'Fira Code', 'monospace'], + display: ['Outfit', 'sans-serif'], + }, + animation: { + 'fade-in': 'fadeIn 0.3s ease-out', + 'slide-up': 'slideUp 0.4s ease-out', + 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', + }, + keyframes: { + fadeIn: { + '0%': { opacity: '0' }, + '100%': { opacity: '1' }, + }, + slideUp: { + '0%': { opacity: '0', transform: 'translateY(10px)' }, + '100%': { opacity: '1', transform: 'translateY(0)' }, + }, + }, + }, + }, + plugins: [], +}; + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b3a42a5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Paths */ + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} + diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..41cdb7d --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} + diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..9d2d3c8 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, +}); +