agentlytics 0.2.3 → 0.2.5
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/editors/antigravity.js +10 -1
- package/editors/base.js +185 -0
- package/editors/claude.js +11 -1
- package/editors/codex.js +6 -0
- package/editors/commandcode.js +6 -1
- package/editors/copilot.js +6 -1
- package/editors/cursor-agent.js +6 -1
- package/editors/cursor.js +9 -1
- package/editors/gemini.js +10 -1
- package/editors/goose.js +50 -1
- package/editors/index.js +52 -1
- package/editors/kiro.js +10 -1
- package/editors/opencode.js +32 -1
- package/editors/vscode.js +13 -1
- package/editors/windsurf.js +14 -1
- package/editors/zed.js +31 -1
- package/index.js +43 -33
- package/package.json +1 -1
- package/server.js +288 -0
- package/ui/src/App.jsx +3 -0
- package/ui/src/components/PageHeader.jsx +11 -0
- package/ui/src/lib/api.js +7 -0
- package/ui/src/pages/Artifacts.jsx +16 -29
- package/ui/src/pages/Compare.jsx +4 -3
- package/ui/src/pages/CostAnalysis.jsx +6 -9
- package/ui/src/pages/Dashboard.jsx +15 -12
- package/ui/src/pages/DeepAnalysis.jsx +6 -5
- package/ui/src/pages/MCPs.jsx +744 -0
- package/ui/src/pages/ProjectDetail.jsx +1 -1
- package/ui/src/pages/Projects.jsx +9 -6
- package/ui/src/pages/RelayDashboard.jsx +4 -1
- package/ui/src/pages/RelayUserDetail.jsx +1 -1
- package/ui/src/pages/Sessions.jsx +19 -19
- package/ui/src/pages/Settings.jsx +3 -5
- package/ui/src/pages/SqlViewer.jsx +15 -16
- package/ui/src/pages/Subscriptions.jsx +4 -7
package/server.js
CHANGED
|
@@ -431,6 +431,294 @@ app.get('/api/artifact-content', (req, res) => {
|
|
|
431
431
|
}
|
|
432
432
|
});
|
|
433
433
|
|
|
434
|
+
// ============================================================
|
|
435
|
+
// MCPs — collect MCP servers from all editors + match tool calls
|
|
436
|
+
// ============================================================
|
|
437
|
+
|
|
438
|
+
// Cache: MCP server tool lists are queried once at startup via initMcpToolsCache()
|
|
439
|
+
let _mcpToolsCache = null; // { servers, serverToolResults, toolToServer, serverToolPatterns }
|
|
440
|
+
|
|
441
|
+
async function initMcpToolsCache() {
|
|
442
|
+
const { getAllMCPServers } = require('./editors');
|
|
443
|
+
const { queryMcpServerTools } = require('./editors/base');
|
|
444
|
+
const projects = cache.getCachedProjects({ hiddenFolders: getHiddenFolders() });
|
|
445
|
+
const projectFolders = projects.map(p => p.folder).filter(Boolean);
|
|
446
|
+
|
|
447
|
+
const servers = getAllMCPServers(projectFolders);
|
|
448
|
+
|
|
449
|
+
const queryPromises = servers.map(async (server) => {
|
|
450
|
+
if (server.disabled) return { server, tools: [] };
|
|
451
|
+
try {
|
|
452
|
+
const tools = await queryMcpServerTools(server);
|
|
453
|
+
return { server, tools };
|
|
454
|
+
} catch {
|
|
455
|
+
return { server, tools: [] };
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const serverToolResults = await Promise.all(queryPromises);
|
|
460
|
+
|
|
461
|
+
const toolToServer = {};
|
|
462
|
+
for (const { server, tools } of serverToolResults) {
|
|
463
|
+
server.tools = tools;
|
|
464
|
+
for (const toolName of tools) {
|
|
465
|
+
toolToServer[toolName] = server.name;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const serverToolPatterns = {};
|
|
470
|
+
for (const { server, tools } of serverToolResults) {
|
|
471
|
+
if (tools.length === 0) continue;
|
|
472
|
+
serverToolPatterns[server.name] = new Set(tools.map(t => t.toLowerCase()));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
_mcpToolsCache = { servers, serverToolResults, toolToServer, serverToolPatterns };
|
|
476
|
+
return _mcpToolsCache;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
app.initMcpToolsCache = initMcpToolsCache;
|
|
480
|
+
|
|
481
|
+
app.get('/api/mcps', async (req, res) => {
|
|
482
|
+
try {
|
|
483
|
+
const db = cache.getDb();
|
|
484
|
+
|
|
485
|
+
// Use cached MCP tool data (queried once at startup)
|
|
486
|
+
if (!_mcpToolsCache) await initMcpToolsCache();
|
|
487
|
+
const { servers, serverToolResults, toolToServer, serverToolPatterns } = _mcpToolsCache;
|
|
488
|
+
|
|
489
|
+
// 3. Get tool call stats from the SQLite cache
|
|
490
|
+
const toolRows = db.prepare(`
|
|
491
|
+
SELECT tc.tool_name, tc.source, tc.chat_id, tc.folder, tc.timestamp, c.name as chat_name
|
|
492
|
+
FROM tool_calls tc JOIN chats c ON tc.chat_id = c.id
|
|
493
|
+
ORDER BY tc.timestamp DESC
|
|
494
|
+
`).all();
|
|
495
|
+
|
|
496
|
+
const toolCallMap = {}; // toolName -> { count, editors: Set, sessions: Set, folders: Set }
|
|
497
|
+
const sessionMap = {}; // chatId -> { ... }
|
|
498
|
+
|
|
499
|
+
for (const row of toolRows) {
|
|
500
|
+
const name = row.tool_name;
|
|
501
|
+
if (!toolCallMap[name]) toolCallMap[name] = { count: 0, editors: new Set(), sessions: new Set(), folders: new Set() };
|
|
502
|
+
toolCallMap[name].count++;
|
|
503
|
+
toolCallMap[name].editors.add(row.source);
|
|
504
|
+
toolCallMap[name].sessions.add(row.chat_id);
|
|
505
|
+
if (row.folder) toolCallMap[name].folders.add(row.folder);
|
|
506
|
+
|
|
507
|
+
if (!sessionMap[row.chat_id]) {
|
|
508
|
+
sessionMap[row.chat_id] = {
|
|
509
|
+
composerId: row.chat_id,
|
|
510
|
+
source: row.source,
|
|
511
|
+
name: row.chat_name,
|
|
512
|
+
folder: row.folder,
|
|
513
|
+
createdAt: row.timestamp,
|
|
514
|
+
totalToolCalls: 0,
|
|
515
|
+
tools: {},
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
sessionMap[row.chat_id].totalToolCalls++;
|
|
519
|
+
sessionMap[row.chat_id].tools[name] = (sessionMap[row.chat_id].tools[name] || 0) + 1;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// 4. Build tool call summary
|
|
523
|
+
const toolCalls = Object.entries(toolCallMap)
|
|
524
|
+
.map(([name, data]) => ({
|
|
525
|
+
name,
|
|
526
|
+
count: data.count,
|
|
527
|
+
editors: [...data.editors],
|
|
528
|
+
sessionCount: data.sessions.size,
|
|
529
|
+
folders: [...data.folders],
|
|
530
|
+
}))
|
|
531
|
+
.sort((a, b) => b.count - a.count);
|
|
532
|
+
|
|
533
|
+
// 5. Match tool calls to MCP servers using actual queried tool names.
|
|
534
|
+
// Editors prefix MCP tool names in various ways:
|
|
535
|
+
// - Windsurf: mcp{N}_{toolName} (e.g. mcp1_query-docs)
|
|
536
|
+
// - Cursor: mcp_{ServerName}_{toolName} (e.g. mcp_Figma_get_figma_data)
|
|
537
|
+
// - VS Code: mcp_{sanitizedId}_{toolName} (e.g. mcp_io_github_byt_execute_sql)
|
|
538
|
+
// - Others: {server}_{sep}_{toolName} (e.g. prompts_chat__search_prompts)
|
|
539
|
+
//
|
|
540
|
+
// IMPORTANT: Only match tool calls that have an explicit MCP prefix.
|
|
541
|
+
// Tool calls without a prefix (e.g. "read_file", "edit_file") are built-in
|
|
542
|
+
// editor tools even if an MCP server happens to expose a tool with the same name.
|
|
543
|
+
const matchedTools = {};
|
|
544
|
+
|
|
545
|
+
for (const tc of toolCalls) {
|
|
546
|
+
const tcName = tc.name;
|
|
547
|
+
let serverName = null;
|
|
548
|
+
|
|
549
|
+
// Pattern 1: Windsurf — mcp{N}_{toolName}
|
|
550
|
+
const windsurfMatch = tcName.match(/^mcp(\d+)_(.+)$/);
|
|
551
|
+
if (windsurfMatch) {
|
|
552
|
+
const stripped = windsurfMatch[2];
|
|
553
|
+
serverName = toolToServer[stripped];
|
|
554
|
+
if (!serverName) {
|
|
555
|
+
// Fallback: search all server tool sets
|
|
556
|
+
for (const [sn, toolSet] of Object.entries(serverToolPatterns)) {
|
|
557
|
+
if (toolSet.has(stripped.toLowerCase())) { serverName = sn; break; }
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Pattern 2: Cursor — mcp_{ServerName}_{toolName}
|
|
563
|
+
if (!serverName) {
|
|
564
|
+
const cursorMatch = tcName.match(/^mcp_([^_]+)_(.+)$/);
|
|
565
|
+
if (cursorMatch) {
|
|
566
|
+
const sName = cursorMatch[1];
|
|
567
|
+
const tName = cursorMatch[2];
|
|
568
|
+
for (const [sn, toolSet] of Object.entries(serverToolPatterns)) {
|
|
569
|
+
if (sn.toLowerCase() === sName.toLowerCase() && toolSet.has(tName.toLowerCase())) {
|
|
570
|
+
serverName = sn; break;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// Even if we can't verify the tool, the prefix confirms it's an MCP call
|
|
574
|
+
if (!serverName) {
|
|
575
|
+
for (const s of servers) {
|
|
576
|
+
if (s.name.toLowerCase() === sName.toLowerCase()) { serverName = s.name; break; }
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Pattern 3: VS Code / generic — mcp_{sanitizedServerId}_{toolName}
|
|
583
|
+
// Server IDs like "io.github.f/prompts.chat-mcp" become "io_github_f_prompts_chat_mcp"
|
|
584
|
+
if (!serverName && tcName.startsWith('mcp_') && !tcName.match(/^mcp\d/)) {
|
|
585
|
+
const suffix = tcName.slice(4).toLowerCase(); // after "mcp_"
|
|
586
|
+
// First try matching via queried tool sets
|
|
587
|
+
for (const [sn, toolSet] of Object.entries(serverToolPatterns)) {
|
|
588
|
+
for (const tool of toolSet) {
|
|
589
|
+
if (suffix.endsWith('_' + tool) || suffix.endsWith(tool)) {
|
|
590
|
+
serverName = sn; break;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (serverName) break;
|
|
594
|
+
}
|
|
595
|
+
// Fallback: match by sanitized server name (for servers whose tools couldn't be queried)
|
|
596
|
+
// VS Code may truncate sanitized server IDs, so find the best (longest) prefix match
|
|
597
|
+
if (!serverName) {
|
|
598
|
+
let bestLen = 0;
|
|
599
|
+
let bestServer = null;
|
|
600
|
+
for (const s of servers) {
|
|
601
|
+
const sanitized = s.name.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_');
|
|
602
|
+
// Full sanitized name match
|
|
603
|
+
if (suffix.startsWith(sanitized + '_') && sanitized.length > bestLen) {
|
|
604
|
+
bestLen = sanitized.length; bestServer = s.name;
|
|
605
|
+
}
|
|
606
|
+
// Truncated prefix: suffix must start with at least 60% of sanitized name
|
|
607
|
+
const minLen = Math.max(6, Math.ceil(sanitized.length * 0.6));
|
|
608
|
+
for (let len = sanitized.length; len >= minLen; len--) {
|
|
609
|
+
const prefix = sanitized.slice(0, len);
|
|
610
|
+
if (suffix.startsWith(prefix + '_') && len > bestLen) {
|
|
611
|
+
bestLen = len; bestServer = s.name; break;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (bestServer) serverName = bestServer;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Pattern 4: Double-underscore separator — {server_name}__{toolName}
|
|
620
|
+
if (!serverName) {
|
|
621
|
+
const sepMatch = tcName.match(/^(.+?)__(.+)$/);
|
|
622
|
+
if (sepMatch) {
|
|
623
|
+
const tName = sepMatch[2];
|
|
624
|
+
for (const [sn, toolSet] of Object.entries(serverToolPatterns)) {
|
|
625
|
+
if (toolSet.has(tName.toLowerCase())) { serverName = sn; break; }
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (serverName) {
|
|
631
|
+
if (!matchedTools[serverName]) matchedTools[serverName] = [];
|
|
632
|
+
matchedTools[serverName].push(tc);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// 6. Top sessions by tool calls
|
|
637
|
+
const topSessions = Object.values(sessionMap)
|
|
638
|
+
.sort((a, b) => b.totalToolCalls - a.totalToolCalls)
|
|
639
|
+
.slice(0, 50);
|
|
640
|
+
|
|
641
|
+
// 7. Per-project MCP stats
|
|
642
|
+
const projects = cache.getCachedProjects({ hiddenFolders: getHiddenFolders() });
|
|
643
|
+
const projectMcpConfigs = [
|
|
644
|
+
{ file: '.mcp.json', editor: 'claude-code', label: 'Claude Code' },
|
|
645
|
+
{ file: '.cursor/mcp.json', editor: 'cursor', label: 'Cursor' },
|
|
646
|
+
{ file: '.vscode/mcp.json', editor: 'vscode', label: 'VS Code' },
|
|
647
|
+
{ file: '.gemini/settings.json', editor: 'gemini-cli', label: 'Gemini CLI' },
|
|
648
|
+
{ file: '.kiro/settings/mcp.json', editor: 'kiro', label: 'Kiro' },
|
|
649
|
+
];
|
|
650
|
+
|
|
651
|
+
const projectMcps = [];
|
|
652
|
+
for (const proj of projects) {
|
|
653
|
+
if (!proj.folder) continue;
|
|
654
|
+
const configs = [];
|
|
655
|
+
for (const pc of projectMcpConfigs) {
|
|
656
|
+
const configPath = path.join(proj.folder, pc.file);
|
|
657
|
+
if (!fs.existsSync(configPath)) continue;
|
|
658
|
+
try {
|
|
659
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
660
|
+
const parsed = JSON.parse(raw);
|
|
661
|
+
const mcpServers = parsed.mcpServers || parsed.mcp_servers || parsed.servers || {};
|
|
662
|
+
configs.push({
|
|
663
|
+
file: pc.file,
|
|
664
|
+
editor: pc.editor,
|
|
665
|
+
editorLabel: pc.label,
|
|
666
|
+
serverCount: Object.keys(mcpServers).length,
|
|
667
|
+
serverNames: Object.keys(mcpServers),
|
|
668
|
+
});
|
|
669
|
+
} catch { /* skip invalid configs */ }
|
|
670
|
+
}
|
|
671
|
+
if (configs.length === 0) continue;
|
|
672
|
+
|
|
673
|
+
// Count MCP tool calls from this project's sessions
|
|
674
|
+
const projToolCalls = toolRows.filter(r => r.folder === proj.folder);
|
|
675
|
+
const mcpToolCallCount = projToolCalls.filter(r => {
|
|
676
|
+
const n = r.tool_name;
|
|
677
|
+
return n.startsWith('mcp') || n.includes('__');
|
|
678
|
+
}).length;
|
|
679
|
+
|
|
680
|
+
// Which configured servers are used (matched to tool calls)
|
|
681
|
+
const configuredServerNames = new Set(configs.flatMap(c => c.serverNames));
|
|
682
|
+
const matchedServerNames = [];
|
|
683
|
+
for (const sn of configuredServerNames) {
|
|
684
|
+
if (matchedTools[sn]) matchedServerNames.push(sn);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
projectMcps.push({
|
|
688
|
+
folder: proj.folder,
|
|
689
|
+
name: proj.name,
|
|
690
|
+
configs,
|
|
691
|
+
totalServers: [...configuredServerNames].length,
|
|
692
|
+
matchedServers: matchedServerNames.length,
|
|
693
|
+
mcpToolCalls: mcpToolCallCount,
|
|
694
|
+
totalSessions: proj.totalSessions || 0,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
projectMcps.sort((a, b) => b.totalServers - a.totalServers || b.mcpToolCalls - a.mcpToolCalls);
|
|
699
|
+
|
|
700
|
+
// Strip _env from response (security)
|
|
701
|
+
const safeServers = servers.map(({ _env, ...rest }) => rest);
|
|
702
|
+
|
|
703
|
+
res.json({
|
|
704
|
+
servers: safeServers,
|
|
705
|
+
toolCalls,
|
|
706
|
+
matchedTools,
|
|
707
|
+
topSessions,
|
|
708
|
+
projectMcps,
|
|
709
|
+
summary: {
|
|
710
|
+
totalServers: servers.length,
|
|
711
|
+
totalToolCalls: toolRows.length,
|
|
712
|
+
uniqueTools: toolCalls.length,
|
|
713
|
+
sessionsWithTools: Object.keys(sessionMap).length,
|
|
714
|
+
editorsWithServers: [...new Set(servers.map(s => s.editor))],
|
|
715
|
+
},
|
|
716
|
+
});
|
|
717
|
+
} catch (err) {
|
|
718
|
+
res.status(500).json({ error: err.message });
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
|
|
434
722
|
app.get('/api/all-projects', (req, res) => {
|
|
435
723
|
try {
|
|
436
724
|
res.json(cache.getCachedProjects({ ...parseDateOpts(req.query), includeHidden: true }));
|
package/ui/src/App.jsx
CHANGED
|
@@ -17,6 +17,7 @@ import SqlViewer from './pages/SqlViewer'
|
|
|
17
17
|
import Artifacts from './pages/Artifacts'
|
|
18
18
|
import Settings from './pages/Settings'
|
|
19
19
|
import Subscriptions from './pages/Subscriptions'
|
|
20
|
+
import MCPs from './pages/MCPs'
|
|
20
21
|
import RelayDashboard from './pages/RelayDashboard'
|
|
21
22
|
import RelayUserDetail from './pages/RelayUserDetail'
|
|
22
23
|
|
|
@@ -148,6 +149,7 @@ export default function App() {
|
|
|
148
149
|
{ to: '/compare', icon: GitCompare, label: 'Compare' },
|
|
149
150
|
]},
|
|
150
151
|
{ to: '/artifacts', icon: Package, label: 'Artifacts' },
|
|
152
|
+
{ to: '/mcps', icon: Plug, label: 'MCPs' },
|
|
151
153
|
{ to: '/sql', icon: Database, label: 'SQL' },
|
|
152
154
|
]
|
|
153
155
|
|
|
@@ -275,6 +277,7 @@ export default function App() {
|
|
|
275
277
|
<Route path="/compare" element={<Compare overview={overview} />} />
|
|
276
278
|
<Route path="/subscriptions" element={<Subscriptions />} />
|
|
277
279
|
<Route path="/artifacts" element={<Artifacts />} />
|
|
280
|
+
<Route path="/mcps" element={<MCPs />} />
|
|
278
281
|
<Route path="/sql" element={<SqlViewer />} />
|
|
279
282
|
<Route path="/settings" element={<Settings />} />
|
|
280
283
|
</Routes>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default function PageHeader({ icon: Icon, title, children }) {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex items-center gap-3">
|
|
4
|
+
<div className="flex items-center gap-1.5 text-[13px] font-bold" style={{ color: 'var(--c-white)' }}>
|
|
5
|
+
{Icon && <Icon size={14} style={{ color: '#6366f1' }} />}
|
|
6
|
+
{title}
|
|
7
|
+
</div>
|
|
8
|
+
{children}
|
|
9
|
+
</div>
|
|
10
|
+
)
|
|
11
|
+
}
|
package/ui/src/lib/api.js
CHANGED
|
@@ -227,6 +227,13 @@ export async function fetchArtifactContent(filePath) {
|
|
|
227
227
|
return res.json();
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
// ── MCPs API ──
|
|
231
|
+
|
|
232
|
+
export async function fetchMCPs() {
|
|
233
|
+
const res = await fetch(`${BASE}/api/mcps`);
|
|
234
|
+
return res.json();
|
|
235
|
+
}
|
|
236
|
+
|
|
230
237
|
// ── Relay API ──
|
|
231
238
|
|
|
232
239
|
export async function fetchMode() {
|
|
@@ -6,6 +6,7 @@ import { fetchArtifacts, fetchArtifactContent } from '../lib/api'
|
|
|
6
6
|
import { editorColor, editorLabel } from '../lib/constants'
|
|
7
7
|
import EditorIcon from '../components/EditorIcon'
|
|
8
8
|
import AnimatedLoader from '../components/AnimatedLoader'
|
|
9
|
+
import PageHeader from '../components/PageHeader'
|
|
9
10
|
|
|
10
11
|
const MONO = 'JetBrains Mono, monospace'
|
|
11
12
|
|
|
@@ -395,36 +396,22 @@ export default function Artifacts() {
|
|
|
395
396
|
|
|
396
397
|
return (
|
|
397
398
|
<div className="h-full">
|
|
398
|
-
{/* Header
|
|
399
|
-
<div className="
|
|
400
|
-
<
|
|
401
|
-
<
|
|
402
|
-
|
|
403
|
-
<
|
|
404
|
-
<
|
|
399
|
+
{/* Header */}
|
|
400
|
+
<div className="px-4 py-3" style={{ borderBottom: '1px solid var(--c-border)' }}>
|
|
401
|
+
<PageHeader icon={Package} title="Artifacts">
|
|
402
|
+
<span className="text-[11px]" style={{ color: 'var(--c-text3)' }}>{totalArtifacts} artifacts · {totalProjects} projects · {allEditors.size} editors</span>
|
|
403
|
+
<div className="ml-auto relative">
|
|
404
|
+
<Search size={12} className="absolute left-2.5 top-1/2 -translate-y-1/2" style={{ color: 'var(--c-text3)' }} />
|
|
405
|
+
<input
|
|
406
|
+
type="text"
|
|
407
|
+
placeholder="Search artifacts..."
|
|
408
|
+
value={search}
|
|
409
|
+
onChange={e => setSearch(e.target.value)}
|
|
410
|
+
className="pl-7 pr-3 py-1 text-[12px] outline-none w-[200px]"
|
|
411
|
+
style={{ background: 'var(--c-bg3)', border: '1px solid var(--c-border)', color: 'var(--c-text)' }}
|
|
412
|
+
/>
|
|
405
413
|
</div>
|
|
406
|
-
|
|
407
|
-
<FolderOpen size={13} style={{ color: '#6366f1' }} />
|
|
408
|
-
<span className="text-[12px] font-medium" style={{ color: 'var(--c-white)' }}>{totalProjects}</span>
|
|
409
|
-
<span className="text-[11px]" style={{ color: 'var(--c-text3)' }}>projects</span>
|
|
410
|
-
</div>
|
|
411
|
-
<div className="flex items-center gap-2 px-3 py-1.5 rounded" style={{ background: 'var(--c-card)', border: '1px solid var(--c-border)' }}>
|
|
412
|
-
<Hash size={13} style={{ color: '#6366f1' }} />
|
|
413
|
-
<span className="text-[12px] font-medium" style={{ color: 'var(--c-white)' }}>{allEditors.size}</span>
|
|
414
|
-
<span className="text-[11px]" style={{ color: 'var(--c-text3)' }}>editors</span>
|
|
415
|
-
</div>
|
|
416
|
-
</div>
|
|
417
|
-
<div className="ml-auto relative">
|
|
418
|
-
<Search size={12} className="absolute left-2.5 top-1/2 -translate-y-1/2" style={{ color: 'var(--c-text3)' }} />
|
|
419
|
-
<input
|
|
420
|
-
type="text"
|
|
421
|
-
placeholder="Search artifacts..."
|
|
422
|
-
value={search}
|
|
423
|
-
onChange={e => setSearch(e.target.value)}
|
|
424
|
-
className="pl-7 pr-3 py-1.5 text-[12px] rounded w-[240px]"
|
|
425
|
-
style={{ background: 'var(--c-card)', border: '1px solid var(--c-border)', color: 'var(--c-text)', outline: 'none' }}
|
|
426
|
-
/>
|
|
427
|
-
</div>
|
|
414
|
+
</PageHeader>
|
|
428
415
|
</div>
|
|
429
416
|
|
|
430
417
|
{filtered.length === 0 ? (
|
package/ui/src/pages/Compare.jsx
CHANGED
|
@@ -5,6 +5,7 @@ import { Bar } from 'react-chartjs-2'
|
|
|
5
5
|
import { fetchDeepAnalytics, fetchChats } from '../lib/api'
|
|
6
6
|
import { editorColor, editorLabel, formatNumber } from '../lib/constants'
|
|
7
7
|
import { useTheme } from '../lib/theme'
|
|
8
|
+
import PageHeader from '../components/PageHeader'
|
|
8
9
|
|
|
9
10
|
ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend)
|
|
10
11
|
|
|
@@ -152,7 +153,7 @@ export default function Compare({ overview }) {
|
|
|
152
153
|
|
|
153
154
|
return (
|
|
154
155
|
<div className="fade-in space-y-3">
|
|
155
|
-
<
|
|
156
|
+
<PageHeader icon={ArrowLeftRight} title="Compare">
|
|
156
157
|
<select
|
|
157
158
|
value={editorA}
|
|
158
159
|
onChange={e => setEditorA(e.target.value)}
|
|
@@ -161,7 +162,7 @@ export default function Compare({ overview }) {
|
|
|
161
162
|
>
|
|
162
163
|
{editors.map(e => <option key={e.id} value={e.id}>{editorLabel(e.id)}</option>)}
|
|
163
164
|
</select>
|
|
164
|
-
<
|
|
165
|
+
<span style={{ color: 'var(--c-text3)' }}>vs</span>
|
|
165
166
|
<select
|
|
166
167
|
value={editorB}
|
|
167
168
|
onChange={e => setEditorB(e.target.value)}
|
|
@@ -171,7 +172,7 @@ export default function Compare({ overview }) {
|
|
|
171
172
|
{editors.map(e => <option key={e.id} value={e.id}>{editorLabel(e.id)}</option>)}
|
|
172
173
|
</select>
|
|
173
174
|
{loading && <Loader2 size={11} className="animate-spin" style={{ color: 'var(--c-text3)' }} />}
|
|
174
|
-
</
|
|
175
|
+
</PageHeader>
|
|
175
176
|
|
|
176
177
|
{result && (
|
|
177
178
|
<div className="space-y-3">
|
|
@@ -12,6 +12,7 @@ import AnimatedLoader from '../components/AnimatedLoader'
|
|
|
12
12
|
import SectionTitle from '../components/SectionTitle'
|
|
13
13
|
import DateRangePicker from '../components/DateRangePicker'
|
|
14
14
|
import ChatSidebar from '../components/ChatSidebar'
|
|
15
|
+
import PageHeader from '../components/PageHeader'
|
|
15
16
|
|
|
16
17
|
ChartJS.register(ArcElement, CategoryScale, LinearScale, PointElement, LineElement, BarElement, Tooltip, Legend, Filler)
|
|
17
18
|
|
|
@@ -102,17 +103,13 @@ export default function CostAnalysis({ overview }) {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
return (
|
|
105
|
-
<div className="fade-in space-y-
|
|
106
|
+
<div className="fade-in space-y-3">
|
|
106
107
|
{/* Filters */}
|
|
107
|
-
<
|
|
108
|
-
<div className="flex items-center gap-1.5 text-[13px] font-bold" style={{ color: 'var(--c-white)' }}>
|
|
109
|
-
<DollarSign size={14} style={{ color: '#6366f1' }} />
|
|
110
|
-
Cost Analysis
|
|
111
|
-
</div>
|
|
108
|
+
<PageHeader icon={DollarSign} title="Cost Analysis">
|
|
112
109
|
<select
|
|
113
110
|
value={editor}
|
|
114
111
|
onChange={e => setEditor(e.target.value)}
|
|
115
|
-
className="px-2 py-1 text-[12px] outline-none
|
|
112
|
+
className="px-2 py-1 text-[12px] outline-none"
|
|
116
113
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
117
114
|
>
|
|
118
115
|
<option value="">All Editors</option>
|
|
@@ -121,7 +118,7 @@ export default function CostAnalysis({ overview }) {
|
|
|
121
118
|
))}
|
|
122
119
|
</select>
|
|
123
120
|
<div className="ml-auto"><DateRangePicker value={apiDateRange} onChange={setApiDateRange} /></div>
|
|
124
|
-
</
|
|
121
|
+
</PageHeader>
|
|
125
122
|
|
|
126
123
|
{/* Disclaimer */}
|
|
127
124
|
<div className="flex items-start gap-2 px-3 py-2 text-[11px] rounded" style={{ background: 'rgba(234,179,8,0.06)', border: '1px solid rgba(234,179,8,0.15)', color: '#ca8a04' }}>
|
|
@@ -130,7 +127,7 @@ export default function CostAnalysis({ overview }) {
|
|
|
130
127
|
</div>
|
|
131
128
|
|
|
132
129
|
{/* KPIs */}
|
|
133
|
-
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(
|
|
130
|
+
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
|
|
134
131
|
<KpiCard label="total est. cost" value={formatCost(totalCost)} sub="all models" />
|
|
135
132
|
<KpiCard label="avg / session" value={formatCost(summary.avgPerSession)} sub={`${formatNumber(summary.totalSessions)} sessions`} />
|
|
136
133
|
<KpiCard label="avg / day" value={formatCost(summary.avgPerDay)} sub={`${summary.totalDays} days`} />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
2
|
import { useNavigate } from 'react-router-dom'
|
|
3
|
-
import { ArrowRight, X, Flame, Zap, MessageSquare, Wrench, Share2, AlertTriangle } from 'lucide-react'
|
|
3
|
+
import { ArrowRight, X, Flame, Zap, MessageSquare, Wrench, Share2, AlertTriangle, LayoutDashboard } 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'
|
|
@@ -14,6 +14,7 @@ import AnimatedLoader from '../components/AnimatedLoader'
|
|
|
14
14
|
import ShareModal from '../components/ShareModal'
|
|
15
15
|
import { useTheme } from '../lib/theme'
|
|
16
16
|
import SectionTitle from '../components/SectionTitle'
|
|
17
|
+
import PageHeader from '../components/PageHeader'
|
|
17
18
|
|
|
18
19
|
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, PointElement, LineElement, Filler)
|
|
19
20
|
|
|
@@ -175,17 +176,19 @@ export default function Dashboard({ overview }) {
|
|
|
175
176
|
return (
|
|
176
177
|
<div className="fade-in space-y-3">
|
|
177
178
|
{/* Top bar */}
|
|
178
|
-
<
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
179
|
+
<PageHeader icon={LayoutDashboard} title="Dashboard">
|
|
180
|
+
<div className="ml-auto flex items-center gap-3">
|
|
181
|
+
<DateRangePicker value={dateRange} onChange={setDateRange} />
|
|
182
|
+
<button
|
|
183
|
+
onClick={() => setShareOpen(true)}
|
|
184
|
+
className="flex items-center gap-1.5 px-3 py-1 text-[12px] rounded-md transition hover:opacity-80"
|
|
185
|
+
style={{ background: '#6366f1', color: '#fff' }}
|
|
186
|
+
>
|
|
187
|
+
<Share2 size={12} />
|
|
188
|
+
Share Stats
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
</PageHeader>
|
|
189
192
|
|
|
190
193
|
{/* Editor breakdown - compact row */}
|
|
191
194
|
<div className="card p-3">
|
|
@@ -9,6 +9,7 @@ import KpiCard from '../components/KpiCard'
|
|
|
9
9
|
import EditorIcon from '../components/EditorIcon'
|
|
10
10
|
import SectionTitle from '../components/SectionTitle'
|
|
11
11
|
import DateRangePicker from '../components/DateRangePicker'
|
|
12
|
+
import PageHeader from '../components/PageHeader'
|
|
12
13
|
|
|
13
14
|
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
|
|
14
15
|
|
|
@@ -247,13 +248,13 @@ export default function DeepAnalysis({ overview }) {
|
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
return (
|
|
250
|
-
<div className="fade-in space-y-
|
|
251
|
+
<div className="fade-in space-y-3">
|
|
251
252
|
{/* Filters */}
|
|
252
|
-
<
|
|
253
|
+
<PageHeader icon={TrendingUp} title="Deep Analysis">
|
|
253
254
|
<select
|
|
254
255
|
value={editor}
|
|
255
256
|
onChange={e => setEditor(e.target.value)}
|
|
256
|
-
className="px-2 py-1
|
|
257
|
+
className="px-2 py-1 text-[12px] outline-none"
|
|
257
258
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
258
259
|
>
|
|
259
260
|
<option value="">All Editors</option>
|
|
@@ -264,7 +265,7 @@ export default function DeepAnalysis({ overview }) {
|
|
|
264
265
|
<select
|
|
265
266
|
value={folder}
|
|
266
267
|
onChange={e => setFolder(e.target.value)}
|
|
267
|
-
className="px-2 py-1
|
|
268
|
+
className="px-2 py-1 text-[12px] outline-none max-w-[200px] truncate"
|
|
268
269
|
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
269
270
|
>
|
|
270
271
|
<option value="">All Projects</option>
|
|
@@ -275,7 +276,7 @@ export default function DeepAnalysis({ overview }) {
|
|
|
275
276
|
{loading && <Loader2 size={11} className="animate-spin" style={{ color: 'var(--c-text3)' }} />}
|
|
276
277
|
{data && <span className="text-[11px]" style={{ color: 'var(--c-text2)' }}>{data.analyzedChats} sessions analyzed</span>}
|
|
277
278
|
<div className="ml-auto"><DateRangePicker value={dateRange} onChange={setDateRange} /></div>
|
|
278
|
-
</
|
|
279
|
+
</PageHeader>
|
|
279
280
|
|
|
280
281
|
{data && insights && (
|
|
281
282
|
<>
|