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.
- package/.env.example +6 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-final.json +6 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +146 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/auth.ts.html +307 -0
- package/coverage/src/components/Header.tsx.html +262 -0
- package/coverage/src/components/Sidebar.tsx.html +262 -0
- package/coverage/src/components/index.html +131 -0
- package/coverage/src/hooks/index.html +131 -0
- package/coverage/src/hooks/useAuth.ts.html +292 -0
- package/coverage/src/hooks/useWorkspace.ts.html +475 -0
- package/coverage/src/index.html +116 -0
- package/index.html +13 -0
- package/package.json +49 -0
- package/playwright.config.ts +38 -0
- package/postcss.config.js +6 -0
- package/src/App.tsx +67 -0
- package/src/api.ts +130 -0
- package/src/auth.ts +74 -0
- package/src/components/AgentLeaderboard.tsx +84 -0
- package/src/components/AnalyticsDashboard.tsx +223 -0
- package/src/components/Header.tsx +62 -0
- package/src/components/IntegrationManager.tsx +292 -0
- package/src/components/RoleManager.tsx +230 -0
- package/src/components/Sidebar.tsx +62 -0
- package/src/components/TrendChart.tsx +92 -0
- package/src/components/WorkspaceSelector.tsx +157 -0
- package/src/components/__tests__/Header.test.tsx +64 -0
- package/src/components/__tests__/Sidebar.test.tsx +39 -0
- package/src/components/index.ts +8 -0
- package/src/hooks/__tests__/useAuth.test.ts +88 -0
- package/src/hooks/__tests__/useWorkspace.test.ts +101 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useAnalytics.ts +76 -0
- package/src/hooks/useApi.ts +62 -0
- package/src/hooks/useAuth.ts +69 -0
- package/src/hooks/useWorkspace.ts +130 -0
- package/src/index.css +57 -0
- package/src/main.tsx +13 -0
- package/src/pages/DashboardPage.tsx +13 -0
- package/src/pages/HomePage.tsx +156 -0
- package/src/pages/IntegrationsPage.tsx +9 -0
- package/src/pages/RolesPage.tsx +9 -0
- package/src/pages/SettingsPage.tsx +118 -0
- package/src/pages/WorkspacesPage.tsx +9 -0
- package/src/pages/index.ts +6 -0
- package/src/test/setup.ts +31 -0
- package/src/test/utils.tsx +15 -0
- package/src/types.ts +132 -0
- package/tailwind.config.js +11 -0
- package/tests/e2e/auth.spec.ts +42 -0
- package/tests/e2e/dashboard.spec.ts +52 -0
- package/tests/e2e/integrations.spec.ts +91 -0
- package/tests/e2e/workspaces.spec.ts +78 -0
- package/tsconfig.json +26 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +26 -0
- 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
|
+
})
|
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)
|