agentlytics 0.1.0 → 0.1.1
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/README.md +3 -3
- package/editors/base.js +24 -0
- package/editors/cursor.js +4 -2
- package/editors/vscode.js +3 -4
- package/editors/zed.js +2 -2
- package/package.json +1 -1
- package/ui/src/components/EditorDot.jsx +3 -5
- package/ui/src/components/EditorIcon.jsx +63 -0
- package/ui/src/components/KpiCard.jsx +3 -3
- package/ui/src/pages/Dashboard.jsx +169 -84
package/README.md
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
<a href="https://www.npmjs.com/package/agentlytics"><img src="https://img.shields.io/npm/v/agentlytics?color=6366f1&label=npm" alt="npm"></a>
|
|
14
14
|
<a href="#supported-editors"><img src="https://img.shields.io/badge/editors-14-818cf8" alt="editors"></a>
|
|
15
15
|
<a href="#license"><img src="https://img.shields.io/badge/license-MIT-green" alt="license"></a>
|
|
16
|
-
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%E2%89%
|
|
16
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%E2%89%A520.19%20%7C%20%E2%89%A522.12-brightgreen" alt="node"></a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
|
-
<img src="
|
|
20
|
+
<img src="misc/screenshot.png" alt="Agentlytics dashboard" width="100%">
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
23
|
---
|
|
@@ -30,7 +30,7 @@ Agentlytics reads local chat history from every major AI coding assistant and pr
|
|
|
30
30
|
npx agentlytics
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Opens at **http://localhost:4637**. Requires Node.js ≥
|
|
33
|
+
Opens at **http://localhost:4637**. Requires Node.js ≥ 20.19 or ≥ 22.12, macOS.
|
|
34
34
|
|
|
35
35
|
To only build the cache database without starting the server:
|
|
36
36
|
|
package/editors/base.js
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const os = require('os');
|
|
1
3
|
const chalk = require('chalk');
|
|
2
4
|
|
|
5
|
+
const HOME = os.homedir();
|
|
6
|
+
|
|
7
|
+
// --- Platform utilities ---
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get platform-specific app data directory path for VS Code-like editors.
|
|
11
|
+
* - macOS: ~/Library/Application Support/{appName}/User/...
|
|
12
|
+
* - Windows: ~/AppData/Roaming/{appName}/User/...
|
|
13
|
+
* - Linux: ~/.config/{appName}/User/...
|
|
14
|
+
*/
|
|
15
|
+
function getAppDataPath(appName) {
|
|
16
|
+
switch (process.platform) {
|
|
17
|
+
case 'darwin':
|
|
18
|
+
return path.join(HOME, 'Library', 'Application Support', appName);
|
|
19
|
+
case 'win32':
|
|
20
|
+
return path.join(HOME, 'AppData', 'Roaming', appName);
|
|
21
|
+
default: // linux, etc.
|
|
22
|
+
return path.join(HOME, '.config', appName);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
3
26
|
// --- Formatting utilities shared across all editor adapters ---
|
|
4
27
|
|
|
5
28
|
function formatArgs(args, maxLen = 300) {
|
|
@@ -117,6 +140,7 @@ function shortenPath(p, maxLen = 40) {
|
|
|
117
140
|
*/
|
|
118
141
|
|
|
119
142
|
module.exports = {
|
|
143
|
+
getAppDataPath,
|
|
120
144
|
formatArgs,
|
|
121
145
|
formatToolCall,
|
|
122
146
|
formatToolResult,
|
package/editors/cursor.js
CHANGED
|
@@ -2,11 +2,13 @@ const Database = require('better-sqlite3');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
|
+
const { getAppDataPath } = require('./base');
|
|
5
6
|
|
|
6
7
|
const HOME = os.homedir();
|
|
7
8
|
const CURSOR_CHATS_DIR = path.join(HOME, '.cursor', 'chats');
|
|
8
|
-
const
|
|
9
|
-
const
|
|
9
|
+
const CURSOR_USER_DIR = path.join(getAppDataPath('Cursor'), 'User');
|
|
10
|
+
const WORKSPACE_STORAGE_DIR = path.join(CURSOR_USER_DIR, 'workspaceStorage');
|
|
11
|
+
const GLOBAL_STORAGE_DB = path.join(CURSOR_USER_DIR, 'globalStorage', 'state.vscdb');
|
|
10
12
|
|
|
11
13
|
// ============================================================
|
|
12
14
|
// Source 1: ~/.cursor/chats/<hash>/<chatId>/store.db (agent KV)
|
package/editors/vscode.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const os = require('os');
|
|
4
|
-
|
|
5
|
-
const HOME = os.homedir();
|
|
4
|
+
const { getAppDataPath } = require('./base');
|
|
6
5
|
|
|
7
6
|
// VS Code variants: stable and insiders
|
|
8
7
|
const VARIANTS = [
|
|
9
8
|
{
|
|
10
9
|
id: 'vscode',
|
|
11
|
-
appSupport:
|
|
10
|
+
appSupport: getAppDataPath('Code'),
|
|
12
11
|
},
|
|
13
12
|
{
|
|
14
13
|
id: 'vscode-insiders',
|
|
15
|
-
appSupport:
|
|
14
|
+
appSupport: getAppDataPath('Code - Insiders'),
|
|
16
15
|
},
|
|
17
16
|
];
|
|
18
17
|
|
package/editors/zed.js
CHANGED
|
@@ -2,9 +2,9 @@ const path = require('path');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { execSync } = require('child_process');
|
|
5
|
+
const { getAppDataPath } = require('./base');
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
const THREADS_DB = path.join(HOME, 'Library', 'Application Support', 'Zed', 'threads', 'threads.db');
|
|
7
|
+
const THREADS_DB = path.join(getAppDataPath('Zed'), 'threads', 'threads.db');
|
|
8
8
|
|
|
9
9
|
// ============================================================
|
|
10
10
|
// Decompress zstd blob via CLI
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlytics",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Comprehensive analytics dashboard for AI coding agents — Cursor, Windsurf, Claude Code, VS Code Copilot, Zed, Antigravity, OpenCode, Command Code",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { editorLabel } from '../lib/constants'
|
|
2
|
+
import EditorIcon from './EditorIcon'
|
|
2
3
|
|
|
3
4
|
export default function EditorDot({ source, showLabel = false, size = 8 }) {
|
|
4
5
|
return (
|
|
5
6
|
<span className="inline-flex items-center gap-1.5">
|
|
6
|
-
<
|
|
7
|
-
className="rounded-full flex-shrink-0"
|
|
8
|
-
style={{ width: size, height: size, background: editorColor(source) }}
|
|
9
|
-
/>
|
|
7
|
+
<EditorIcon source={source} size={size} />
|
|
10
8
|
{showLabel && <span className="text-[10px]" style={{ color: 'var(--c-text)' }}>{editorLabel(source)}</span>}
|
|
11
9
|
</span>
|
|
12
10
|
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { editorColor } from '../lib/constants'
|
|
2
|
+
|
|
3
|
+
const PATHS = {
|
|
4
|
+
cursor: 'M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23',
|
|
5
|
+
windsurf: 'M23.55 5.067c-1.2038-.002-2.1806.973-2.1806 2.1765v4.8676c0 .972-.8035 1.7594-1.7597 1.7594-.568 0-1.1352-.286-1.4718-.7659l-4.9713-7.1003c-.4125-.5896-1.0837-.941-1.8103-.941-1.1334 0-2.1533.9635-2.1533 2.153v4.8957c0 .972-.7969 1.7594-1.7596 1.7594-.57 0-1.1363-.286-1.4728-.7658L.4076 5.1598C.2822 4.9798 0 5.0688 0 5.2882v4.2452c0 .2147.0656.4228.1884.599l5.4748 7.8183c.3234.462.8006.8052 1.3509.9298 1.3771.313 2.6446-.747 2.6446-2.0977v-4.893c0-.972.7875-1.7593 1.7596-1.7593h.003a1.798 1.798 0 0 1 1.4718.7658l4.9723 7.0994c.4135.5905 1.05.941 1.8093.941 1.1587 0 2.1515-.9645 2.1515-2.153v-4.8948c0-.972.7875-1.7594 1.7596-1.7594h.194a.22.22 0 0 0 .2204-.2202v-4.622a.22.22 0 0 0-.2203-.2203Z',
|
|
6
|
+
claude: 'm4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z',
|
|
7
|
+
zed: 'M2.25 1.5a.75.75 0 0 0-.75.75v16.5H0V2.25A2.25 2.25 0 0 1 2.25 0h20.095c1.002 0 1.504 1.212.795 1.92L10.764 14.298h3.486V12.75h1.5v1.922a1.125 1.125 0 0 1-1.125 1.125H9.264l-2.578 2.578h11.689V9h1.5v9.375a1.5 1.5 0 0 1-1.5 1.5H5.185L2.562 22.5H21.75a.75.75 0 0 0 .75-.75V5.25H24v16.5A2.25 2.25 0 0 1 21.75 24H1.655C.653 24 .151 22.788.86 22.08L13.19 9.75H9.75v1.5h-1.5V9.375A1.125 1.125 0 0 1 9.375 8.25h5.314l2.625-2.625H5.625V15h-1.5V5.625a1.5 1.5 0 0 1 1.5-1.5h13.19L21.438 1.5z',
|
|
8
|
+
gemini: 'M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81',
|
|
9
|
+
copilot: 'M23.922 16.997C23.061 18.492 18.063 22.02 12 22.02 5.937 22.02.939 18.492.078 16.997A.641.641 0 0 1 0 16.741v-2.869a.883.883 0 0 1 .053-.22c.372-.935 1.347-2.292 2.605-2.656.167-.429.414-1.055.644-1.517a10.098 10.098 0 0 1-.052-1.086c0-1.331.282-2.499 1.132-3.368.397-.406.89-.717 1.474-.952C7.255 2.937 9.248 1.98 11.978 1.98c2.731 0 4.767.957 6.166 2.093.584.235 1.077.546 1.474.952.85.869 1.132 2.037 1.132 3.368 0 .368-.014.733-.052 1.086.23.462.477 1.088.644 1.517 1.258.364 2.233 1.721 2.605 2.656a.841.841 0 0 1 .053.22v2.869a.641.641 0 0 1-.078.256Zm-11.75-5.992h-.344a4.359 4.359 0 0 1-.355.508c-.77.947-1.918 1.492-3.508 1.492-1.725 0-2.989-.359-3.782-1.259a2.137 2.137 0 0 1-.085-.104L4 11.746v6.585c1.435.779 4.514 2.179 8 2.179 3.486 0 6.565-1.4 8-2.179v-6.585l-.098-.104s-.033.045-.085.104c-.793.9-2.057 1.259-3.782 1.259-1.59 0-2.738-.545-3.508-1.492a4.359 4.359 0 0 1-.355-.508Zm2.328 3.25c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm-5 0c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm3.313-6.185c.136 1.057.403 1.913.878 2.497.442.544 1.134.938 2.344.938 1.573 0 2.292-.337 2.657-.751.384-.435.558-1.15.558-2.361 0-1.14-.243-1.847-.705-2.319-.477-.488-1.319-.862-2.824-1.025-1.487-.161-2.192.138-2.533.529-.269.307-.437.808-.438 1.578v.021c0 .265.021.562.063.893Zm-1.626 0c.042-.331.063-.628.063-.894v-.02c-.001-.77-.169-1.271-.438-1.578-.341-.391-1.046-.69-2.533-.529-1.505.163-2.347.537-2.824 1.025-.462.472-.705 1.179-.705 2.319 0 1.211.175 1.926.558 2.361.365.414 1.084.751 2.657.751 1.21 0 1.902-.394 2.344-.938.475-.584.742-1.44.878-2.497Z',
|
|
10
|
+
// VS Code - official bracket icon
|
|
11
|
+
vscode: 'M17.583.063a1.5 1.5 0 0 0-1.032.392 1.5 1.5 0 0 0-.001 0L7.04 9.708 2.81 6.442a1 1 0 0 0-1.32.098L.178 7.853a1 1 0 0 0 0 1.414l3.612 3.29-3.612 3.29a1 1 0 0 0 0 1.414L1.49 18.574a1 1 0 0 0 1.32.098L7.04 15.408l9.51 9.253a1.5 1.5 0 0 0 1.033.392A1.5 1.5 0 0 0 19.08 23.5V1.5A1.5 1.5 0 0 0 17.583.063M17.08 5.442l-5.5 7.115 5.5 7.115Z',
|
|
12
|
+
antigravity: 'M21.751 22.607c1.34 1.005 3.35.335 1.508-1.508C17.73 15.74 18.904 1 12.037 1 5.17 1 6.342 15.74.815 21.1c-2.01 2.009.167 2.511 1.507 1.506 5.192-3.517 4.857-9.714 9.715-9.714 4.857 0 4.522 6.197 9.714 9.715z',
|
|
13
|
+
command: 'M6,2A4,4 0 0,1 10,6V8H14V6A4,4 0 0,1 18,2A4,4 0 0,1 22,6A4,4 0 0,1 18,10H16V14H18A4,4 0 0,1 22,18A4,4 0 0,1 18,22A4,4 0 0,1 14,18V16H10V18A4,4 0 0,1 6,22A4,4 0 0,1 2,18A4,4 0 0,1 6,14H8V10H6A4,4 0 0,1 2,6A4,4 0 0,1 6,2M16,18A2,2 0 0,0 18,20A2,2 0 0,0 20,18A2,2 0 0,0 18,16H16V18M14,10H10V14H14V10M6,16A2,2 0 0,0 4,18A2,2 0 0,0 6,20A2,2 0 0,0 8,18V16H6M8,6A2,2 0 0,0 6,4A2,2 0 0,0 4,6A2,2 0 0,0 6,8H8V6M18,8A2,2 0 0,0 20,6A2,2 0 0,0 18,4A2,2 0 0,0 16,6V8H18Z',
|
|
14
|
+
// Terminal icon for generic editors
|
|
15
|
+
terminal: 'M4 17.27V19h16v-1.73ZM4 5v1.73l7.07 4.55L4 15.82v1.73l10-6.46Z',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Map editor IDs to icon paths
|
|
19
|
+
const EDITOR_ICONS = {
|
|
20
|
+
'cursor': 'cursor',
|
|
21
|
+
'cursor-agent': 'cursor',
|
|
22
|
+
'windsurf': 'windsurf',
|
|
23
|
+
'windsurf-next': 'windsurf',
|
|
24
|
+
'antigravity': 'antigravity',
|
|
25
|
+
'claude-code': 'claude',
|
|
26
|
+
'claude': 'claude',
|
|
27
|
+
'vscode': 'vscode',
|
|
28
|
+
'vscode-insiders': 'vscode',
|
|
29
|
+
'zed': 'zed',
|
|
30
|
+
'gemini-cli': 'gemini',
|
|
31
|
+
'copilot-cli': 'copilot',
|
|
32
|
+
'opencode': 'terminal',
|
|
33
|
+
'codex': 'terminal',
|
|
34
|
+
'commandcode': 'command',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default function EditorIcon({ source, size = 16, className = '' }) {
|
|
38
|
+
const pathKey = EDITOR_ICONS[source] || 'terminal'
|
|
39
|
+
const d = PATHS[pathKey]
|
|
40
|
+
const color = editorColor(source)
|
|
41
|
+
|
|
42
|
+
if (!d) {
|
|
43
|
+
return (
|
|
44
|
+
<span
|
|
45
|
+
className={`inline-block rounded-full flex-shrink-0 ${className}`}
|
|
46
|
+
style={{ width: size, height: size, background: color }}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<svg
|
|
53
|
+
viewBox="0 0 24 24"
|
|
54
|
+
width={size}
|
|
55
|
+
height={size}
|
|
56
|
+
className={`inline-block flex-shrink-0 ${className}`}
|
|
57
|
+
fill={color}
|
|
58
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
59
|
+
>
|
|
60
|
+
<path d={d} />
|
|
61
|
+
</svg>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export default function KpiCard({ label, value, sub }) {
|
|
1
|
+
export default function KpiCard({ label, value, sub, onClick }) {
|
|
2
2
|
return (
|
|
3
|
-
<div className=
|
|
4
|
-
<div className="text-base font-bold" style={{ color: 'var(--c-white)' }}>{value}</div>
|
|
3
|
+
<div className={`card px-3 py-2${onClick ? ' cursor-pointer hover:opacity-80 transition' : ''}`} onClick={onClick}>
|
|
4
|
+
<div className="text-base font-bold" style={{ color: onClick ? 'var(--c-accent)' : 'var(--c-white)' }}>{value}</div>
|
|
5
5
|
<div className="text-[10px]" style={{ color: 'var(--c-text2)' }}>{label}</div>
|
|
6
6
|
{sub && <div className="text-[9px] mt-0.5" style={{ color: 'var(--c-text3)' }}>{sub}</div>}
|
|
7
7
|
</div>
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
2
|
import { useNavigate } from 'react-router-dom'
|
|
3
|
-
import { ArrowRight, X, Flame, Zap, MessageSquare, Wrench, Share2 } from 'lucide-react'
|
|
3
|
+
import { ArrowRight, X, Flame, Zap, MessageSquare, Wrench, Share2, AlertTriangle } from 'lucide-react'
|
|
4
4
|
import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, PointElement, LineElement, Filler } from 'chart.js'
|
|
5
5
|
import { Doughnut, Bar, Line } from 'react-chartjs-2'
|
|
6
6
|
import KpiCard from '../components/KpiCard'
|
|
7
7
|
import ActivityHeatmap from '../components/ActivityHeatmap'
|
|
8
8
|
import DateRangePicker from '../components/DateRangePicker'
|
|
9
9
|
import { editorColor, editorLabel, formatNumber, dateRangeToApiParams } from '../lib/constants'
|
|
10
|
-
import
|
|
10
|
+
import EditorIcon from '../components/EditorIcon'
|
|
11
|
+
import { fetchDailyActivity, fetchOverview as fetchOverviewApi, fetchDashboardStats, fetchShareImage, fetchChats } from '../lib/api'
|
|
11
12
|
import { useTheme } from '../lib/theme'
|
|
12
13
|
import SectionTitle from '../components/SectionTitle'
|
|
13
14
|
|
|
@@ -30,6 +31,7 @@ export default function Dashboard({ overview }) {
|
|
|
30
31
|
const [dateRange, setDateRange] = useState(null)
|
|
31
32
|
const { dark } = useTheme()
|
|
32
33
|
const [sharing, setSharing] = useState(false)
|
|
34
|
+
const [largeContextChats, setLargeContextChats] = useState(null)
|
|
33
35
|
const txtColor = dark ? '#888' : '#555'
|
|
34
36
|
const txtDim = dark ? '#555' : '#999'
|
|
35
37
|
const gridColor = dark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.06)'
|
|
@@ -50,6 +52,14 @@ export default function Dashboard({ overview }) {
|
|
|
50
52
|
|
|
51
53
|
useEffect(() => {
|
|
52
54
|
const dateParams = dateRangeToApiParams(dateRange)
|
|
55
|
+
const chatParams = { limit: 500, named: false, ...dateParams }
|
|
56
|
+
if (selectedEditor) chatParams.editor = selectedEditor
|
|
57
|
+
|
|
58
|
+
fetchChats(chatParams).then(r => {
|
|
59
|
+
const big = (r.chats || []).filter(c => c.bubbleCount >= 100).sort((a, b) => b.bubbleCount - a.bubbleCount)
|
|
60
|
+
setLargeContextChats(big)
|
|
61
|
+
})
|
|
62
|
+
|
|
53
63
|
if (!selectedEditor) {
|
|
54
64
|
setFilteredData(null)
|
|
55
65
|
fetchDailyActivity(dateParams).then(setDailyData)
|
|
@@ -218,76 +228,124 @@ export default function Dashboard({ overview }) {
|
|
|
218
228
|
</button>
|
|
219
229
|
</div>
|
|
220
230
|
|
|
221
|
-
{/* Editor breakdown -
|
|
231
|
+
{/* Editor breakdown - compact row */}
|
|
222
232
|
<div className="card p-3">
|
|
223
|
-
<
|
|
224
|
-
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-2">
|
|
233
|
+
<div className="flex items-center flex-wrap gap-1.5">
|
|
225
234
|
{allEditors.map(e => {
|
|
226
235
|
const isSelected = selectedEditor === e.id
|
|
227
236
|
return (
|
|
228
|
-
<
|
|
237
|
+
<button
|
|
229
238
|
key={e.id}
|
|
230
|
-
className="
|
|
239
|
+
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-[11px] cursor-pointer transition rounded-sm"
|
|
231
240
|
style={{
|
|
232
241
|
border: isSelected ? `1.5px solid ${editorColor(e.id)}` : '1px solid var(--c-border)',
|
|
242
|
+
background: isSelected ? editorColor(e.id) + '15' : 'transparent',
|
|
233
243
|
opacity: selectedEditor && !isSelected ? 0.4 : 1,
|
|
244
|
+
color: 'var(--c-text)',
|
|
234
245
|
}}
|
|
235
246
|
onClick={() => setSelectedEditor(isSelected ? null : e.id)}
|
|
236
247
|
>
|
|
237
|
-
<
|
|
238
|
-
<
|
|
239
|
-
<
|
|
240
|
-
</
|
|
248
|
+
<EditorIcon source={e.id} size={14} />
|
|
249
|
+
<span style={{ color: 'var(--c-text2)' }}>{editorLabel(e.id)}</span>
|
|
250
|
+
<span className="font-bold" style={{ color: 'var(--c-white)' }}>{e.count}</span>
|
|
251
|
+
</button>
|
|
241
252
|
)
|
|
242
253
|
})}
|
|
243
254
|
</div>
|
|
244
255
|
{selectedEditor && sel && (
|
|
245
|
-
<div className="mt-
|
|
256
|
+
<div className="mt-2 flex items-center gap-2">
|
|
246
257
|
<button onClick={() => navigate(`/sessions?editor=${selectedEditor}`)} className="flex items-center gap-1 text-[11px] px-2.5 py-1 transition" style={{ color: 'var(--c-accent)', border: '1px solid var(--c-border)' }}>
|
|
247
258
|
Show Sessions <ArrowRight size={11} />
|
|
248
259
|
</button>
|
|
249
260
|
<button onClick={() => setSelectedEditor(null)} className="flex items-center gap-1 text-[11px] px-2.5 py-1 transition" style={{ color: 'var(--c-text2)', border: '1px solid var(--c-border)' }}>
|
|
250
261
|
<X size={9} /> Clear
|
|
251
262
|
</button>
|
|
252
|
-
<span className="text-[11px] ml-auto" style={{ color: 'var(--c-text)' }}>
|
|
253
|
-
<span className="font-bold" style={{ color: editorColor(selectedEditor) }}>{editorLabel(selectedEditor)}</span>
|
|
254
|
-
<span style={{ color: 'var(--c-text2)' }}> — {sel.count} sessions</span>
|
|
255
|
-
</span>
|
|
256
263
|
</div>
|
|
257
264
|
)}
|
|
258
265
|
</div>
|
|
259
266
|
|
|
260
267
|
|
|
261
|
-
{/* KPIs
|
|
262
|
-
<div className="grid
|
|
263
|
-
<KpiCard label="
|
|
264
|
-
<KpiCard label="projects" value={d.topProjects.length} sub=
|
|
265
|
-
<KpiCard label="
|
|
266
|
-
<KpiCard label="this month" value={thisMonth ? thisMonth.count : 0} sub={thisMonth ? thisMonth.month : ''} />
|
|
268
|
+
{/* KPIs — compact single row */}
|
|
269
|
+
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
|
|
270
|
+
<KpiCard label="sessions" value={formatNumber(d.totalChats)} sub={sel ? editorLabel(sel.id) : `${allEditors.length} editors`} onClick={() => navigate(selectedEditor ? `/sessions?editor=${selectedEditor}` : '/sessions')} />
|
|
271
|
+
<KpiCard label="projects" value={d.topProjects.length} sub={`${daysSpan}d span`} onClick={() => navigate('/projects')} />
|
|
272
|
+
<KpiCard label="this month" value={thisMonth ? thisMonth.count : 0} sub={thisMonth ? thisMonth.month : ''} onClick={() => navigate('/sessions')} />
|
|
267
273
|
{stats && <>
|
|
268
|
-
<KpiCard label="
|
|
269
|
-
<KpiCard label="
|
|
270
|
-
|
|
271
|
-
|
|
274
|
+
<KpiCard label="avg depth" value={avgMsgsPerSession} sub={<span className="flex items-center gap-0.5"><MessageSquare size={8} /> msgs/session</span>} />
|
|
275
|
+
<KpiCard label="tool calls" value={formatNumber(stats.totalToolCalls)} sub={<span className="flex items-center gap-0.5"><Wrench size={8} /> total</span>} />
|
|
276
|
+
</>}
|
|
277
|
+
{tk && tk.input > 0 && <>
|
|
278
|
+
<KpiCard label="tokens in" value={formatNumber(tk.input)} sub="prompt" />
|
|
279
|
+
<KpiCard label="tokens out" value={formatNumber(tk.output)} sub={`${outputInputRatio}× ratio`} />
|
|
280
|
+
<KpiCard label="cache hit" value={`${cacheHitRate}%`} sub={formatNumber(tk.cacheRead)} />
|
|
281
|
+
<KpiCard label="you wrote" value={formatNumber(tk.userChars)} sub={`AI: ${formatNumber(tk.assistantChars)}`} />
|
|
272
282
|
</>}
|
|
273
283
|
</div>
|
|
274
284
|
|
|
275
|
-
{/*
|
|
276
|
-
{tk && tk.input > 0 && (
|
|
277
|
-
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-2">
|
|
278
|
-
<KpiCard label="input tokens" value={formatNumber(tk.input)} sub="total prompt" />
|
|
279
|
-
<KpiCard label="output tokens" value={formatNumber(tk.output)} sub="total completion" />
|
|
280
|
-
<KpiCard label="cache read" value={formatNumber(tk.cacheRead)} sub={`${cacheHitRate}% hit rate`} />
|
|
281
|
-
<KpiCard label="cache write" value={formatNumber(tk.cacheWrite)} />
|
|
282
|
-
<KpiCard label="output/input" value={`${outputInputRatio}×`} sub={<span className="flex items-center gap-0.5"><Zap size={8} /> efficiency ratio</span>} />
|
|
283
|
-
<KpiCard label="you wrote" value={formatNumber(tk.userChars)} sub={`AI wrote ${formatNumber(tk.assistantChars)}`} />
|
|
284
|
-
</div>
|
|
285
|
-
)}
|
|
286
|
-
|
|
287
|
-
{/* Activity Heatmap */}
|
|
285
|
+
{/* Activity Heatmap | Col 1 | Col 2 | Col 3 */}
|
|
288
286
|
<div className="card p-3">
|
|
289
287
|
<SectionTitle>agentic coding activity</SectionTitle>
|
|
290
|
-
|
|
288
|
+
<div className="flex gap-4">
|
|
289
|
+
<div className="min-w-0 flex-shrink-0">
|
|
290
|
+
{dailyData ? <ActivityHeatmap dailyData={dailyData} /> : <div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>loading...</div>}
|
|
291
|
+
</div>
|
|
292
|
+
{stats && dailyData && (() => {
|
|
293
|
+
const activeDays = dailyData.filter(d => d.total > 0)
|
|
294
|
+
const busiest = activeDays.length > 0 ? activeDays.reduce((a, b) => a.total > b.total ? a : b) : null
|
|
295
|
+
const totalSessions = activeDays.reduce((s, d) => s + d.total, 0)
|
|
296
|
+
const avgPerDay = activeDays.length > 0 ? (totalSessions / activeDays.length).toFixed(1) : 0
|
|
297
|
+
return (
|
|
298
|
+
<div className="flex-1 grid grid-cols-3 gap-3 text-[10px] min-w-0" style={{ borderLeft: '1px solid var(--c-border)', paddingLeft: 16 }}>
|
|
299
|
+
<div className="space-y-2 min-w-0">
|
|
300
|
+
<div>
|
|
301
|
+
<div style={{ color: 'var(--c-text3)' }} className="uppercase tracking-wider mb-1">streaks</div>
|
|
302
|
+
<div className="flex items-center gap-1">
|
|
303
|
+
<Flame size={10} className="text-orange-400 flex-shrink-0" />
|
|
304
|
+
<span style={{ color: 'var(--c-white)' }} className="font-bold">{stats.streaks.current}d</span>
|
|
305
|
+
<span style={{ color: 'var(--c-text3)' }}>now</span>
|
|
306
|
+
</div>
|
|
307
|
+
<div className="flex items-center gap-1 mt-0.5">
|
|
308
|
+
<Zap size={10} className="text-yellow-400 flex-shrink-0" />
|
|
309
|
+
<span style={{ color: 'var(--c-white)' }} className="font-bold">{stats.streaks.longest}d</span>
|
|
310
|
+
<span style={{ color: 'var(--c-text3)' }}>best</span>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
<div>
|
|
314
|
+
<div style={{ color: 'var(--c-text3)' }} className="uppercase tracking-wider mb-1">active days</div>
|
|
315
|
+
<span style={{ color: 'var(--c-white)' }} className="font-bold">{stats.streaks.totalDays}</span>
|
|
316
|
+
<span className="ml-1" style={{ color: 'var(--c-text3)' }}>{avgPerDay}/day</span>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
<div className="space-y-2 min-w-0">
|
|
320
|
+
{busiest && (
|
|
321
|
+
<div>
|
|
322
|
+
<div style={{ color: 'var(--c-text3)' }} className="uppercase tracking-wider mb-1">busiest day</div>
|
|
323
|
+
<div style={{ color: 'var(--c-white)' }} className="font-bold">{busiest.day}</div>
|
|
324
|
+
<div style={{ color: 'var(--c-text3)' }}>{busiest.total} sessions</div>
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
<div>
|
|
328
|
+
<div style={{ color: 'var(--c-text3)' }} className="uppercase tracking-wider mb-1">peak hour</div>
|
|
329
|
+
<div style={{ color: 'var(--c-white)' }} className="font-bold">{String(stats.hourly.indexOf(Math.max(...stats.hourly))).padStart(2, '0')}:00</div>
|
|
330
|
+
<div style={{ color: 'var(--c-text3)' }}>{Math.max(...stats.hourly)} sessions</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
<div className="min-w-0">
|
|
334
|
+
<div style={{ color: 'var(--c-text3)' }} className="uppercase tracking-wider mb-1">top modes</div>
|
|
335
|
+
<div className="space-y-1">
|
|
336
|
+
{modes.slice(0, 5).map(([mode, count]) => (
|
|
337
|
+
<div key={mode} className="flex items-center gap-1">
|
|
338
|
+
<div className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ background: MODE_COLORS[mode] || '#6b7280' }} />
|
|
339
|
+
<span className="truncate" style={{ color: 'var(--c-text)' }}>{mode}</span>
|
|
340
|
+
<span className="ml-auto font-bold flex-shrink-0" style={{ color: 'var(--c-white)' }}>{formatNumber(count)}</span>
|
|
341
|
+
</div>
|
|
342
|
+
))}
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
)
|
|
347
|
+
})()}
|
|
348
|
+
</div>
|
|
291
349
|
</div>
|
|
292
350
|
|
|
293
351
|
{/* Monthly trend (stacked bar by editor) */}
|
|
@@ -405,56 +463,83 @@ export default function Dashboard({ overview }) {
|
|
|
405
463
|
</div>
|
|
406
464
|
</div>
|
|
407
465
|
|
|
408
|
-
{/* Bottom row: Top models + Top tools */}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
<div className="
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
<div
|
|
420
|
-
<
|
|
421
|
-
|
|
422
|
-
<div className="h-full rounded-sm flex items-center px-1.5" style={{ width: `${(m.count / maxM * 100).toFixed(1)}%`, background: i === 0 ? '#6366f1' : i === 1 ? '#818cf8' : '#a5b4fc40' }}>
|
|
423
|
-
<span className="text-[8px] truncate" style={{ color: i < 2 ? '#fff' : 'var(--c-text2)' }}>{m.name}</span>
|
|
424
|
-
</div>
|
|
466
|
+
{/* Bottom row: Top models + Top tools + Large context */}
|
|
467
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-2">
|
|
468
|
+
{stats && stats.topModels.length > 0 && (
|
|
469
|
+
<div className="card p-3">
|
|
470
|
+
<SectionTitle>top models</SectionTitle>
|
|
471
|
+
<div className="space-y-1">
|
|
472
|
+
{stats.topModels.map((m, i) => {
|
|
473
|
+
const maxM = stats.topModels[0].count
|
|
474
|
+
return (
|
|
475
|
+
<div key={m.name} className="flex items-center gap-2">
|
|
476
|
+
<span className="text-[9px] w-3 text-right" style={{ color: 'var(--c-text3)' }}>{i + 1}</span>
|
|
477
|
+
<div className="flex-1 h-4 rounded-sm overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
|
|
478
|
+
<div className="h-full rounded-sm flex items-center px-1.5" style={{ width: `${(m.count / maxM * 100).toFixed(1)}%`, background: i === 0 ? '#6366f1' : i === 1 ? '#818cf8' : '#a5b4fc40' }}>
|
|
479
|
+
<span className="text-[8px] truncate" style={{ color: i < 2 ? '#fff' : 'var(--c-text2)' }}>{m.name}</span>
|
|
425
480
|
</div>
|
|
426
|
-
<span className="text-[9px] w-8 text-right" style={{ color: 'var(--c-text3)' }}>{m.count}</span>
|
|
427
481
|
</div>
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
482
|
+
<span className="text-[9px] w-8 text-right" style={{ color: 'var(--c-text3)' }}>{m.count}</span>
|
|
483
|
+
</div>
|
|
484
|
+
)
|
|
485
|
+
})}
|
|
431
486
|
</div>
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
</div>
|
|
487
|
+
</div>
|
|
488
|
+
)}
|
|
489
|
+
|
|
490
|
+
{stats && stats.topTools.length > 0 && (
|
|
491
|
+
<div className="card p-3">
|
|
492
|
+
<SectionTitle>top tools <span style={{ color: 'var(--c-text3)' }}>({formatNumber(stats.totalToolCalls)} total)</span></SectionTitle>
|
|
493
|
+
<div className="space-y-1">
|
|
494
|
+
{stats.topTools.map((t, i) => {
|
|
495
|
+
const maxT = stats.topTools[0].count
|
|
496
|
+
return (
|
|
497
|
+
<div key={t.name} className="flex items-center gap-2">
|
|
498
|
+
<span className="text-[9px] w-3 text-right" style={{ color: 'var(--c-text3)' }}>{i + 1}</span>
|
|
499
|
+
<div className="flex-1 h-4 rounded-sm overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
|
|
500
|
+
<div className="h-full rounded-sm flex items-center px-1.5" style={{ width: `${(t.count / maxT * 100).toFixed(1)}%`, background: i === 0 ? '#10b981' : i === 1 ? '#34d399' : '#6ee7b740' }}>
|
|
501
|
+
<span className="text-[8px] truncate font-mono" style={{ color: i < 2 ? '#fff' : 'var(--c-text2)' }}>{t.name}</span>
|
|
448
502
|
</div>
|
|
449
|
-
<span className="text-[9px] w-8 text-right" style={{ color: 'var(--c-text3)' }}>{formatNumber(t.count)}</span>
|
|
450
503
|
</div>
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
504
|
+
<span className="text-[9px] w-8 text-right" style={{ color: 'var(--c-text3)' }}>{formatNumber(t.count)}</span>
|
|
505
|
+
</div>
|
|
506
|
+
)
|
|
507
|
+
})}
|
|
454
508
|
</div>
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
509
|
+
</div>
|
|
510
|
+
)}
|
|
511
|
+
|
|
512
|
+
{largeContextChats && largeContextChats.length > 0 && (
|
|
513
|
+
<div className="card p-3">
|
|
514
|
+
<SectionTitle>
|
|
515
|
+
<span className="inline-flex items-center gap-1">
|
|
516
|
+
<AlertTriangle size={10} className="text-amber-400" />
|
|
517
|
+
large context
|
|
518
|
+
<span style={{ color: 'var(--c-text3)' }}>({largeContextChats.length})</span>
|
|
519
|
+
</span>
|
|
520
|
+
</SectionTitle>
|
|
521
|
+
<div className="space-y-1">
|
|
522
|
+
{largeContextChats.slice(0, 10).map(c => (
|
|
523
|
+
<div
|
|
524
|
+
key={c.id}
|
|
525
|
+
className="flex items-center gap-1.5 px-1.5 py-1 rounded-sm cursor-pointer transition hover:opacity-80"
|
|
526
|
+
style={{ background: c.bubbleCount >= 500 ? 'rgba(239,68,68,0.06)' : 'rgba(245,158,11,0.06)' }}
|
|
527
|
+
onClick={() => navigate(`/sessions/${c.id}`)}
|
|
528
|
+
>
|
|
529
|
+
<EditorIcon source={c.source} size={10} />
|
|
530
|
+
<span className="text-[9px] truncate flex-1" style={{ color: 'var(--c-text)' }}>{c.name || 'Untitled'}</span>
|
|
531
|
+
<span
|
|
532
|
+
className="text-[9px] font-bold flex-shrink-0"
|
|
533
|
+
style={{ color: c.bubbleCount >= 500 ? '#ef4444' : '#f59e0b' }}
|
|
534
|
+
>
|
|
535
|
+
{c.bubbleCount}
|
|
536
|
+
</span>
|
|
537
|
+
</div>
|
|
538
|
+
))}
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
541
|
+
)}
|
|
542
|
+
</div>
|
|
458
543
|
</div>
|
|
459
544
|
)
|
|
460
545
|
}
|