session-wrap-dashboard 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.env.example +6 -0
  2. package/coverage/base.css +224 -0
  3. package/coverage/block-navigation.js +87 -0
  4. package/coverage/coverage-final.json +6 -0
  5. package/coverage/favicon.png +0 -0
  6. package/coverage/index.html +146 -0
  7. package/coverage/prettify.css +1 -0
  8. package/coverage/prettify.js +2 -0
  9. package/coverage/sort-arrow-sprite.png +0 -0
  10. package/coverage/sorter.js +210 -0
  11. package/coverage/src/auth.ts.html +307 -0
  12. package/coverage/src/components/Header.tsx.html +262 -0
  13. package/coverage/src/components/Sidebar.tsx.html +262 -0
  14. package/coverage/src/components/index.html +131 -0
  15. package/coverage/src/hooks/index.html +131 -0
  16. package/coverage/src/hooks/useAuth.ts.html +292 -0
  17. package/coverage/src/hooks/useWorkspace.ts.html +475 -0
  18. package/coverage/src/index.html +116 -0
  19. package/index.html +13 -0
  20. package/package.json +49 -0
  21. package/playwright.config.ts +38 -0
  22. package/postcss.config.js +6 -0
  23. package/src/App.tsx +67 -0
  24. package/src/api.ts +130 -0
  25. package/src/auth.ts +74 -0
  26. package/src/components/AgentLeaderboard.tsx +84 -0
  27. package/src/components/AnalyticsDashboard.tsx +223 -0
  28. package/src/components/Header.tsx +62 -0
  29. package/src/components/IntegrationManager.tsx +292 -0
  30. package/src/components/RoleManager.tsx +230 -0
  31. package/src/components/Sidebar.tsx +62 -0
  32. package/src/components/TrendChart.tsx +92 -0
  33. package/src/components/WorkspaceSelector.tsx +157 -0
  34. package/src/components/__tests__/Header.test.tsx +64 -0
  35. package/src/components/__tests__/Sidebar.test.tsx +39 -0
  36. package/src/components/index.ts +8 -0
  37. package/src/hooks/__tests__/useAuth.test.ts +88 -0
  38. package/src/hooks/__tests__/useWorkspace.test.ts +101 -0
  39. package/src/hooks/index.ts +4 -0
  40. package/src/hooks/useAnalytics.ts +76 -0
  41. package/src/hooks/useApi.ts +62 -0
  42. package/src/hooks/useAuth.ts +69 -0
  43. package/src/hooks/useWorkspace.ts +130 -0
  44. package/src/index.css +57 -0
  45. package/src/main.tsx +13 -0
  46. package/src/pages/DashboardPage.tsx +13 -0
  47. package/src/pages/HomePage.tsx +156 -0
  48. package/src/pages/IntegrationsPage.tsx +9 -0
  49. package/src/pages/RolesPage.tsx +9 -0
  50. package/src/pages/SettingsPage.tsx +118 -0
  51. package/src/pages/WorkspacesPage.tsx +9 -0
  52. package/src/pages/index.ts +6 -0
  53. package/src/test/setup.ts +31 -0
  54. package/src/test/utils.tsx +15 -0
  55. package/src/types.ts +132 -0
  56. package/tailwind.config.js +11 -0
  57. package/tests/e2e/auth.spec.ts +42 -0
  58. package/tests/e2e/dashboard.spec.ts +52 -0
  59. package/tests/e2e/integrations.spec.ts +91 -0
  60. package/tests/e2e/workspaces.spec.ts +78 -0
  61. package/tsconfig.json +26 -0
  62. package/tsconfig.node.json +10 -0
  63. package/vite.config.ts +26 -0
  64. package/vitest.config.ts +31 -0
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "session-wrap-dashboard",
3
+ "version": "3.9.0",
4
+ "type": "module",
5
+ "description": "React 18 dashboard for Session Wrap Backend (Phase 9 - Complete)",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview",
10
+ "lint": "eslint src --ext ts,tsx",
11
+ "type-check": "tsc --noEmit",
12
+ "test": "vitest",
13
+ "test:ui": "vitest --ui",
14
+ "test:coverage": "vitest --coverage",
15
+ "test:e2e": "playwright test",
16
+ "test:e2e:ui": "playwright test --ui",
17
+ "test:e2e:debug": "playwright test --debug"
18
+ },
19
+ "dependencies": {
20
+ "axios": "^1.6.0",
21
+ "lucide-react": "^0.294.0",
22
+ "react": "^18.2.0",
23
+ "react-dom": "^18.2.0",
24
+ "react-router-dom": "^6.20.0",
25
+ "recharts": "^2.10.0",
26
+ "zustand": "^4.4.0"
27
+ },
28
+ "devDependencies": {
29
+ "@playwright/test": "^1.40.0",
30
+ "@testing-library/jest-dom": "^6.9.1",
31
+ "@testing-library/react": "^16.3.2",
32
+ "@types/node": "^20.10.0",
33
+ "@types/react": "^18.2.43",
34
+ "@types/react-dom": "^18.2.17",
35
+ "@vitejs/plugin-react": "^4.2.0",
36
+ "@vitest/coverage-v8": "^4.1.2",
37
+ "@vitest/ui": "^4.1.2",
38
+ "autoprefixer": "^10.4.16",
39
+ "happy-dom": "^20.8.9",
40
+ "postcss": "^8.4.32",
41
+ "tailwindcss": "^3.3.0",
42
+ "typescript": "^5.3.0",
43
+ "vite": "^5.0.0",
44
+ "vitest": "^4.1.2"
45
+ },
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ }
49
+ }
@@ -0,0 +1,38 @@
1
+ import { defineConfig, devices } from '@playwright/test'
2
+
3
+ export default defineConfig({
4
+ testDir: './tests/e2e',
5
+ fullyParallel: true,
6
+ forbidOnly: !!process.env.CI,
7
+ retries: process.env.CI ? 2 : 0,
8
+ workers: process.env.CI ? 1 : undefined,
9
+ reporter: [['html', { outputFolder: 'playwright-report' }]],
10
+ use: {
11
+ baseURL: 'http://localhost:3000',
12
+ trace: 'on-first-retry',
13
+ screenshot: 'only-on-failure',
14
+ video: 'retain-on-failure'
15
+ },
16
+
17
+ projects: [
18
+ {
19
+ name: 'chromium',
20
+ use: { ...devices['Desktop Chrome'] }
21
+ },
22
+ {
23
+ name: 'firefox',
24
+ use: { ...devices['Desktop Firefox'] }
25
+ },
26
+ {
27
+ name: 'webkit',
28
+ use: { ...devices['Desktop Safari'] }
29
+ }
30
+ ],
31
+
32
+ webServer: {
33
+ command: 'npm run dev',
34
+ url: 'http://localhost:3000',
35
+ reuseExistingServer: !process.env.CI,
36
+ timeout: 120000
37
+ }
38
+ })
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,67 @@
1
+ import { Routes, Route, Navigate } from 'react-router-dom'
2
+ import { useAuth } from './hooks'
3
+ import { Sidebar, Header } from './components'
4
+ import {
5
+ HomePage,
6
+ DashboardPage,
7
+ WorkspacesPage,
8
+ RolesPage,
9
+ IntegrationsPage,
10
+ SettingsPage
11
+ } from './pages'
12
+
13
+ // 登入頁面
14
+ const LoginPage = () => (
15
+ <div className="flex items-center justify-center h-screen bg-gradient-to-br from-blue-50 to-indigo-50">
16
+ <div className="text-center">
17
+ <h1 className="text-4xl font-bold text-slate-900 mb-2">🎯 Session Wrap</h1>
18
+ <p className="text-slate-600 mb-8">Connecting to auth service...</p>
19
+ <div className="inline-block w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
20
+ </div>
21
+ </div>
22
+ )
23
+
24
+ interface AppLayoutProps {
25
+ isAuthenticated: boolean
26
+ children: React.ReactNode
27
+ }
28
+
29
+ const AppLayout = ({ isAuthenticated, children }: AppLayoutProps) => {
30
+ if (!isAuthenticated) {
31
+ return <LoginPage />
32
+ }
33
+
34
+ return (
35
+ <div className="flex h-screen bg-slate-50">
36
+ <Sidebar />
37
+ <div className="flex-1 flex flex-col overflow-hidden">
38
+ <Header />
39
+ <main className="flex-1 overflow-auto">
40
+ {children}
41
+ </main>
42
+ </div>
43
+ </div>
44
+ )
45
+ }
46
+
47
+ export default function App() {
48
+ const { isAuthenticated, isLoading } = useAuth()
49
+
50
+ if (isLoading) {
51
+ return <LoginPage />
52
+ }
53
+
54
+ return (
55
+ <AppLayout isAuthenticated={isAuthenticated}>
56
+ <Routes>
57
+ <Route path="/" element={<HomePage />} />
58
+ <Route path="/dashboard" element={<DashboardPage />} />
59
+ <Route path="/workspaces" element={<WorkspacesPage />} />
60
+ <Route path="/roles" element={<RolesPage />} />
61
+ <Route path="/integrations" element={<IntegrationsPage />} />
62
+ <Route path="/settings" element={<SettingsPage />} />
63
+ <Route path="*" element={<Navigate to="/" />} />
64
+ </Routes>
65
+ </AppLayout>
66
+ )
67
+ }
package/src/api.ts ADDED
@@ -0,0 +1,130 @@
1
+ import axios, { AxiosInstance } from 'axios'
2
+ import * as types from './types'
3
+
4
+ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'
5
+
6
+ // 創建 Axios 實例
7
+ const api: AxiosInstance = axios.create({
8
+ baseURL: API_URL,
9
+ headers: {
10
+ 'Content-Type': 'application/json'
11
+ }
12
+ })
13
+
14
+ // 請求攔截器 - 添加認證令牌
15
+ api.interceptors.request.use((config) => {
16
+ const token = localStorage.getItem(import.meta.env.VITE_JWT_STORAGE_KEY || 'auth_token')
17
+ if (token) {
18
+ config.headers.Authorization = `Bearer ${token}`
19
+ }
20
+ return config
21
+ })
22
+
23
+ // 響應攔截器 - 處理錯誤
24
+ api.interceptors.response.use(
25
+ (response) => response,
26
+ (error) => {
27
+ if (error.response?.status === 401) {
28
+ localStorage.removeItem(import.meta.env.VITE_JWT_STORAGE_KEY || 'auth_token')
29
+ window.location.href = '/login'
30
+ }
31
+ return Promise.reject(error)
32
+ }
33
+ )
34
+
35
+ // 認證端點
36
+ export const authAPI = {
37
+ login: (claudeToken: string) =>
38
+ api.post<{ token: string; user: types.User }>('/api/auth/login', { claudeToken }),
39
+ verify: (token: string) =>
40
+ api.post<{ valid: boolean }>('/api/auth/verify', { token })
41
+ }
42
+
43
+ // 用戶端點
44
+ export const userAPI = {
45
+ getProfile: () =>
46
+ api.get<{ user: types.User }>('/api/users/profile'),
47
+ getStorage: () =>
48
+ api.get<any>('/api/users/storage')
49
+ }
50
+
51
+ // 工作區端點
52
+ export const workspaceAPI = {
53
+ list: () =>
54
+ api.get<types.Workspace[]>('/api/workspaces'),
55
+ create: (name: string, isPublic: boolean = false) =>
56
+ api.post<types.Workspace>('/api/workspaces', { name, is_public: isPublic }),
57
+ get: (id: string) =>
58
+ api.get<types.Workspace>(`/api/workspaces/${id}`),
59
+ getMembers: (workspaceId: string) =>
60
+ api.get<types.WorkspaceMember[]>(`/api/workspaces/${workspaceId}/members`),
61
+ addMember: (workspaceId: string, userId: string, roleName: string) =>
62
+ api.post(`/api/workspaces/${workspaceId}/members`, { user_id: userId, role_name: roleName }),
63
+ updateMember: (workspaceId: string, userId: string, roleName: string) =>
64
+ api.put(`/api/workspaces/${workspaceId}/members/${userId}`, { role_name: roleName }),
65
+ removeMember: (workspaceId: string, userId: string) =>
66
+ api.delete(`/api/workspaces/${workspaceId}/members/${userId}`)
67
+ }
68
+
69
+ // RBAC 端點
70
+ export const rbacAPI = {
71
+ getRoles: () =>
72
+ api.get<types.Role[]>('/api/roles'),
73
+ getPermissions: (workspaceId?: string) =>
74
+ api.get(`/api/me/permissions${workspaceId ? `?workspaceId=${workspaceId}` : ''}`),
75
+ checkPermission: (resourceType: string, resourceId: string, permission: string) =>
76
+ api.get(`/api/permissions/check`, {
77
+ params: { resource_type: resourceType, resource_id: resourceId, permission }
78
+ })
79
+ }
80
+
81
+ // 分析端點
82
+ export const analyticsAPI = {
83
+ getDashboard: (workspaceId: string, days: number = 30) =>
84
+ api.get<types.AnalyticsDashboard>(`/api/analytics/dashboard/${workspaceId}`, {
85
+ params: { days }
86
+ }),
87
+ getTrends: (workspaceId: string, days: number = 30) =>
88
+ api.get<any>(`/api/analytics/trends/${workspaceId}`, {
89
+ params: { days }
90
+ }),
91
+ getAgents: (workspaceId: string, days: number = 30) =>
92
+ api.get<{ agents: types.AgentPerformance[] }>(`/api/analytics/agents/${workspaceId}`, {
93
+ params: { days }
94
+ }),
95
+ getDecisions: (workspaceId: string, days: number = 30) =>
96
+ api.get<any>(`/api/analytics/decisions/${workspaceId}`, {
97
+ params: { days }
98
+ }),
99
+ getInsights: (workspaceId: string, days: number = 30) =>
100
+ api.get<{ insights: types.Insight[] }>(`/api/analytics/insights/${workspaceId}`, {
101
+ params: { days }
102
+ })
103
+ }
104
+
105
+ // 集成端點
106
+ export const integrationAPI = {
107
+ list: (workspaceId: string) =>
108
+ api.get<{ integrations: types.Integration[] }>(`/api/integrations/${workspaceId}`),
109
+ setup: (workspaceId: string, serviceName: string, config: any) =>
110
+ api.post(`/api/integrations/${workspaceId}/setup`, { service_name: serviceName, config }),
111
+ toggle: (integrationId: string) =>
112
+ api.put(`/api/integrations/${integrationId}/toggle`),
113
+ delete: (integrationId: string) =>
114
+ api.delete(`/api/integrations/${integrationId}`),
115
+ getEvents: (integrationId: string) =>
116
+ api.get<{ events: types.IntegrationEvent[] }>(`/api/integrations/${integrationId}/events`),
117
+ testSlack: (workspaceId: string) =>
118
+ api.post(`/api/integrations/${workspaceId}/slack/test`),
119
+ testGitHub: (workspaceId: string) =>
120
+ api.post(`/api/integrations/${workspaceId}/github/test`),
121
+ testJira: (workspaceId: string) =>
122
+ api.post(`/api/integrations/${workspaceId}/jira/test`)
123
+ }
124
+
125
+ // 健康檢查
126
+ export const healthAPI = {
127
+ check: () => api.get('/health')
128
+ }
129
+
130
+ export default api
package/src/auth.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { authAPI } from './api'
2
+
3
+ const TOKEN_KEY = import.meta.env.VITE_JWT_STORAGE_KEY || 'auth_token'
4
+
5
+ export interface AuthState {
6
+ token: string | null
7
+ isAuthenticated: boolean
8
+ }
9
+
10
+ /**
11
+ * 使用 Claude Code token 登入
12
+ */
13
+ export const login = async (claudeToken: string) => {
14
+ const response = await authAPI.login(claudeToken)
15
+ const { token } = response.data
16
+
17
+ // 保存 token
18
+ localStorage.setItem(TOKEN_KEY, token)
19
+
20
+ return response.data
21
+ }
22
+
23
+ /**
24
+ * 驗證 token 是否有效
25
+ */
26
+ export const verifyToken = async (token: string): Promise<boolean> => {
27
+ try {
28
+ const response = await authAPI.verify(token)
29
+ return response.data.valid
30
+ } catch {
31
+ return false
32
+ }
33
+ }
34
+
35
+ /**
36
+ * 取得已保存的 token
37
+ */
38
+ export const getAuthToken = (): string | null => {
39
+ return localStorage.getItem(TOKEN_KEY)
40
+ }
41
+
42
+ /**
43
+ * 檢查用戶是否已認證
44
+ */
45
+ export const isAuthenticated = (): boolean => {
46
+ return getAuthToken() !== null
47
+ }
48
+
49
+ /**
50
+ * 登出
51
+ */
52
+ export const logout = () => {
53
+ localStorage.removeItem(TOKEN_KEY)
54
+ }
55
+
56
+ /**
57
+ * 初始化認證狀態
58
+ */
59
+ export const initAuth = async (): Promise<AuthState> => {
60
+ const token = getAuthToken()
61
+
62
+ if (!token) {
63
+ return { token: null, isAuthenticated: false }
64
+ }
65
+
66
+ const isValid = await verifyToken(token)
67
+
68
+ if (!isValid) {
69
+ logout()
70
+ return { token: null, isAuthenticated: false }
71
+ }
72
+
73
+ return { token, isAuthenticated: true }
74
+ }
@@ -0,0 +1,84 @@
1
+ import { memo } from 'react'
2
+ import { AgentPerformance } from '../types'
3
+ import { TrendingUp } from 'lucide-react'
4
+
5
+ interface AgentLeaderboardProps {
6
+ agents: AgentPerformance[]
7
+ }
8
+
9
+ const AgentLeaderboardComponent = ({ agents }: AgentLeaderboardProps) => {
10
+ if (!agents || agents.length === 0) {
11
+ return (
12
+ <div className="flex items-center justify-center h-64">
13
+ <p className="text-slate-500">No agent data available</p>
14
+ </div>
15
+ )
16
+ }
17
+
18
+ // Sort by efficiency score
19
+ const sorted = [...agents].sort(
20
+ (a, b) => (b.efficiency_score || 0) - (a.efficiency_score || 0)
21
+ )
22
+
23
+ const getMedalEmoji = (index: number) => {
24
+ if (index === 0) return '🥇'
25
+ if (index === 1) return '🥈'
26
+ if (index === 2) return '🥉'
27
+ return '·'
28
+ }
29
+
30
+ return (
31
+ <div className="overflow-x-auto">
32
+ <table className="w-full text-sm">
33
+ <thead>
34
+ <tr className="border-b border-slate-200">
35
+ <th className="text-left py-3 px-4 font-semibold text-slate-700">#</th>
36
+ <th className="text-left py-3 px-4 font-semibold text-slate-700">Agent</th>
37
+ <th className="text-center py-3 px-4 font-semibold text-slate-700">Tasks Created</th>
38
+ <th className="text-center py-3 px-4 font-semibold text-slate-700">Completed</th>
39
+ <th className="text-center py-3 px-4 font-semibold text-slate-700">Comments</th>
40
+ <th className="text-center py-3 px-4 font-semibold text-slate-700">Avg Response Time</th>
41
+ <th className="text-center py-3 px-4 font-semibold text-slate-700">Score</th>
42
+ </tr>
43
+ </thead>
44
+ <tbody>
45
+ {sorted.map((agent, index) => (
46
+ <tr
47
+ key={agent.agent_name}
48
+ className="border-b border-slate-100 hover:bg-slate-50 transition-colors"
49
+ >
50
+ <td className="py-3 px-4">
51
+ <span className="text-lg">{getMedalEmoji(index)}</span>
52
+ </td>
53
+ <td className="py-3 px-4 font-medium text-slate-900">
54
+ {agent.agent_name}
55
+ </td>
56
+ <td className="py-3 px-4 text-center text-slate-600">
57
+ {agent.tasks_created}
58
+ </td>
59
+ <td className="py-3 px-4 text-center text-slate-600">
60
+ {agent.tasks_completed}
61
+ </td>
62
+ <td className="py-3 px-4 text-center text-slate-600">
63
+ {agent.comments_added}
64
+ </td>
65
+ <td className="py-3 px-4 text-center text-slate-600">
66
+ {agent.avg_response_time}ms
67
+ </td>
68
+ <td className="py-3 px-4 text-center">
69
+ <div className="flex items-center justify-center gap-1">
70
+ <TrendingUp size={14} className="text-green-600" />
71
+ <span className="font-semibold text-slate-900">
72
+ {(agent.efficiency_score || 0).toFixed(1)}
73
+ </span>
74
+ </div>
75
+ </td>
76
+ </tr>
77
+ ))}
78
+ </tbody>
79
+ </table>
80
+ </div>
81
+ )
82
+ }
83
+
84
+ export const AgentLeaderboard = memo(AgentLeaderboardComponent)
@@ -0,0 +1,223 @@
1
+ import { memo } from 'react'
2
+ import { useAnalytics, useWorkspace } from '../hooks'
3
+ import { TrendChart } from './TrendChart'
4
+ import { AgentLeaderboard } from './AgentLeaderboard'
5
+ import { Insight } from '../types'
6
+ import { AlertCircle, CheckCircle2, Zap } from 'lucide-react'
7
+
8
+ interface KPICardProps {
9
+ title: string
10
+ value: string | number
11
+ icon: React.ReactNode
12
+ trend?: string
13
+ color: 'blue' | 'green' | 'yellow' | 'purple'
14
+ }
15
+
16
+ const KPICardComponent = ({ title, value, icon, trend, color }: KPICardProps) => {
17
+ const colorClasses = {
18
+ blue: 'bg-blue-50 text-blue-600',
19
+ green: 'bg-green-50 text-green-600',
20
+ yellow: 'bg-yellow-50 text-yellow-600',
21
+ purple: 'bg-purple-50 text-purple-600'
22
+ }
23
+
24
+ return (
25
+ <div className={`${colorClasses[color]} rounded-lg p-6 flex items-start gap-4`}>
26
+ <div className="text-3xl">{icon}</div>
27
+ <div className="flex-1">
28
+ <p className="text-sm font-medium text-slate-600">{title}</p>
29
+ <p className="text-2xl font-bold mt-1">{value}</p>
30
+ {trend && <p className="text-xs text-green-600 mt-2">{trend}</p>}
31
+ </div>
32
+ </div>
33
+ )
34
+ }
35
+
36
+ const KPICard = memo(KPICardComponent)
37
+
38
+ const InsightCardComponent = (insight: Insight) => {
39
+ const iconMap = {
40
+ positive: <CheckCircle2 size={18} className="text-green-600" />,
41
+ warning: <AlertCircle size={18} className="text-yellow-600" />,
42
+ info: <Zap size={18} className="text-blue-600" />
43
+ }
44
+
45
+ const bgMap = {
46
+ positive: 'bg-green-50 border-green-200',
47
+ warning: 'bg-yellow-50 border-yellow-200',
48
+ info: 'bg-blue-50 border-blue-200'
49
+ }
50
+
51
+ return (
52
+ <div className={`${bgMap[insight.type]} border rounded-lg p-4`}>
53
+ <div className="flex gap-3">
54
+ <div className="flex-shrink-0">{iconMap[insight.type]}</div>
55
+ <div className="flex-1">
56
+ <p className="font-medium text-slate-900">{insight.message}</p>
57
+ <p className="text-sm text-slate-600 mt-1">{insight.recommendation}</p>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ )
62
+ }
63
+
64
+ const InsightCard = memo(InsightCardComponent)
65
+
66
+ interface AnalyticsDashboardProps {
67
+ workspaceId?: string | null
68
+ }
69
+
70
+ const AnalyticsDashboardComponent = ({ workspaceId }: AnalyticsDashboardProps) => {
71
+ const { currentWorkspace } = useWorkspace()
72
+ const wsId = workspaceId || currentWorkspace?.id
73
+ const { dashboard, insights, agents, isLoading, error, days, setDays } = useAnalytics(wsId || null)
74
+
75
+ if (!wsId) {
76
+ return (
77
+ <div className="flex items-center justify-center h-screen">
78
+ <div className="text-center">
79
+ <p className="text-slate-600">Please select a workspace</p>
80
+ </div>
81
+ </div>
82
+ )
83
+ }
84
+
85
+ if (isLoading) {
86
+ return (
87
+ <div className="flex items-center justify-center h-screen">
88
+ <div className="loading-spinner"></div>
89
+ </div>
90
+ )
91
+ }
92
+
93
+ if (error) {
94
+ return (
95
+ <div className="flex items-center justify-center h-screen">
96
+ <div className="text-center">
97
+ <p className="text-red-600">{error}</p>
98
+ </div>
99
+ </div>
100
+ )
101
+ }
102
+
103
+ if (!dashboard) {
104
+ return (
105
+ <div className="flex items-center justify-center h-screen">
106
+ <div className="text-center">
107
+ <p className="text-slate-600">No analytics data available</p>
108
+ </div>
109
+ </div>
110
+ )
111
+ }
112
+
113
+ const completionRate = dashboard.completion_rate * 100
114
+
115
+ return (
116
+ <div className="p-6 space-y-6">
117
+ {/* Header with Controls */}
118
+ <div className="flex justify-between items-center">
119
+ <h1 className="text-2xl font-bold text-slate-900">Analytics Dashboard</h1>
120
+ <select
121
+ value={days}
122
+ onChange={(e) => setDays(Number(e.target.value))}
123
+ className="input"
124
+ >
125
+ <option value={7}>Last 7 days</option>
126
+ <option value={30}>Last 30 days</option>
127
+ <option value={90}>Last 90 days</option>
128
+ </select>
129
+ </div>
130
+
131
+ {/* KPI Cards */}
132
+ <div className="grid grid-cols-4 gap-4">
133
+ <KPICard
134
+ title="Task Completion"
135
+ value={`${completionRate.toFixed(1)}%`}
136
+ icon="📊"
137
+ color="blue"
138
+ trend="+5% from last period"
139
+ />
140
+ <KPICard
141
+ title="Total Tasks"
142
+ value={dashboard.snapshot.total_tasks}
143
+ icon="✓"
144
+ color="green"
145
+ />
146
+ <KPICard
147
+ title="Avg Decision Quality"
148
+ value={dashboard.snapshot.avg_decision_quality.toFixed(2)}
149
+ icon="⭐"
150
+ color="purple"
151
+ trend="Scale: 1-5"
152
+ />
153
+ <KPICard
154
+ title="Active Agents"
155
+ value={dashboard.snapshot.active_agents}
156
+ icon="🤖"
157
+ color="yellow"
158
+ />
159
+ </div>
160
+
161
+ {/* Trends Chart */}
162
+ {dashboard.trends.length > 0 && (
163
+ <div className="card">
164
+ <h2 className="text-lg font-semibold mb-4">Trends ({days} days)</h2>
165
+ <TrendChart data={dashboard.trends} />
166
+ </div>
167
+ )}
168
+
169
+ {/* Two Column Layout */}
170
+ <div className="grid grid-cols-3 gap-6">
171
+ {/* Agent Leaderboard */}
172
+ {agents.length > 0 && (
173
+ <div className="col-span-2 card">
174
+ <h2 className="text-lg font-semibold mb-4">Top Agents</h2>
175
+ <AgentLeaderboard agents={agents} />
176
+ </div>
177
+ )}
178
+
179
+ {/* Insights */}
180
+ <div className="card space-y-3">
181
+ <h2 className="text-lg font-semibold">Key Insights</h2>
182
+ {insights.length > 0 ? (
183
+ insights.map((insight) => (
184
+ <InsightCard key={insight.message} {...insight} />
185
+ ))
186
+ ) : (
187
+ <p className="text-sm text-slate-500">No insights available</p>
188
+ )}
189
+ </div>
190
+ </div>
191
+
192
+ {/* Task Status Breakdown */}
193
+ <div className="card grid grid-cols-4 gap-4">
194
+ <div>
195
+ <p className="text-sm text-slate-600">Completed</p>
196
+ <p className="text-2xl font-bold text-green-600">
197
+ {dashboard.snapshot.completed_tasks}
198
+ </p>
199
+ </div>
200
+ <div>
201
+ <p className="text-sm text-slate-600">In Progress</p>
202
+ <p className="text-2xl font-bold text-blue-600">
203
+ {dashboard.snapshot.in_progress_tasks}
204
+ </p>
205
+ </div>
206
+ <div>
207
+ <p className="text-sm text-slate-600">Pending</p>
208
+ <p className="text-2xl font-bold text-yellow-600">
209
+ {dashboard.snapshot.pending_tasks}
210
+ </p>
211
+ </div>
212
+ <div>
213
+ <p className="text-sm text-slate-600">Total Decisions</p>
214
+ <p className="text-2xl font-bold text-purple-600">
215
+ {dashboard.snapshot.total_decisions}
216
+ </p>
217
+ </div>
218
+ </div>
219
+ </div>
220
+ )
221
+ }
222
+
223
+ export const AnalyticsDashboard = memo(AnalyticsDashboardComponent)