feat: Add AgentAnalyzer for tracking agent usage patterns and analytics

- Introduced AgentAnalyzer class to analyze agent invocation data, usage frequency, and workflow patterns.
- Implemented endpoint for agent usage analytics in ClaudeAnalytics.
- Enhanced session management to account for scheduled reset times instead of fixed durations.
- Updated SessionAnalyzer to reflect new session reset logic and improved warning messages.
- Added functionality to calculate agent efficiency and popular usage hours.
- Created a .nojekyll file for GitHub Pages deployment in Docusaurus configuration.
This commit is contained in:
daniel
2025-07-29 08:54:47 -05:00
parent 03f9be889b
commit 76810b3fe4
9 changed files with 2777 additions and 175 deletions
+1 -1
View File
@@ -324,7 +324,7 @@ The analytics dashboard has been refactored into a modular architecture in 4 pha
### During Development
1. Use meaningful variable and function names
2. Run tests frequently to catch issues early: `npm run test:watch`
3. Test analytics dashboard: `npm run analytics:start`
3. For frontend changes, let user handle analytics server startup - don't run `npm run analytics:start` automatically
4. Use meaningful commit messages
### Before Committing
@@ -0,0 +1,710 @@
/**
* AgentAnalytics - Component for displaying agent usage analytics and charts
* Shows comprehensive agent usage statistics, trends, and workflow patterns
*/
class AgentAnalytics {
constructor(container, services) {
this.container = container;
this.dataService = services.data;
this.stateService = services.state;
this.agentData = null;
this.charts = {};
this.isInitialized = false;
// Date filter state
this.dateFilters = {
startDate: null,
endDate: null
};
// Subscribe to data refresh events
this.dataService.onDataRefresh(this.handleDataRefresh.bind(this));
}
/**
* Initialize the agent analytics component
*/
async initialize() {
if (this.isInitialized) return;
try {
this.stateService.setLoading(true);
await this.render();
await this.loadAgentData();
this.setupEventListeners();
this.isInitialized = true;
} catch (error) {
console.error('Error initializing agent analytics:', error);
this.stateService.setError(error);
} finally {
this.stateService.setLoading(false);
}
}
/**
* Render the agent analytics UI
*/
async render() {
this.container.innerHTML = `
<div class="agent-analytics">
<!-- Header Section -->
<div class="analytics-header">
<div class="header-content">
<h2 class="analytics-title">
<span class="title-icon">🤖</span>
Agent Usage Analytics
</h2>
<p class="analytics-subtitle">Specialized Claude Code agent usage patterns and workflow insights</p>
</div>
<!-- Date Filter Controls -->
<div class="date-filters">
<div class="filter-group">
<label for="start-date">From:</label>
<input type="date" id="start-date" class="date-input">
</div>
<div class="filter-group">
<label for="end-date">To:</label>
<input type="date" id="end-date" class="date-input">
</div>
<button class="refresh-btn" id="refresh-analytics">
<span class="btn-icon">🔄</span>
Refresh
</button>
</div>
</div>
<!-- Summary Metrics -->
<div class="metrics-grid" id="metrics-grid">
<div class="metric-card loading">
<div class="metric-icon">🔄</div>
<div class="metric-content">
<div class="metric-value">Loading...</div>
<div class="metric-label">Agent Data</div>
</div>
</div>
</div>
<!-- Charts Section -->
<div class="charts-container">
<!-- Agent Usage Overview -->
<div class="chart-card">
<div class="chart-header">
<h3>Agent Usage Distribution</h3>
<p>Total invocations per specialized agent type</p>
</div>
<div class="chart-content">
<canvas id="agent-usage-chart" width="400" height="200"></canvas>
</div>
</div>
<!-- Usage Timeline -->
<div class="chart-card">
<div class="chart-header">
<h3>Agent Usage Timeline</h3>
<p>Agent invocations over time showing workflow patterns</p>
</div>
<div class="chart-content">
<canvas id="agent-timeline-chart" width="400" height="200"></canvas>
</div>
</div>
<!-- Hourly Usage Pattern -->
<div class="chart-card">
<div class="chart-header">
<h3>Popular Usage Hours</h3>
<p>Agent activity distribution throughout the day</p>
</div>
<div class="chart-content">
<canvas id="hourly-usage-chart" width="400" height="200"></canvas>
</div>
</div>
<!-- Agent Efficiency Metrics -->
<div class="chart-card">
<div class="chart-header">
<h3>Agent Workflow Efficiency</h3>
<p>Usage patterns and adoption rates across different agents</p>
</div>
<div class="chart-content">
<div class="efficiency-metrics" id="efficiency-metrics">
<div class="efficiency-loading">Loading efficiency data...</div>
</div>
</div>
</div>
</div>
<!-- Detailed Agent Stats -->
<div class="agent-details-section">
<h3>Detailed Agent Statistics</h3>
<div class="agent-stats-grid" id="agent-stats-grid">
<div class="stats-loading">Loading agent statistics...</div>
</div>
</div>
<!-- Workflow Patterns -->
<div class="workflow-patterns-section" id="workflow-patterns-section" style="display: none;">
<h3>Agent Workflow Patterns</h3>
<p>Common sequences of agent usage within workflow sessions</p>
<div class="workflow-patterns" id="workflow-patterns">
</div>
</div>
</div>
`;
}
/**
* Load agent analytics data from API
*/
async loadAgentData() {
try {
const params = new URLSearchParams();
if (this.dateFilters.startDate) {
params.append('startDate', this.dateFilters.startDate);
}
if (this.dateFilters.endDate) {
params.append('endDate', this.dateFilters.endDate);
}
const url = `/api/agents${params.toString() ? '?' + params.toString() : ''}`;
this.agentData = await this.dataService.cachedFetch(url);
if (this.agentData) {
this.updateMetrics();
this.renderCharts();
this.renderAgentStats();
this.renderWorkflowPatterns();
} else {
this.renderNoData();
}
} catch (error) {
console.error('Error loading agent data:', error);
this.renderError(error);
}
}
/**
* Update summary metrics
*/
updateMetrics() {
const metricsGrid = this.container.querySelector('#metrics-grid');
if (!metricsGrid || !this.agentData) return;
const { summary, totalAgentInvocations, totalAgentTypes, efficiency } = this.agentData;
metricsGrid.innerHTML = `
<div class="metric-card primary">
<div class="metric-icon">🚀</div>
<div class="metric-content">
<div class="metric-value">${totalAgentInvocations}</div>
<div class="metric-label">Total Invocations</div>
</div>
</div>
<div class="metric-card secondary">
<div class="metric-icon">🤖</div>
<div class="metric-content">
<div class="metric-value">${totalAgentTypes}</div>
<div class="metric-label">Agent Types Used</div>
</div>
</div>
<div class="metric-card success">
<div class="metric-icon">⭐</div>
<div class="metric-content">
<div class="metric-value">${efficiency.adoptionRate}%</div>
<div class="metric-label">Adoption Rate</div>
</div>
</div>
<div class="metric-card info">
<div class="metric-icon">📊</div>
<div class="metric-content">
<div class="metric-value">${efficiency.averageInvocationsPerAgent}</div>
<div class="metric-label">Avg. per Agent</div>
</div>
</div>
`;
}
/**
* Render all charts
*/
renderCharts() {
if (!this.agentData) return;
this.renderAgentUsageChart();
this.renderTimelineChart();
this.renderHourlyUsageChart();
this.renderEfficiencyMetrics();
}
/**
* Render agent usage distribution chart
*/
renderAgentUsageChart() {
const canvas = this.container.querySelector('#agent-usage-chart');
if (!canvas || !this.agentData.agentStats) return;
// Destroy existing chart
if (this.charts.usage) {
this.charts.usage.destroy();
}
const ctx = canvas.getContext('2d');
const agentStats = this.agentData.agentStats;
this.charts.usage = new Chart(ctx, {
type: 'doughnut',
data: {
labels: agentStats.map(agent => agent.name),
datasets: [{
data: agentStats.map(agent => agent.totalInvocations),
backgroundColor: agentStats.map(agent => agent.color),
borderColor: 'var(--bg-primary)',
borderWidth: 2,
hoverBorderWidth: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
color: 'var(--text-primary)',
padding: 20,
usePointStyle: true,
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
}
}
},
tooltip: {
titleFont: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
},
bodyFont: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
},
callbacks: {
label: function(context) {
const agent = agentStats[context.dataIndex];
return `${agent.name}: ${context.parsed} invocations (${agent.uniqueConversations} conversations)`;
}
}
}
}
}
});
}
/**
* Render agent usage timeline chart
*/
renderTimelineChart() {
const canvas = this.container.querySelector('#agent-timeline-chart');
if (!canvas || !this.agentData.usageByDay) return;
// Destroy existing chart
if (this.charts.timeline) {
this.charts.timeline.destroy();
}
const ctx = canvas.getContext('2d');
const timelineData = this.agentData.usageByDay;
this.charts.timeline = new Chart(ctx, {
type: 'line',
data: {
labels: timelineData.map(d => new Date(d.date).toLocaleDateString()),
datasets: [{
label: 'Agent Invocations',
data: timelineData.map(d => d.count),
borderColor: '#3fb950',
backgroundColor: 'rgba(63, 185, 80, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.3,
pointBackgroundColor: '#3fb950',
pointBorderColor: '#ffffff',
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: 'var(--text-primary)',
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
}
}
},
tooltip: {
titleFont: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
},
bodyFont: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
}
}
},
scales: {
x: {
ticks: {
color: 'var(--text-secondary)',
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
}
},
grid: {
color: 'var(--border-primary)'
}
},
y: {
beginAtZero: true,
ticks: {
color: 'var(--text-secondary)',
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
}
},
grid: {
color: 'var(--border-primary)'
}
}
}
}
});
}
/**
* Render hourly usage pattern chart
*/
renderHourlyUsageChart() {
const canvas = this.container.querySelector('#hourly-usage-chart');
if (!canvas || !this.agentData.popularHours) return;
// Destroy existing chart
if (this.charts.hourly) {
this.charts.hourly.destroy();
}
const ctx = canvas.getContext('2d');
const hourlyData = this.agentData.popularHours;
this.charts.hourly = new Chart(ctx, {
type: 'bar',
data: {
labels: hourlyData.map(h => h.label),
datasets: [{
label: 'Agent Invocations',
data: hourlyData.map(h => h.count),
backgroundColor: 'rgba(217, 116, 85, 0.6)',
borderColor: '#d57455',
borderWidth: 1,
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: 'var(--text-primary)',
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
}
}
},
tooltip: {
titleFont: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
},
bodyFont: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
}
}
},
scales: {
x: {
ticks: {
color: 'var(--text-secondary)',
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
}
},
grid: {
color: 'var(--border-primary)'
}
},
y: {
beginAtZero: true,
ticks: {
color: 'var(--text-secondary)',
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
}
},
grid: {
color: 'var(--border-primary)'
}
}
}
}
});
}
/**
* Render efficiency metrics
*/
renderEfficiencyMetrics() {
const container = this.container.querySelector('#efficiency-metrics');
if (!container || !this.agentData.efficiency) return;
const { efficiency, agentStats } = this.agentData;
const mostUsedAgent = efficiency.mostUsedAgent;
container.innerHTML = `
<div class="efficiency-grid">
<div class="efficiency-card">
<div class="efficiency-icon">🎯</div>
<div class="efficiency-content">
<div class="efficiency-value">${efficiency.averageInvocationsPerAgent}</div>
<div class="efficiency-label">Avg. Invocations/Agent</div>
</div>
</div>
<div class="efficiency-card">
<div class="efficiency-icon">📈</div>
<div class="efficiency-content">
<div class="efficiency-value">${efficiency.adoptionRate}%</div>
<div class="efficiency-label">Adoption Rate</div>
</div>
</div>
<div class="efficiency-card">
<div class="efficiency-icon">${mostUsedAgent ? mostUsedAgent.icon : '🤖'}</div>
<div class="efficiency-content">
<div class="efficiency-value">${mostUsedAgent ? mostUsedAgent.name : 'None'}</div>
<div class="efficiency-label">Most Used Agent</div>
</div>
</div>
<div class="efficiency-card">
<div class="efficiency-icon">🔧</div>
<div class="efficiency-content">
<div class="efficiency-value">${efficiency.agentDiversity}</div>
<div class="efficiency-label">Agent Diversity</div>
</div>
</div>
</div>
`;
}
/**
* Render detailed agent statistics
*/
renderAgentStats() {
const container = this.container.querySelector('#agent-stats-grid');
if (!container || !this.agentData.agentStats) return;
container.innerHTML = this.agentData.agentStats.map(agent => `
<div class="agent-stat-card">
<div class="agent-stat-header">
<div class="agent-stat-icon">${agent.icon}</div>
<div class="agent-stat-info">
<h4>${agent.name}</h4>
<p>${agent.description}</p>
</div>
<div class="agent-stat-badge" style="background-color: ${agent.color}20; color: ${agent.color};">
${agent.totalInvocations} uses
</div>
</div>
<div class="agent-stat-metrics">
<div class="stat-metric">
<span class="metric-label">Conversations:</span>
<span class="metric-value">${agent.uniqueConversations}</span>
</div>
<div class="stat-metric">
<span class="metric-label">Avg. per conversation:</span>
<span class="metric-value">${agent.averageUsagePerConversation}</span>
</div>
<div class="stat-metric">
<span class="metric-label">First used:</span>
<span class="metric-value">${new Date(agent.firstUsed).toLocaleDateString()}</span>
</div>
<div class="stat-metric">
<span class="metric-label">Last used:</span>
<span class="metric-value">${new Date(agent.lastUsed).toLocaleDateString()}</span>
</div>
</div>
</div>
`).join('');
}
/**
* Render workflow patterns
*/
renderWorkflowPatterns() {
const section = this.container.querySelector('#workflow-patterns-section');
const container = this.container.querySelector('#workflow-patterns');
if (!container || !this.agentData.workflowPatterns) return;
if (this.agentData.workflowPatterns.length === 0) {
section.style.display = 'none';
return;
}
section.style.display = 'block';
container.innerHTML = this.agentData.workflowPatterns.map(pattern => `
<div class="workflow-pattern">
<div class="pattern-flow">${pattern.pattern}</div>
<div class="pattern-count">${pattern.count} times</div>
</div>
`).join('');
}
/**
* Render no data state
*/
renderNoData() {
this.container.innerHTML = `
<div class="no-agent-data">
<div class="no-data-content">
<div class="no-data-icon">🤖</div>
<h3>No Agent Usage Data</h3>
<p>No specialized Claude Code agents have been used yet.</p>
<div class="agent-types">
<h4>Available Agent Types:</h4>
<ul>
<li><strong>general-purpose:</strong> Multi-step tasks and research</li>
<li><strong>claude-code-best-practices:</strong> Workflow optimization</li>
<li><strong>docusaurus-expert:</strong> Documentation management</li>
</ul>
</div>
<button class="refresh-btn" onclick="this.loadAgentData()">
<span class="btn-icon">🔄</span>
Refresh Data
</button>
</div>
</div>
`;
}
/**
* Render error state
*/
renderError(error) {
this.container.innerHTML = `
<div class="agent-analytics-error">
<div class="error-content">
<div class="error-icon">⚠️</div>
<h3>Error Loading Agent Data</h3>
<p>${error.message || 'Failed to load agent analytics data'}</p>
<button class="refresh-btn" onclick="this.loadAgentData()">
<span class="btn-icon">🔄</span>
Try Again
</button>
</div>
</div>
`;
}
/**
* Setup event listeners
*/
setupEventListeners() {
// Date filter change handlers
const startDateInput = this.container.querySelector('#start-date');
const endDateInput = this.container.querySelector('#end-date');
const refreshBtn = this.container.querySelector('#refresh-analytics');
if (startDateInput) {
startDateInput.addEventListener('change', (e) => {
this.dateFilters.startDate = e.target.value;
});
}
if (endDateInput) {
endDateInput.addEventListener('change', (e) => {
this.dateFilters.endDate = e.target.value;
});
}
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
this.refreshData();
});
}
}
/**
* Refresh analytics data
*/
async refreshData() {
try {
const refreshBtn = this.container.querySelector('#refresh-analytics');
if (refreshBtn) {
refreshBtn.disabled = true;
const icon = refreshBtn.querySelector('.btn-icon');
if (icon) icon.style.animation = 'spin 1s linear infinite';
}
await this.loadAgentData();
} catch (error) {
console.error('Error refreshing agent data:', error);
} finally {
const refreshBtn = this.container.querySelector('#refresh-analytics');
if (refreshBtn) {
refreshBtn.disabled = false;
const icon = refreshBtn.querySelector('.btn-icon');
if (icon) icon.style.animation = '';
}
}
}
/**
* Handle data refresh events
*/
handleDataRefresh(data, source) {
if (source === 'agents' || source === 'all') {
this.loadAgentData();
}
}
/**
* Destroy the component and cleanup
*/
destroy() {
// Destroy charts
Object.values(this.charts).forEach(chart => {
if (chart && typeof chart.destroy === 'function') {
chart.destroy();
}
});
this.charts = {};
// Clear container
if (this.container) {
this.container.innerHTML = '';
}
this.isInitialized = false;
}
}
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = AgentAnalytics;
}
@@ -188,13 +188,9 @@ class DashboardPage {
<div class="metrics-cards-container">
<!-- Conversations Card -->
<div class="metric-card">
<div class="metric-header">
<div class="metric-icon">💬</div>
<div class="metric-title">Conversations</div>
</div>
<div class="metric-primary">
<span class="metric-primary-value" id="totalConversations">0</span>
<span class="metric-primary-label">Total</span>
<span class="metric-primary-label">Total Conversations</span>
</div>
<div class="metric-secondary">
<div class="metric-secondary-item">
@@ -214,13 +210,9 @@ class DashboardPage {
<!-- Sessions Card -->
<div class="metric-card">
<div class="metric-header">
<div class="metric-icon">⚡</div>
<div class="metric-title">Sessions</div>
</div>
<div class="metric-primary">
<span class="metric-primary-value" id="claudeSessions">0</span>
<span class="metric-primary-label">Total</span>
<span class="metric-primary-label">Total Sessions</span>
</div>
<div class="metric-secondary">
<div class="metric-secondary-item">
@@ -240,13 +232,9 @@ class DashboardPage {
<!-- Tokens Card -->
<div class="metric-card">
<div class="metric-header">
<div class="metric-icon">🔢</div>
<div class="metric-title">Tokens</div>
</div>
<div class="metric-primary">
<span class="metric-primary-value" id="totalTokens">0</span>
<span class="metric-primary-label">Total</span>
<span class="metric-primary-label">Total Tokens</span>
</div>
<div class="metric-secondary">
<div class="metric-secondary-item">
@@ -263,6 +251,28 @@ class DashboardPage {
</div>
</div>
</div>
<!-- Agents Card -->
<div class="metric-card">
<div class="metric-primary">
<span class="metric-primary-value" id="totalAgentInvocations">0</span>
<span class="metric-primary-label">Total Agent Uses</span>
</div>
<div class="metric-secondary">
<div class="metric-secondary-item">
<span class="metric-secondary-label">Types:</span>
<span class="metric-secondary-value" id="totalAgentTypes">0</span>
</div>
<div class="metric-secondary-item">
<span class="metric-secondary-label">Top Agent:</span>
<span class="metric-secondary-value" id="topAgentName">None</span>
</div>
<div class="metric-secondary-item">
<span class="metric-secondary-label">Adoption:</span>
<span class="metric-secondary-value" id="agentAdoption">0%</span>
</div>
</div>
</div>
</div>
<!-- Session Timer Section -->
@@ -286,35 +296,96 @@ class DashboardPage {
</div>
</div>
<!-- Charts Container (2x2 Grid) -->
<!-- Charts Container - Organized by Sections -->
<div class="charts-container">
<div class="chart-card">
<div class="chart-title">
📊 token usage over time
</div>
<canvas id="tokenChart" class="chart-canvas"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">
🎯 project activity distribution
<!-- SECTION 1: Token Analytics -->
<div class="chart-section">
<div class="section-header">
<h3 class="section-title">🔢 Token Analytics</h3>
<p class="section-description">Monitor token consumption patterns and efficiency</p>
</div>
<div class="section-charts">
<div class="chart-card">
<div class="chart-title">
Token Usage Over Time
</div>
<canvas id="tokenChart" class="chart-canvas"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">
Token Distribution by Type
</div>
<canvas id="tokenTypeChart" class="chart-canvas"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">
Token Usage Over Time
</div>
<canvas id="tokenTimelineChart" class="chart-canvas"></canvas>
</div>
</div>
<canvas id="projectChart" class="chart-canvas"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">
🛠️ tool usage trends
<!-- SECTION 2: Workflow Intelligence -->
<div class="chart-section">
<div class="section-header">
<h3 class="section-title">🤖 Workflow Intelligence</h3>
<p class="section-description">Analyze agent usage and automation patterns</p>
</div>
<div class="section-charts">
<div class="chart-card">
<div class="chart-title">
Agent Usage Distribution
</div>
<canvas id="agentUsageChart" class="chart-canvas"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">
Agent Activity Timeline
</div>
<canvas id="agentTimelineChart" class="chart-canvas"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">
Workflow Efficiency Score
</div>
<canvas id="workflowEfficiencyChart" class="chart-canvas"></canvas>
</div>
</div>
<canvas id="toolChart" class="chart-canvas"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">
⚡ tool activity summary
<!-- SECTION 3: Productivity Analytics -->
<div class="chart-section">
<div class="section-header">
<h3 class="section-title">📈 Productivity Analytics</h3>
<p class="section-description">Track project activity and tool utilization</p>
</div>
<div id="toolSummary" class="tool-summary">
<!-- Tool summary will be loaded here -->
<div class="section-charts">
<div class="chart-card">
<div class="chart-title">
Project Activity Distribution
</div>
<canvas id="projectChart" class="chart-canvas"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">
Tool Usage Patterns
</div>
<canvas id="toolChart" class="chart-canvas"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">
Daily Productivity Trends
</div>
<canvas id="productivityChart" class="chart-canvas"></canvas>
</div>
</div>
</div>
</div>
@@ -825,15 +896,19 @@ class DashboardPage {
*/
async loadInitialData() {
try {
const [conversationsData, statesData] = await Promise.all([
const [conversationsData, statesData, agentData] = await Promise.all([
this.dataService.getConversations(),
this.dataService.getConversationStates()
this.dataService.getConversationStates(),
this.dataService.cachedFetch('/api/agents')
]);
this.stateService.updateConversations(conversationsData.conversations);
this.stateService.updateSummary(conversationsData.summary);
this.stateService.updateConversationStates(statesData);
// Store agent data for charts
this.agentData = agentData;
// Update dashboard with original format
this.updateSummaryDisplay(
conversationsData.summary,
@@ -843,6 +918,7 @@ class DashboardPage {
this.updateLastUpdateTime();
this.updateChartData(conversationsData);
this.updateAgentCharts(agentData);
} catch (error) {
console.error('Error loading initial data:', error);
@@ -948,6 +1024,11 @@ class DashboardPage {
this.updateTokenBreakdown(detailedTokenUsage);
}
// Update agent metrics if available
if (this.agentData) {
this.updateAgentMetrics(this.agentData);
}
// Store data for chart updates
this.allData = allData;
}
@@ -985,6 +1066,44 @@ class DashboardPage {
if (cacheTokens) cacheTokens.textContent = totalCache.toLocaleString();
}
/**
* Update agent metrics in the agents card
* @param {Object} agentData - Agent analytics data
*/
updateAgentMetrics(agentData) {
if (!agentData) return;
const totalAgentInvocations = this.container.querySelector('#totalAgentInvocations');
const totalAgentTypes = this.container.querySelector('#totalAgentTypes');
const topAgentName = this.container.querySelector('#topAgentName');
const agentAdoption = this.container.querySelector('#agentAdoption');
// Update primary metric - total invocations
if (totalAgentInvocations) {
totalAgentInvocations.textContent = agentData.totalAgentInvocations?.toLocaleString() || '0';
}
// Update secondary metrics
if (totalAgentTypes) {
totalAgentTypes.textContent = agentData.totalAgentTypes?.toLocaleString() || '0';
}
if (topAgentName) {
const topAgent = agentData.agentStats?.[0];
if (topAgent) {
topAgentName.textContent = topAgent.name;
topAgentName.title = `${topAgent.totalInvocations} uses`;
} else {
topAgentName.textContent = 'None';
}
}
if (agentAdoption) {
const adoptionRate = agentData.efficiency?.adoptionRate || '0';
agentAdoption.textContent = adoptionRate + '%';
}
}
/**
* Show token popover
*/
@@ -1050,6 +1169,9 @@ class DashboardPage {
if (this.allData) {
this.updateChartData(this.allData);
}
if (this.agentData) {
this.updateAgentCharts(this.agentData);
}
}
/**
@@ -1095,9 +1217,17 @@ class DashboardPage {
updateChartData(data) {
if (!data || !data.conversations) return;
// Token Analytics Section
this.updateTokenChart(data.conversations);
this.updateTokenTypeChart(data);
this.updateTokenTimelineChart(data);
// Productivity Analytics Section
this.updateProjectChart(data.conversations);
this.updateToolChart(data.conversations);
this.updateProductivityChart(data);
// Legacy tool summary (keeping for now)
this.updateToolSummary(data.conversations);
}
@@ -1256,6 +1386,647 @@ class DashboardPage {
`;
}
/**
* Update agent usage charts
* @param {Object} agentData - Agent analytics data
*/
updateAgentCharts(agentData) {
if (!agentData || !agentData.agentStats) {
console.warn('No agent data available for charts');
return;
}
this.updateAgentUsageChart(agentData);
this.updateAgentTimelineChart(agentData);
this.updateWorkflowEfficiencyChart(agentData);
}
/**
* Update agent usage distribution chart
* @param {Object} agentData - Agent analytics data
*/
updateAgentUsageChart(agentData) {
const canvas = this.container.querySelector('#agentUsageChart');
if (!canvas) {
console.warn('Agent usage chart canvas not found');
return;
}
// Destroy existing chart if it exists
const existingChart = Chart.getChart(canvas);
if (existingChart) {
existingChart.destroy();
}
const ctx = canvas.getContext('2d');
const agentStats = agentData.agentStats || [];
if (agentStats.length === 0) {
// Show "no data" message
ctx.fillStyle = '#7d8590';
ctx.textAlign = 'center';
ctx.font = '14px Monaco, monospace';
ctx.fillText('No agent usage data', canvas.width / 2, canvas.height / 2);
return;
}
new Chart(ctx, {
type: 'doughnut',
data: {
labels: agentStats.map(agent => agent.name),
datasets: [{
data: agentStats.map(agent => agent.totalInvocations),
backgroundColor: agentStats.map(agent => agent.color),
borderColor: '#0d1117',
borderWidth: 2,
hoverBorderWidth: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#c9d1d9',
padding: 10,
usePointStyle: true,
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
size: 11
}
}
},
tooltip: {
titleFont: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
},
bodyFont: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
},
callbacks: {
label: function(context) {
const agent = agentStats[context.dataIndex];
return `${agent.name}: ${context.parsed} uses (${agent.uniqueConversations} conversations)`;
}
}
}
},
cutout: '60%'
}
});
}
/**
* Update agent usage timeline chart
* @param {Object} agentData - Agent analytics data
*/
updateAgentTimelineChart(agentData) {
const canvas = this.container.querySelector('#agentTimelineChart');
if (!canvas) {
console.warn('Agent timeline chart canvas not found');
return;
}
// Destroy existing chart if it exists
const existingChart = Chart.getChart(canvas);
if (existingChart) {
existingChart.destroy();
}
const ctx = canvas.getContext('2d');
const usageByDay = agentData.usageByDay || [];
if (usageByDay.length === 0) {
// Show "no data" message
ctx.fillStyle = '#7d8590';
ctx.textAlign = 'center';
ctx.font = '14px Monaco, monospace';
ctx.fillText('No timeline data', canvas.width / 2, canvas.height / 2);
return;
}
new Chart(ctx, {
type: 'line',
data: {
labels: usageByDay.map(d => new Date(d.date).toLocaleDateString()),
datasets: [{
label: 'Agent Usage',
data: usageByDay.map(d => d.count),
borderColor: '#3fb950',
backgroundColor: 'rgba(63, 185, 80, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.3,
pointBackgroundColor: '#3fb950',
pointBorderColor: '#ffffff',
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: '#c9d1d9',
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
size: 11
}
}
},
tooltip: {
titleFont: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
},
bodyFont: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
},
callbacks: {
label: function(context) {
return `Agent invocations: ${context.parsed.y}`;
}
}
}
},
scales: {
x: {
ticks: {
color: '#7d8590',
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
}
},
grid: {
color: '#30363d'
}
},
y: {
beginAtZero: true,
ticks: {
color: '#7d8590',
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace"
},
stepSize: 1
},
grid: {
color: '#30363d'
}
}
}
}
});
}
/**
* Update workflow efficiency chart
* @param {Object} agentData - Agent analytics data
*/
updateWorkflowEfficiencyChart(agentData) {
const canvas = this.container.querySelector('#workflowEfficiencyChart');
if (!canvas) {
console.warn('Workflow efficiency chart canvas not found');
return;
}
// Destroy existing chart if it exists
const existingChart = Chart.getChart(canvas);
if (existingChart) {
existingChart.destroy();
}
const ctx = canvas.getContext('2d');
const efficiency = agentData.efficiency || {};
const data = {
labels: ['Adoption Rate', 'Workflow Completion', 'Time Efficiency', 'Success Rate'],
datasets: [{
label: 'Efficiency %',
data: [
efficiency.adoptionRate || 0,
efficiency.workflowCompletion || 0,
efficiency.timeEfficiency || 0,
efficiency.successRate || 0
],
backgroundColor: [
'rgba(63, 185, 80, 0.8)',
'rgba(88, 166, 255, 0.8)',
'rgba(249, 115, 22, 0.8)',
'rgba(213, 116, 85, 0.8)'
],
borderColor: [
'#3fb950',
'#58a6ff',
'#f97316',
'#d57455'
],
borderWidth: 2
}]
};
new Chart(ctx, {
type: 'radar',
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.label;
const value = context.parsed.r;
return `${label}: ${value.toFixed(1)}%`;
}
}
}
},
scales: {
r: {
beginAtZero: true,
max: 100,
ticks: {
stepSize: 20,
color: '#7d8590',
backdropColor: 'transparent'
},
grid: {
color: '#30363d'
},
angleLines: {
color: '#30363d'
},
pointLabels: {
color: '#c9d1d9',
font: {
size: 11
}
}
}
}
}
});
}
/**
* Update token type distribution chart
* @param {Object} data - Chart data
*/
updateTokenTypeChart(data) {
const canvas = this.container.querySelector('#tokenTypeChart');
if (!canvas) {
console.warn('Token type chart canvas not found');
return;
}
const existingChart = Chart.getChart(canvas);
if (existingChart) existingChart.destroy();
const ctx = canvas.getContext('2d');
const tokenData = data.detailedTokenUsage || {};
console.log('Token type chart data:', tokenData);
const chartData = [
tokenData.inputTokens || 0,
tokenData.outputTokens || 0,
tokenData.cacheCreationTokens || 0,
tokenData.cacheReadTokens || 0
];
const totalTokens = chartData.reduce((sum, val) => sum + val, 0);
if (totalTokens === 0) {
// Show "no data" message
ctx.fillStyle = '#7d8590';
ctx.textAlign = 'center';
ctx.font = '14px Monaco, monospace';
ctx.fillText('No token data available', canvas.width / 2, canvas.height / 2);
return;
}
new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Input Tokens', 'Output Tokens', 'Cache Creation', 'Cache Read'],
datasets: [{
data: chartData,
backgroundColor: ['#3fb950', '#58a6ff', '#f97316', '#d57455'],
borderColor: '#0d1117',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#c9d1d9',
font: { size: 11 }
}
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.label;
const value = context.parsed;
const total = context.dataset.data.reduce((sum, val) => sum + val, 0);
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
return `${label}: ${value.toLocaleString()} tokens (${percentage}%)`;
}
}
}
}
}
});
}
/**
* Update token usage over time chart
* @param {Object} data - Chart data
*/
updateTokenTimelineChart(data) {
const canvas = this.container.querySelector('#tokenTimelineChart');
if (!canvas) {
console.warn('Token timeline chart canvas not found');
return;
}
const existingChart = Chart.getChart(canvas);
if (existingChart) existingChart.destroy();
const ctx = canvas.getContext('2d');
const conversations = data.conversations || [];
if (conversations.length === 0) {
// Show "no data" message
ctx.fillStyle = '#7d8590';
ctx.textAlign = 'center';
ctx.font = '14px Monaco, monospace';
ctx.fillText('No token timeline data', canvas.width / 2, canvas.height / 2);
return;
}
// Calculate daily token usage
const dailyTokens = this.calculateDailyTokenUsage(conversations);
new Chart(ctx, {
type: 'line',
data: {
labels: dailyTokens.labels,
datasets: [{
label: 'Input Tokens',
data: dailyTokens.inputTokens,
borderColor: '#3fb950',
backgroundColor: 'rgba(63, 185, 80, 0.1)',
fill: false,
tension: 0.3,
pointBackgroundColor: '#3fb950',
pointBorderColor: '#ffffff',
pointBorderWidth: 2,
pointRadius: 3,
pointHoverRadius: 5
}, {
label: 'Output Tokens',
data: dailyTokens.outputTokens,
borderColor: '#58a6ff',
backgroundColor: 'rgba(88, 166, 255, 0.1)',
fill: false,
tension: 0.3,
pointBackgroundColor: '#58a6ff',
pointBorderColor: '#ffffff',
pointBorderWidth: 2,
pointRadius: 3,
pointHoverRadius: 5
}, {
label: 'Cache Usage',
data: dailyTokens.cacheTokens,
borderColor: '#f97316',
backgroundColor: 'rgba(249, 115, 22, 0.1)',
fill: false,
tension: 0.3,
pointBackgroundColor: '#f97316',
pointBorderColor: '#ffffff',
pointBorderWidth: 2,
pointRadius: 3,
pointHoverRadius: 5
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#c9d1d9',
font: { size: 11 },
padding: 15,
usePointStyle: true
}
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.dataset.label;
const value = context.parsed.y;
return `${label}: ${value.toLocaleString()} tokens`;
}
}
}
},
scales: {
x: {
ticks: { color: '#7d8590' },
grid: { color: '#30363d' }
},
y: {
beginAtZero: true,
ticks: {
color: '#7d8590',
callback: function(value) {
return value.toLocaleString();
}
},
grid: { color: '#30363d' }
}
},
interaction: {
intersect: false,
mode: 'index'
}
}
});
}
/**
* Update daily productivity trends chart
* @param {Object} data - Chart data
*/
updateProductivityChart(data) {
const canvas = this.container.querySelector('#productivityChart');
if (!canvas) return;
const existingChart = Chart.getChart(canvas);
if (existingChart) existingChart.destroy();
const ctx = canvas.getContext('2d');
// Calculate productivity metrics by day
const dailyData = this.calculateDailyProductivity(data);
new Chart(ctx, {
type: 'line',
data: {
labels: dailyData.labels,
datasets: [{
label: 'Messages per Day',
data: dailyData.messages,
borderColor: '#3fb950',
backgroundColor: 'rgba(63, 185, 80, 0.1)',
fill: true,
tension: 0.3
}, {
label: 'Tokens per Day',
data: dailyData.tokens,
borderColor: '#58a6ff',
backgroundColor: 'rgba(88, 166, 255, 0.1)',
fill: true,
tension: 0.3,
yAxisID: 'y1'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: { color: '#c9d1d9', font: { size: 11 } }
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.dataset.label;
const value = context.parsed.y;
if (label === 'Messages per Day') {
return `${label}: ${value} messages`;
} else {
return `${label}: ${value.toLocaleString()} tokens`;
}
}
}
}
},
scales: {
x: {
ticks: { color: '#7d8590' },
grid: { color: '#30363d' }
},
y: {
type: 'linear',
display: true,
position: 'left',
ticks: { color: '#7d8590' },
grid: { color: '#30363d' }
},
y1: {
type: 'linear',
display: true,
position: 'right',
ticks: { color: '#7d8590' },
grid: { drawOnChartArea: false }
}
},
interaction: {
intersect: false,
mode: 'index'
}
}
});
}
/**
* Calculate daily productivity metrics
* @param {Object} data - Raw data
* @returns {Object} Processed daily data
*/
/**
* Calculate daily token usage from conversations
* @param {Array} conversations - Array of conversation objects
* @returns {Object} Daily token data
*/
calculateDailyTokenUsage(conversations) {
const dailyData = {};
const { fromDate, toDate } = this.getDateRange();
conversations.forEach(conv => {
const convDate = new Date(conv.lastModified);
if (convDate >= fromDate && convDate <= toDate) {
const dateKey = convDate.toISOString().split('T')[0]; // YYYY-MM-DD
if (!dailyData[dateKey]) {
dailyData[dateKey] = {
inputTokens: 0,
outputTokens: 0,
cacheTokens: 0
};
}
if (conv.tokenUsage) {
dailyData[dateKey].inputTokens += conv.tokenUsage.inputTokens || 0;
dailyData[dateKey].outputTokens += conv.tokenUsage.outputTokens || 0;
dailyData[dateKey].cacheTokens += (conv.tokenUsage.cacheCreationTokens || 0) + (conv.tokenUsage.cacheReadTokens || 0);
}
}
});
// Sort dates and create arrays
const sortedDates = Object.keys(dailyData).sort();
const labels = sortedDates.map(date => new Date(date).toLocaleDateString());
const inputTokens = sortedDates.map(date => dailyData[date].inputTokens);
const outputTokens = sortedDates.map(date => dailyData[date].outputTokens);
const cacheTokens = sortedDates.map(date => dailyData[date].cacheTokens);
return { labels, inputTokens, outputTokens, cacheTokens };
}
calculateDailyProductivity(data) {
const conversations = data.conversations || [];
const dailyStats = {};
// Group data by day
conversations.forEach(conv => {
if (!conv.lastModified) return;
const date = new Date(conv.lastModified).toDateString();
if (!dailyStats[date]) {
dailyStats[date] = { messages: 0, tokens: 0 };
}
dailyStats[date].messages += conv.messageCount || 0;
dailyStats[date].tokens += (conv.tokenUsage?.inputTokens || 0) + (conv.tokenUsage?.outputTokens || 0);
});
// Convert to arrays for chart
const sortedDates = Object.keys(dailyStats).sort((a, b) => new Date(a) - new Date(b));
return {
labels: sortedDates.map(date => new Date(date).toLocaleDateString()),
messages: sortedDates.map(date => dailyStats[date].messages),
tokens: sortedDates.map(date => Math.round(dailyStats[date].tokens / 1000)) // Convert to K tokens
};
}
/**
* Update usage chart
* @param {string} period - Time period
@@ -169,7 +169,6 @@ class SessionTimer {
}
// Calculate progress colors based on usage
const progressPercentage = Math.round(timer.sessionProgress);
const timeProgressPercentage = Math.round(((this.SESSION_DURATION - timer.timeRemaining) / this.SESSION_DURATION) * 100);
const getProgressColor = (percentage) => {
@@ -178,7 +177,10 @@ class SessionTimer {
return '#f85149';
};
const messageProgressColor = getProgressColor(progressPercentage);
// For messages, use a relative progress based on typical usage patterns
// Since Claude uses dynamic limits, we'll show relative activity level
const messageActivityLevel = Math.min(100, (timer.messagesUsed / (timer.messagesEstimate || 45)) * 100);
const messageProgressColor = timer.messagesEstimate ? getProgressColor(messageActivityLevel) : '#3fb950';
const timeProgressColor = getProgressColor(timeProgressPercentage);
// Format time remaining with better UX
@@ -213,7 +215,7 @@ class SessionTimer {
<div class="session-timer-progress-header">
<span class="session-timer-progress-label">Messages</span>
<span class="session-timer-progress-value">
${timer.messagesUsed}/${timer.messagesLimit}
${timer.messagesUsed}${timer.messagesEstimate ? `/${timer.messagesEstimate} est.` : ''}
<span class="session-timer-info-icon" data-tooltip="message-info" title="Message calculation info">
ℹ️
</span>
@@ -221,7 +223,7 @@ class SessionTimer {
</div>
<div class="session-timer-progress-bar">
<div class="session-timer-progress-fill"
style="width: ${progressPercentage}%; background-color: ${messageProgressColor};"></div>
style="width: ${messageActivityLevel}%; background-color: ${messageProgressColor};"></div>
</div>
${timer.usageDetails && timer.usageDetails.shortMessages > 0 ? `
<div class="session-timer-usage-details">
@@ -278,9 +280,10 @@ class SessionTimer {
const popoverHTML = `
<div class="session-timer-tooltip" id="message-info-tooltip" style="display: ${this.isTooltipVisible ? 'block' : 'none'};">
<div class="session-timer-tooltip-content">
<h4>Message Count Calculation</h4>
<p>This count includes only user messages (your prompts) within the current 5-hour session window. Assistant responses are not counted toward usage limits.</p>
<p>The actual limit varies based on message length, conversation context, and current system capacity. The displayed limit is an estimate for typical usage.</p>
<h4>Claude Pro Plan Usage</h4>
<p>Shows user messages (prompts) sent in this session. Claude Pro doesn't have fixed message limits - usage is based on message complexity, conversation length, and current capacity.</p>
<p>The "45 est." is a rough estimate for typical short messages (~200 sentences). You may send more or fewer messages depending on complexity.</p>
<p><strong>Projects benefit from caching</strong> - repeated content uses fewer resources, allowing more messages.</p>
<div class="session-timer-tooltip-link">
<a href="https://support.anthropic.com/en/articles/9797557-usage-limit-best-practices" target="_blank" rel="noopener noreferrer">
<i class="fas fa-external-link-alt"></i> Usage Limit Best Practices
@@ -416,11 +419,11 @@ class SessionTimer {
}
/**
* Get message limit based on current plan
* Get message estimate based on current plan
*/
getMessageLimit() {
if (!this.sessionData || !this.sessionData.limits) return 225;
return this.sessionData.limits.messagesPerSession;
getMessageEstimate() {
if (!this.sessionData || !this.sessionData.limits) return 45;
return this.sessionData.limits.estimatedMessagesPerSession || 45;
}
/**
+667 -66
View File
@@ -646,8 +646,8 @@
}
.session-timer-accordion {
background: #21262d;
border: 1px solid #30363d;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 6px;
margin-bottom: 15px;
position: relative;
@@ -659,12 +659,12 @@
align-items: center;
padding: 12px 16px;
cursor: pointer;
border-bottom: 1px solid #30363d;
border-bottom: 1px solid var(--border-primary);
transition: background-color 0.2s ease;
}
.session-timer-header:hover {
background: #30363d;
background: var(--bg-tertiary);
}
.session-timer-title-section {
@@ -675,12 +675,12 @@
.session-timer-chevron {
font-size: 0.75rem;
color: #7d8590;
color: var(--text-secondary);
transition: transform 0.2s ease;
}
.session-timer-title {
color: #d57455;
color: var(--text-accent);
font-size: 0.9rem;
font-weight: 500;
margin: 0;
@@ -713,7 +713,7 @@
}
.session-timer-status-text {
color: #c9d1d9;
color: var(--text-primary);
}
.session-timer-content {
@@ -750,13 +750,13 @@
}
.session-timer-empty-text {
color: #c9d1d9;
color: var(--text-primary);
font-size: 0.9rem;
margin-bottom: 4px;
}
.session-timer-empty-subtext {
color: #7d8590;
color: var(--text-secondary);
font-size: 0.8rem;
}
@@ -780,13 +780,13 @@
.session-timer-time-value {
font-size: 1.5rem;
font-weight: 700;
color: #d57455;
color: var(--text-accent);
line-height: 1;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
.session-timer-time-label {
color: #7d8590;
color: var(--text-secondary);
font-size: 0.75rem;
margin-top: 2px;
}
@@ -811,13 +811,13 @@
}
.session-timer-progress-label {
color: #c9d1d9;
color: var(--text-primary);
font-size: 0.75rem;
font-weight: 500;
}
.session-timer-progress-value {
color: #7d8590;
color: var(--text-secondary);
font-size: 0.75rem;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
display: inline-flex;
@@ -827,7 +827,7 @@
.session-timer-progress-bar {
width: 100%;
height: 4px;
background: #30363d;
background: var(--border-primary);
border-radius: 2px;
overflow: hidden;
position: relative;
@@ -835,7 +835,7 @@
.session-timer-progress-fill {
height: 100%;
background: #3fb950;
background: var(--text-success);
transition: width 0.3s ease, background-color 0.3s ease;
border-radius: 2px;
}
@@ -844,7 +844,7 @@
display: flex;
gap: 12px;
padding-top: 8px;
border-top: 1px solid #30363d;
border-top: 1px solid var(--border-primary);
flex-wrap: wrap;
}
@@ -857,13 +857,13 @@
}
.session-timer-stat-label {
color: #7d8590;
color: var(--text-secondary);
font-size: 0.75rem;
white-space: nowrap;
}
.session-timer-stat-value {
color: #c9d1d9;
color: var(--text-primary);
font-weight: 500;
font-size: 0.75rem;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
@@ -875,7 +875,7 @@
.session-timer-error {
text-align: center;
padding: 20px;
color: #ffa198;
color: var(--text-error);
}
.session-timer-error-text {
@@ -884,9 +884,9 @@
}
.session-timer-retry-btn {
background: #f85149;
border: 1px solid #f85149;
color: #ffffff;
background: var(--text-error);
border: 1px solid var(--text-error);
color: var(--bg-primary);
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
@@ -895,8 +895,7 @@
}
.session-timer-retry-btn:hover {
background: #da3633;
border-color: #da3633;
opacity: 0.8;
}
.session-warnings {
@@ -946,7 +945,7 @@
}
.session-timer-info-icon {
color: #7d8590;
color: var(--text-secondary);
font-size: 0.7rem;
cursor: pointer;
transition: all 0.2s ease;
@@ -961,7 +960,7 @@
}
.session-timer-info-icon:hover {
color: #58a6ff;
color: var(--text-info);
opacity: 1;
background-color: rgba(88, 166, 255, 0.1);
}
@@ -973,8 +972,8 @@
.session-timer-tooltip {
position: absolute;
z-index: 1000;
background: #161b22;
border: 1px solid #30363d;
background: var(--bg-primary);
border: 1px solid var(--border-primary);
border-radius: 6px;
padding: 0;
box-shadow: 0 16px 32px rgba(1, 4, 9, 0.85);
@@ -989,14 +988,14 @@
.session-timer-tooltip-content h4 {
margin: 0 0 8px 0;
color: #f0f6fc;
color: var(--text-primary);
font-size: 0.8rem;
font-weight: 600;
}
.session-timer-tooltip-content p {
margin: 0 0 8px 0;
color: #c9d1d9;
color: var(--text-primary);
line-height: 1.5;
}
@@ -1005,12 +1004,12 @@
}
.session-timer-tooltip-link {
border-top: 1px solid #30363d;
border-top: 1px solid var(--border-primary);
padding-top: 8px;
}
.session-timer-tooltip-link a {
color: #58a6ff;
color: var(--text-info);
text-decoration: none;
display: inline-flex;
align-items: center;
@@ -1019,7 +1018,7 @@
}
.session-timer-tooltip-link a:hover {
color: #79c0ff;
opacity: 0.8;
}
.session-timer-tooltip-link i {
@@ -3056,7 +3055,7 @@
/* Metrics Cards */
.metrics-cards-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0 30px 0;
}
@@ -3067,35 +3066,27 @@
border-radius: 8px;
padding: 20px;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
gap: 16px;
min-height: 220px;
max-height: 260px;
overflow: hidden;
}
.metric-card:hover {
border-color: var(--text-accent);
background: var(--bg-tertiary);
}
.metric-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.metric-icon {
font-size: 1.25rem;
opacity: 0.8;
}
.metric-title {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
transform: translateY(-2px);
box-shadow: 0 8px 16px var(--shadow-primary);
}
.metric-primary {
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
flex-shrink: 0;
}
.metric-primary-value {
@@ -3103,13 +3094,16 @@
font-size: 2rem;
font-weight: bold;
color: var(--text-accent);
line-height: 1.2;
margin-bottom: 4px;
line-height: 1;
margin-bottom: 6px;
word-break: break-all;
overflow-wrap: break-word;
}
.metric-primary-label {
font-size: 0.75rem;
font-size: 0.9rem;
color: var(--text-secondary);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
@@ -3117,7 +3111,12 @@
.metric-secondary {
display: flex;
flex-direction: column;
gap: 8px;
gap: 6px;
flex: 1;
overflow-y: auto;
padding: 14px 0 0 0;
border-top: 1px solid var(--border-primary);
min-height: 130px;
}
.metric-secondary-item {
@@ -3125,7 +3124,7 @@
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid var(--border-primary);
min-height: 28px;
}
.metric-secondary-item:last-child {
@@ -3133,17 +3132,31 @@
}
.metric-secondary-label {
font-size: 0.8rem;
font-size: 0.85rem;
color: var(--text-secondary);
flex-shrink: 0;
margin-right: 12px;
}
.metric-secondary-value {
font-size: 0.85rem;
font-size: 0.9rem;
font-weight: 600;
color: var(--text-primary);
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 90%;
}
/* Responsive design for metrics cards */
@media (max-width: 1024px) {
.metrics-cards-container {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
}
@media (max-width: 768px) {
.metrics-cards-container {
grid-template-columns: 1fr;
@@ -3151,25 +3164,45 @@
}
.metric-card {
padding: 16px;
padding: 18px;
min-height: 200px;
max-height: 240px;
gap: 14px;
}
.metric-primary-value {
font-size: 1.75rem;
font-size: 1.8rem;
}
}
@media (max-width: 480px) {
.metric-card {
padding: 16px;
min-height: 180px;
max-height: 220px;
gap: 12px;
}
.metric-primary-value {
font-size: 1.5rem;
font-size: 1.6rem;
}
.metric-secondary {
gap: 6px;
gap: 4px;
}
.metric-secondary-item {
padding: 4px 0;
min-height: 28px;
}
.metric-secondary-label {
font-size: 0.75rem;
}
.metric-secondary-value {
font-size: 0.8rem;
max-width: 100%;
}
}
@@ -6513,6 +6546,574 @@
border-radius: 4px;
}
/* Agent Analytics Styles */
.agent-analytics {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.analytics-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 30px;
gap: 20px;
}
.header-content {
flex: 1;
}
.analytics-title {
color: var(--text-accent);
font-size: 1.5rem;
font-weight: normal;
margin: 0 0 8px 0;
display: flex;
align-items: center;
gap: 12px;
}
.title-icon {
font-size: 1.4em;
}
.analytics-subtitle {
color: var(--text-secondary);
margin: 0;
font-size: 0.9rem;
}
.date-filters {
display: flex;
align-items: center;
gap: 12px;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 8px;
padding: 12px 16px;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.filter-group label {
font-size: 0.8rem;
color: var(--text-secondary);
font-weight: 600;
}
.date-input {
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: 4px;
padding: 6px 8px;
color: var(--text-primary);
font-family: inherit;
font-size: 0.85rem;
}
.date-input:focus {
outline: none;
border-color: var(--text-accent);
}
.refresh-btn {
background: var(--text-accent);
border: none;
border-radius: 4px;
padding: 8px 12px;
color: white;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-family: inherit;
font-size: 0.85rem;
font-weight: 600;
transition: all 0.2s ease;
}
.refresh-btn:hover {
background: var(--text-warning);
transform: translateY(-1px);
}
.refresh-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-icon {
font-size: 0.9em;
}
/* Metrics Grid */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 30px;
}
.metric-card {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 8px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
transition: all 0.2s ease;
}
.metric-card:hover {
border-color: var(--text-accent);
box-shadow: 0 4px 12px var(--shadow-primary);
}
.metric-card.primary {
border-color: var(--text-accent);
box-shadow: 0 0 0 1px rgba(213, 116, 85, 0.1);
}
.metric-card.secondary {
border-color: var(--text-info);
box-shadow: 0 0 0 1px rgba(165, 214, 255, 0.1);
}
.metric-card.success {
border-color: var(--text-success);
box-shadow: 0 0 0 1px rgba(63, 185, 80, 0.1);
}
.metric-card.info {
border-color: var(--text-warning);
box-shadow: 0 0 0 1px rgba(249, 115, 22, 0.1);
}
.metric-icon {
font-size: 2em;
opacity: 0.8;
}
.metric-content {
flex: 1;
}
.metric-value {
font-size: 1.8rem;
font-weight: bold;
color: var(--text-primary);
margin: 0 0 4px 0;
}
.metric-label {
font-size: 0.85rem;
color: var(--text-secondary);
font-weight: 500;
}
/* Charts Container - Sectioned Layout */
.charts-container {
display: flex;
flex-direction: column;
gap: 40px;
margin-bottom: 30px;
}
.chart-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.section-header {
border-bottom: 2px solid var(--border-primary);
padding-bottom: 12px;
}
.section-title {
color: var(--text-accent);
font-size: 1.1rem;
font-weight: 700;
margin: 0 0 6px 0;
display: flex;
align-items: center;
gap: 8px;
}
.section-description {
color: var(--text-secondary);
font-size: 0.85rem;
margin: 0;
font-weight: 400;
}
.section-charts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 20px;
}
.chart-card {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 8px;
padding: 20px;
transition: all 0.2s ease;
}
.chart-card:hover {
border-color: var(--text-accent);
box-shadow: 0 4px 12px var(--shadow-primary);
}
.chart-header {
margin-bottom: 20px;
}
.chart-header h3 {
color: var(--text-primary);
font-size: 1.1rem;
margin: 0 0 4px 0;
font-weight: 600;
}
.chart-header p {
color: var(--text-secondary);
font-size: 0.85rem;
margin: 0;
}
.chart-content {
position: relative;
height: 300px;
}
.chart-content canvas {
width: 100% !important;
height: 100% !important;
}
/* Efficiency Metrics */
.efficiency-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
}
.efficiency-card {
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: 6px;
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s ease;
}
.efficiency-card:hover {
border-color: var(--text-accent);
}
.efficiency-icon {
font-size: 1.5em;
opacity: 0.8;
}
.efficiency-content {
flex: 1;
}
.efficiency-value {
font-size: 1.2rem;
font-weight: bold;
color: var(--text-primary);
margin: 0 0 2px 0;
}
.efficiency-label {
font-size: 0.75rem;
color: var(--text-secondary);
}
.efficiency-loading {
text-align: center;
padding: 40px;
color: var(--text-secondary);
}
/* Agent Details Section */
.agent-details-section {
margin-bottom: 30px;
}
.agent-details-section h3 {
color: var(--text-primary);
font-size: 1.2rem;
margin: 0 0 16px 0;
font-weight: 600;
}
.agent-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 16px;
}
.agent-stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 8px;
padding: 20px;
transition: all 0.2s ease;
}
.agent-stat-card:hover {
border-color: var(--text-accent);
box-shadow: 0 4px 12px var(--shadow-primary);
}
.agent-stat-header {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 16px;
}
.agent-stat-icon {
font-size: 1.8em;
flex-shrink: 0;
}
.agent-stat-info {
flex: 1;
}
.agent-stat-info h4 {
color: var(--text-primary);
font-size: 1rem;
margin: 0 0 4px 0;
font-weight: 600;
}
.agent-stat-info p {
color: var(--text-secondary);
font-size: 0.85rem;
margin: 0;
}
.agent-stat-badge {
background: var(--bg-tertiary);
border-radius: 12px;
padding: 4px 8px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.agent-stat-metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 8px;
}
.stat-metric {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid var(--border-primary);
}
.stat-metric:last-child {
border-bottom: none;
}
.metric-label {
font-size: 0.8rem;
color: var(--text-secondary);
}
.metric-value {
font-size: 0.8rem;
color: var(--text-primary);
font-weight: 600;
}
.stats-loading {
text-align: center;
padding: 40px;
color: var(--text-secondary);
}
/* Workflow Patterns */
.workflow-patterns-section {
margin-bottom: 30px;
}
.workflow-patterns-section h3 {
color: var(--text-primary);
font-size: 1.2rem;
margin: 0 0 8px 0;
font-weight: 600;
}
.workflow-patterns-section p {
color: var(--text-secondary);
font-size: 0.9rem;
margin: 0 0 16px 0;
}
.workflow-patterns {
display: flex;
flex-direction: column;
gap: 12px;
}
.workflow-pattern {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 6px;
padding: 16px 20px;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.2s ease;
}
.workflow-pattern:hover {
border-color: var(--text-accent);
}
.pattern-flow {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
color: var(--text-primary);
font-size: 0.9rem;
}
.pattern-count {
background: var(--text-accent);
color: white;
border-radius: 12px;
padding: 4px 8px;
font-size: 0.75rem;
font-weight: 600;
}
/* No Data States */
.no-agent-data,
.agent-analytics-error {
text-align: center;
padding: 60px 20px;
}
.no-data-content,
.error-content {
max-width: 500px;
margin: 0 auto;
}
.no-data-icon,
.error-icon {
font-size: 4em;
margin-bottom: 20px;
opacity: 0.6;
}
.no-agent-data h3,
.agent-analytics-error h3 {
color: var(--text-primary);
font-size: 1.3rem;
margin: 0 0 12px 0;
font-weight: 600;
}
.no-agent-data p,
.agent-analytics-error p {
color: var(--text-secondary);
margin: 0 0 20px 0;
line-height: 1.5;
}
.agent-types {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 8px;
padding: 20px;
margin: 20px 0;
text-align: left;
}
.agent-types h4 {
color: var(--text-primary);
font-size: 1rem;
margin: 0 0 12px 0;
font-weight: 600;
}
.agent-types ul {
list-style: none;
padding: 0;
margin: 0;
}
.agent-types li {
padding: 8px 0;
border-bottom: 1px solid var(--border-primary);
}
.agent-types li:last-child {
border-bottom: none;
}
.agent-types strong {
color: var(--text-accent);
font-weight: 600;
}
/* Responsive Design */
@media (max-width: 768px) {
.analytics-header {
flex-direction: column;
gap: 16px;
}
.date-filters {
flex-wrap: wrap;
gap: 8px;
}
.metrics-grid {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
}
.charts-container {
gap: 30px;
}
.section-charts {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.agent-stats-grid {
grid-template-columns: 1fr;
}
.efficiency-grid {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
}
</style>
</head>
<body>
+112 -10
View File
@@ -10,6 +10,7 @@ const ProcessDetector = require('./analytics/core/ProcessDetector');
const ConversationAnalyzer = require('./analytics/core/ConversationAnalyzer');
const FileWatcher = require('./analytics/core/FileWatcher');
const SessionAnalyzer = require('./analytics/core/SessionAnalyzer');
const AgentAnalyzer = require('./analytics/core/AgentAnalyzer');
const DataCache = require('./analytics/data/DataCache');
const WebSocketServer = require('./analytics/notifications/WebSocketServer');
const NotificationManager = require('./analytics/notifications/NotificationManager');
@@ -24,6 +25,7 @@ class ClaudeAnalytics {
this.processDetector = new ProcessDetector();
this.fileWatcher = new FileWatcher();
this.sessionAnalyzer = new SessionAnalyzer();
this.agentAnalyzer = new AgentAnalyzer();
this.dataCache = new DataCache();
this.performanceMonitor = new PerformanceMonitor({
enabled: true,
@@ -667,6 +669,28 @@ class ClaudeAnalytics {
}
});
// Agent usage analytics endpoint
this.app.get('/api/agents', async (req, res) => {
try {
const startDate = req.query.startDate;
const endDate = req.query.endDate;
const dateRange = (startDate || endDate) ? { startDate, endDate } : null;
const agentAnalysis = await this.agentAnalyzer.analyzeAgentUsage(this.data.conversations, dateRange);
const agentSummary = this.agentAnalyzer.generateSummary(agentAnalysis);
res.json({
...agentAnalysis,
summary: agentSummary,
dateRange,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Error getting agent analytics:', error);
res.status(500).json({ error: 'Failed to get agent analytics' });
}
});
this.app.get('/api/realtime', async (req, res) => {
const realtimeWithTimestamp = {
...this.data.realtimeStats,
@@ -1661,12 +1685,88 @@ class ClaudeAnalytics {
const timeSinceLastUpdate = now - lastUpdate;
const timeSinceLastUpdateMinutes = Math.floor(timeSinceLastUpdate / (1000 * 60));
// Based on observed pattern: ~2 hours and 21 minutes session limit
const sessionLimitMs = 2 * 60 * 60 * 1000 + 21 * 60 * 1000; // 2h 21m
const timeRemaining = sessionLimitMs - sessionDuration;
const timeRemainingMinutes = Math.floor(timeRemaining / (1000 * 60));
const timeRemainingHours = Math.floor(timeRemainingMinutes / 60);
const remainingMinutesDisplay = timeRemainingMinutes % 60;
// CORRECTED: Calculate next reset time based on scheduled reset hours
// Claude sessions reset at specific times, not fixed durations
const resetHours = [1, 7, 13, 19]; // 1am, 7am, 1pm, 7pm local time
const sessionStartDate = new Date(startTime);
// Find next reset time after session start
let nextResetTime = new Date(sessionStartDate);
nextResetTime.setMinutes(0, 0, 0); // Set to exact hour
// Find the next reset hour
let foundReset = false;
for (let i = 0; i < resetHours.length * 2; i++) { // Check up to 2 full days
const currentDay = Math.floor(i / resetHours.length);
const hourIndex = i % resetHours.length;
const resetHour = resetHours[hourIndex];
const testResetTime = new Date(sessionStartDate);
testResetTime.setDate(testResetTime.getDate() + currentDay);
testResetTime.setHours(resetHour, 0, 0, 0);
if (testResetTime > sessionStartDate) {
nextResetTime = testResetTime;
foundReset = true;
break;
}
}
if (!foundReset) {
// Fallback: assume next day 7am if no pattern found
nextResetTime.setDate(nextResetTime.getDate() + 1);
nextResetTime.setHours(7, 0, 0, 0);
}
const sessionLimitMs = nextResetTime.getTime() - startTime;
let timeRemaining = nextResetTime.getTime() - now;
let timeRemainingMinutes = Math.floor(timeRemaining / (1000 * 60));
let timeRemainingHours = Math.floor(timeRemainingMinutes / 60);
let remainingMinutesDisplay = timeRemainingMinutes % 60;
// If session is expired but has recent activity, calculate next reset time
if (timeRemaining <= 0) {
const RECENT_ACTIVITY_THRESHOLD = 5 * 60 * 1000; // 5 minutes
const timeSinceLastUpdate = now - lastUpdate;
if (timeSinceLastUpdate < RECENT_ACTIVITY_THRESHOLD) {
// Session was renewed, find the NEXT reset time
let nextNextResetTime = new Date(nextResetTime);
let foundNextReset = false;
for (let i = 0; i < resetHours.length; i++) {
const resetHour = resetHours[i];
const testResetTime = new Date(nextResetTime);
if (resetHour > nextResetTime.getHours()) {
// Same day, later hour
testResetTime.setHours(resetHour, 0, 0, 0);
} else {
// Next day
testResetTime.setDate(testResetTime.getDate() + 1);
testResetTime.setHours(resetHour, 0, 0, 0);
}
if (testResetTime > nextResetTime) {
nextNextResetTime = testResetTime;
foundNextReset = true;
break;
}
}
if (!foundNextReset) {
// Default to next day 1am
nextNextResetTime.setDate(nextNextResetTime.getDate() + 1);
nextNextResetTime.setHours(1, 0, 0, 0);
}
timeRemaining = nextNextResetTime.getTime() - now;
timeRemainingMinutes = Math.floor(timeRemaining / (1000 * 60));
timeRemainingHours = Math.floor(timeRemainingMinutes / 60);
remainingMinutesDisplay = timeRemainingMinutes % 60;
nextResetTime = nextNextResetTime;
}
}
return {
hasSession: true,
@@ -1691,13 +1791,15 @@ class ClaudeAnalytics {
hours: timeRemainingHours,
remainingMinutes: remainingMinutesDisplay,
formatted: timeRemaining > 0 ? `${timeRemainingHours}h ${remainingMinutesDisplay}m` : 'Session expired',
isExpired: timeRemaining <= 0
isExpired: timeRemaining <= 0 && timeSinceLastUpdate >= (5 * 60 * 1000) // Expired only if past reset time AND no recent activity
},
sessionLimit: {
ms: sessionLimitMs,
hours: 2,
minutes: 21,
formatted: '2h 21m'
hours: Math.floor(sessionLimitMs / (1000 * 60 * 60)),
minutes: Math.floor((sessionLimitMs % (1000 * 60 * 60)) / (1000 * 60)),
formatted: `${Math.floor(sessionLimitMs / (1000 * 60 * 60))}h ${Math.floor((sessionLimitMs % (1000 * 60 * 60)) / (1000 * 60))}m`,
nextResetTime: nextResetTime.toISOString(),
resetHour: nextResetTime.getHours()
}
};
} catch (error) {
@@ -0,0 +1,341 @@
/**
* AgentAnalyzer - Analyzes Claude Code specialized agent usage patterns
* Extracts agent invocation data, usage frequency, and workflow patterns
*/
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('path');
class AgentAnalyzer {
constructor() {
// Known Claude Code specialized agents
this.AGENT_TYPES = {
'general-purpose': {
name: 'General Purpose',
description: 'Multi-step tasks and research',
color: '#3fb950',
icon: '🔧'
},
'claude-code-best-practices': {
name: 'Claude Code Best Practices',
description: 'Workflow optimization and setup guidance',
color: '#f97316',
icon: '⚡'
},
'docusaurus-expert': {
name: 'Docusaurus Expert',
description: 'Documentation site management',
color: '#0969da',
icon: '📚'
}
};
}
/**
* Analyze agent usage across all conversations
* @param {Array} conversations - Array of conversation objects with parsed messages
* @param {Object} dateRange - Optional date range filter
* @returns {Object} Agent usage analysis
*/
async analyzeAgentUsage(conversations, dateRange = null) {
const agentStats = {};
const agentTimeline = [];
const agentWorkflows = {};
let totalAgentInvocations = 0;
for (const conversation of conversations) {
// Parse messages from JSONL file if not already parsed
let messages = conversation.parsedMessages;
if (!messages && conversation.filePath) {
messages = await this.parseJsonlFile(conversation.filePath);
}
if (!messages) continue;
messages.forEach(message => {
// Skip if outside date range
if (dateRange && !this.isWithinDateRange(message.timestamp, dateRange)) {
return;
}
// Look for Task tool usage with subagent_type
// Handle both direct message structure and nested message structure
const messageContent = message.message ? message.message.content : message.content;
const messageRole = message.message ? message.message.role : message.role;
if (messageRole === 'assistant' &&
messageContent &&
Array.isArray(messageContent)) {
messageContent.forEach(content => {
if (content.type === 'tool_use' &&
content.name === 'Task' &&
content.input &&
content.input.subagent_type) {
const agentType = content.input.subagent_type;
const timestamp = new Date(message.timestamp);
const prompt = content.input.prompt || content.input.description || 'No description';
// Initialize agent stats
if (!agentStats[agentType]) {
agentStats[agentType] = {
type: agentType,
name: this.AGENT_TYPES[agentType]?.name || agentType,
description: this.AGENT_TYPES[agentType]?.description || 'Custom agent',
color: this.AGENT_TYPES[agentType]?.color || '#8b5cf6',
icon: this.AGENT_TYPES[agentType]?.icon || '🤖',
totalInvocations: 0,
uniqueConversations: new Set(),
firstUsed: timestamp,
lastUsed: timestamp,
prompts: [],
hourlyDistribution: new Array(24).fill(0),
dailyUsage: {}
};
}
const stats = agentStats[agentType];
// Update stats
stats.totalInvocations++;
stats.uniqueConversations.add(conversation.id);
stats.lastUsed = new Date(Math.max(stats.lastUsed, timestamp));
stats.firstUsed = new Date(Math.min(stats.firstUsed, timestamp));
// Store prompt for analysis
stats.prompts.push({
text: prompt,
timestamp: timestamp,
conversationId: conversation.id
});
// Track hourly distribution
const hour = timestamp.getHours();
stats.hourlyDistribution[hour]++;
// Track daily usage
const dateKey = timestamp.toISOString().split('T')[0];
stats.dailyUsage[dateKey] = (stats.dailyUsage[dateKey] || 0) + 1;
// Add to timeline
agentTimeline.push({
timestamp: timestamp,
agentType: agentType,
agentName: stats.name,
prompt: prompt.substring(0, 100) + (prompt.length > 100 ? '...' : ''),
conversationId: conversation.id,
color: stats.color,
icon: stats.icon
});
totalAgentInvocations++;
}
});
}
});
}
// Convert Sets to counts and finalize stats
Object.keys(agentStats).forEach(agentType => {
const stats = agentStats[agentType];
stats.uniqueConversations = stats.uniqueConversations.size;
stats.averageUsagePerConversation = stats.uniqueConversations > 0 ?
(stats.totalInvocations / stats.uniqueConversations).toFixed(1) : 0;
});
// Sort timeline by timestamp
agentTimeline.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
// Calculate agent workflows (sequences of agent usage)
const workflowPatterns = this.analyzeAgentWorkflows(agentTimeline);
return {
totalAgentInvocations,
totalAgentTypes: Object.keys(agentStats).length,
agentStats: Object.values(agentStats).sort((a, b) => b.totalInvocations - a.totalInvocations),
agentTimeline,
workflowPatterns,
popularHours: this.calculatePopularHours(agentStats),
usageByDay: this.calculateDailyUsage(agentStats),
efficiency: this.calculateAgentEfficiency(agentStats)
};
}
/**
* Analyze agent workflow patterns
* @param {Array} timeline - Chronological agent invocations
* @returns {Object} Workflow patterns
*/
analyzeAgentWorkflows(timeline) {
const workflows = {};
const SESSION_GAP_MINUTES = 30; // Minutes between workflow sessions
let currentWorkflow = [];
let lastTimestamp = null;
timeline.forEach(event => {
const currentTime = new Date(event.timestamp);
// Start new workflow if gap is too large or first event
if (!lastTimestamp ||
(currentTime - lastTimestamp) > (SESSION_GAP_MINUTES * 60 * 1000)) {
// Save previous workflow if it had multiple agents
if (currentWorkflow.length > 1) {
const workflowKey = currentWorkflow.map(e => e.agentType).join(' → ');
workflows[workflowKey] = (workflows[workflowKey] || 0) + 1;
}
currentWorkflow = [event];
} else {
currentWorkflow.push(event);
}
lastTimestamp = currentTime;
});
// Don't forget the last workflow
if (currentWorkflow.length > 1) {
const workflowKey = currentWorkflow.map(e => e.agentType).join(' → ');
workflows[workflowKey] = (workflows[workflowKey] || 0) + 1;
}
// Convert to sorted array
return Object.entries(workflows)
.map(([pattern, count]) => ({ pattern, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10); // Top 10 workflows
}
/**
* Calculate popular usage hours across all agents
* @param {Object} agentStats - Agent statistics
* @returns {Array} Hour usage data
*/
calculatePopularHours(agentStats) {
const hourlyTotals = new Array(24).fill(0);
Object.values(agentStats).forEach(stats => {
stats.hourlyDistribution.forEach((count, hour) => {
hourlyTotals[hour] += count;
});
});
return hourlyTotals.map((count, hour) => ({
hour,
count,
label: `${hour.toString().padStart(2, '0')}:00`
}));
}
/**
* Calculate daily usage across all agents
* @param {Object} agentStats - Agent statistics
* @returns {Array} Daily usage data
*/
calculateDailyUsage(agentStats) {
const dailyTotals = {};
Object.values(agentStats).forEach(stats => {
Object.entries(stats.dailyUsage).forEach(([date, count]) => {
dailyTotals[date] = (dailyTotals[date] || 0) + count;
});
});
return Object.entries(dailyTotals)
.map(([date, count]) => ({
date,
count,
timestamp: new Date(date)
}))
.sort((a, b) => a.timestamp - b.timestamp);
}
/**
* Calculate agent efficiency metrics
* @param {Object} agentStats - Agent statistics
* @returns {Object} Efficiency metrics
*/
calculateAgentEfficiency(agentStats) {
const agents = Object.values(agentStats);
if (agents.length === 0) return {};
const totalInvocations = agents.reduce((sum, agent) => sum + agent.totalInvocations, 0);
const totalConversations = agents.reduce((sum, agent) => sum + agent.uniqueConversations, 0);
return {
averageInvocationsPerAgent: (totalInvocations / agents.length).toFixed(1),
averageConversationsPerAgent: (totalConversations / agents.length).toFixed(1),
mostUsedAgent: agents[0],
agentDiversity: agents.length,
adoptionRate: (agents.filter(a => a.totalInvocations > 1).length / agents.length * 100).toFixed(1)
};
}
/**
* Parse JSONL file to extract messages
* @param {string} filePath - Path to the JSONL file
* @returns {Array} Array of parsed messages
*/
async parseJsonlFile(filePath) {
try {
if (!await fs.pathExists(filePath)) {
return null;
}
const content = await fs.readFile(filePath, 'utf8');
const lines = content.trim().split('\n').filter(line => line.trim());
return lines.map(line => {
try {
return JSON.parse(line);
} catch (error) {
console.warn(`Error parsing JSONL line in ${filePath}:`, error.message);
return null;
}
}).filter(Boolean);
} catch (error) {
console.error(`Error reading JSONL file ${filePath}:`, error);
return null;
}
}
/**
* Check if timestamp is within date range
* @param {string|Date} timestamp - Message timestamp
* @param {Object} dateRange - Date range with startDate and endDate
* @returns {boolean} Whether timestamp is in range
*/
isWithinDateRange(timestamp, dateRange) {
if (!dateRange || (!dateRange.startDate && !dateRange.endDate)) return true;
const messageDate = new Date(timestamp);
const startDate = dateRange.startDate ? new Date(dateRange.startDate) : new Date(0);
const endDate = dateRange.endDate ? new Date(dateRange.endDate) : new Date();
return messageDate >= startDate && messageDate <= endDate;
}
/**
* Generate agent usage summary for display
* @param {Object} analysisResult - Result from analyzeAgentUsage
* @returns {Object} Summary data
*/
generateSummary(analysisResult) {
const { totalAgentInvocations, totalAgentTypes, agentStats, efficiency } = analysisResult;
return {
totalInvocations: totalAgentInvocations,
totalAgentTypes,
topAgent: agentStats[0] || null,
averageUsage: efficiency.averageInvocationsPerAgent,
adoptionRate: efficiency.adoptionRate,
summary: totalAgentInvocations > 0 ?
`${totalAgentInvocations} agent invocations across ${totalAgentTypes} different agents` :
'No agent usage detected'
};
}
}
module.exports = AgentAnalyzer;
+103 -48
View File
@@ -6,34 +6,40 @@ const chalk = require('chalk');
class SessionAnalyzer {
constructor() {
this.SESSION_DURATION = 5 * 60 * 60 * 1000; // 5 hours in milliseconds
// CORRECTED: Sessions don't have fixed duration - they reset at scheduled times
// Reset hours: 1am, 7am, 1pm, 7pm local time
this.RESET_HOURS = [1, 7, 13, 19];
this.MONTHLY_SESSION_LIMIT = 50;
// Plan-specific message limits (conservative estimates)
// Plan-specific usage information (Claude uses complexity-based limits, not fixed message counts)
this.PLAN_LIMITS = {
'free': {
name: 'Free Plan',
messagesPerSession: null,
estimatedMessagesPerSession: null,
monthlyPrice: 0,
hasSessionLimits: false
hasSessionLimits: false,
description: 'Daily usage limits apply'
},
'standard': {
name: 'Pro Plan',
messagesPerSession: 45,
estimatedMessagesPerSession: 45, // Rough estimate for ~200 sentence messages
monthlyPrice: 20,
hasSessionLimits: true
hasSessionLimits: true,
description: 'Usage based on message complexity, conversation length, and current capacity. Limits reset every 5 hours.'
},
'max': {
name: 'Max Plan (5x)',
messagesPerSession: 225,
estimatedMessagesPerSession: null, // 5x more than Pro, but still complexity-based
monthlyPrice: 100,
hasSessionLimits: true
hasSessionLimits: true,
description: '5x the usage of Pro plan. Complexity-based limits.'
},
'premium': {
name: 'Max Plan (5x)',
messagesPerSession: 900,
name: 'Max Plan (20x)',
estimatedMessagesPerSession: null, // 20x more than Pro
monthlyPrice: 200,
hasSessionLimits: true
hasSessionLimits: true,
description: '20x the usage of Pro plan. Complexity-based limits.'
}
};
}
@@ -325,7 +331,7 @@ class SessionAnalyzer {
// Create current session based on Claude's actual session window
const sessionStartTime = new Date(claudeSessionInfo.startTime);
const sessionEndTime = new Date(claudeSessionInfo.startTime + claudeSessionInfo.sessionLimit.ms);
const sessionEndTime = new Date(claudeSessionInfo.sessionLimit.nextResetTime);
const now = new Date();
// Find the first user message that occurred AT OR AFTER the Claude session started
@@ -346,8 +352,50 @@ class SessionAnalyzer {
return msgTime >= effectiveSessionStart && msgTime < sessionEndTime;
});
// If no estimated messages found in session window, check for active conversations by lastModified
if (currentSessionMessages.length === 0) {
return [];
const RECENT_ACTIVITY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
const now = new Date();
// Find conversations with recent activity (lastModified within session timeframe)
const activeConversations = conversations.filter(conversation => {
if (!conversation.lastModified) return false;
const lastModified = new Date(conversation.lastModified);
const timeSinceModified = now - lastModified;
// Consider conversation active if:
// 1. Modified after session start, AND
// 2. Recently modified (within threshold)
return lastModified >= sessionStartTime && timeSinceModified < RECENT_ACTIVITY_THRESHOLD;
});
if (activeConversations.length === 0) {
return [];
}
// Create messages for active conversations based on their real message count
activeConversations.forEach(conversation => {
const lastModified = new Date(conversation.lastModified);
const messageCount = conversation.messageCount || 0;
// Create messages based on real message count, distributing them over the session
const sessionDuration = now - sessionStartTime;
const timePerMessage = sessionDuration / messageCount;
for (let i = 0; i < messageCount; i++) {
// Distribute messages over the session timeline, alternating user/assistant
const messageTime = new Date(sessionStartTime.getTime() + (i * timePerMessage));
const role = i % 2 === 0 ? 'user' : 'assistant';
currentSessionMessages.push({
timestamp: messageTime,
role: role,
conversationId: conversation.id,
usage: conversation.tokenUsage || null
});
}
});
}
// Create the current session object
@@ -409,12 +457,19 @@ class SessionAnalyzer {
getCurrentActiveSessionFromClaudeInfo(sessions, claudeSessionInfo) {
if (sessions.length === 0) return null;
// If Claude session is expired, return null
if (claudeSessionInfo.estimatedTimeRemaining.isExpired) {
return null;
const now = Date.now();
const RECENT_ACTIVITY_THRESHOLD = 5 * 60 * 1000; // 5 minutes
// Check if there's recent activity - sessions can be renewed at reset time
const timeSinceLastUpdate = now - claudeSessionInfo.lastUpdate;
const hasRecentActivity = timeSinceLastUpdate < RECENT_ACTIVITY_THRESHOLD;
// Session is active if not expired OR has recent activity (session was renewed)
if (!claudeSessionInfo.estimatedTimeRemaining.isExpired || hasRecentActivity) {
return sessions[0];
}
return sessions[0]; // Since we only create one session based on Claude's current session
return null;
}
/**
@@ -516,38 +571,33 @@ class SessionAnalyzer {
const warnings = [];
const planLimits = this.PLAN_LIMITS[userPlan.planType] || this.PLAN_LIMITS['standard'];
// Session-level warnings
// Session-level warnings - only for time remaining and token usage
if (currentSession) {
const sessionProgress = currentSession.messageCount / planLimits.messagesPerSession;
if (sessionProgress >= 0.9) {
warnings.push({
type: 'session_limit_critical',
level: 'error',
message: `You're near your session message limit (${currentSession.messageCount}/${planLimits.messagesPerSession})`,
timeRemaining: currentSession.timeRemaining
});
} else if (sessionProgress >= 0.75) {
warnings.push({
type: 'session_limit_warning',
level: 'warning',
message: `75% of session messages used (${currentSession.messageCount}/${planLimits.messagesPerSession})`,
timeRemaining: currentSession.timeRemaining
});
}
// Time remaining warning
// Time remaining warning (30 minutes before reset)
if (currentSession.timeRemaining < 30 * 60 * 1000) { // 30 minutes
warnings.push({
type: 'session_time_warning',
level: 'info',
message: `Session expires in ${Math.round(currentSession.timeRemaining / 60000)} minutes`,
message: `Session resets in ${Math.round(currentSession.timeRemaining / 60000)} minutes`,
timeRemaining: currentSession.timeRemaining
});
}
// High token usage warning (if we have token data and it's exceptionally high)
if (currentSession.tokenUsage && currentSession.tokenUsage.total > 1000000) { // 1M tokens
warnings.push({
type: 'high_token_usage',
level: 'info',
message: `High token usage in this session (${Math.round(currentSession.tokenUsage.total / 1000)}K tokens)`,
tokenUsage: currentSession.tokenUsage.total
});
}
// Note: We don't warn about message counts since Claude uses complexity-based limits
// that can't be accurately predicted from simple message counts
}
// Monthly warnings
// Monthly warnings (these limits are more predictable)
const monthlyProgress = monthlyUsage.sessionCount / this.MONTHLY_SESSION_LIMIT;
if (monthlyProgress >= 0.9) {
@@ -605,25 +655,30 @@ class SessionAnalyzer {
// Ensure limits exist, fallback to standard plan
const planLimits = limits || this.PLAN_LIMITS['standard'];
// Use weighted message calculation for more accurate progress
const weightedProgress = (currentSession.messageWeight / planLimits.messagesPerSession) * 100;
// Calculate only user messages (Claude only counts prompts toward limits)
const userMessages = currentSession.messages ? currentSession.messages.filter(msg => msg.role === 'user') : [];
const userMessageCount = userMessages.length;
return {
hasActiveSession: true,
timeRemaining: currentSession.timeRemaining,
timeRemainingFormatted: this.formatTimeRemaining(currentSession.timeRemaining),
messagesUsed: currentSession.messageCount,
messagesLimit: planLimits.messagesPerSession,
messageWeight: currentSession.messageWeight,
usageDetails: currentSession.usageDetails,
messagesUsed: userMessageCount,
messagesEstimate: planLimits.estimatedMessagesPerSession, // Show as estimate, not limit
tokensUsed: currentSession.tokenUsage.total,
sessionProgress: weightedProgress,
sessionProgressSimple: (currentSession.messageCount / planLimits.messagesPerSession) * 100,
planName: planLimits.name,
planDescription: planLimits.description,
monthlySessionsUsed: monthlyUsage.sessionCount,
monthlySessionsLimit: this.MONTHLY_SESSION_LIMIT,
warnings: warnings.filter(w => w.type.includes('session')),
willResetAt: currentSession.endTime
willResetAt: currentSession.endTime,
// Usage insights
usageInsights: {
tokensPerMessage: userMessageCount > 0 ? Math.round(currentSession.tokenUsage.total / userMessageCount) : 0,
averageMessageComplexity: userMessageCount > 0 ? currentSession.messageWeight / userMessageCount : 0,
conversationLength: currentSession.messages ? currentSession.messages.length : 0,
sessionDuration: Date.now() - currentSession.startTime
}
};
}
}
+19
View File
@@ -28,6 +28,25 @@ const config: Config = {
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
// GitHub Pages deployment configuration
trailingSlash: false,
// Add plugin to create .nojekyll file automatically
plugins: [
function createNojekyllPlugin() {
return {
name: 'create-nojekyll',
async postBuild({ outDir }) {
const fs = require('fs');
const path = require('path');
const nojekyllPath = path.join(outDir, '.nojekyll');
fs.writeFileSync(nojekyllPath, '');
console.log('✅ Created .nojekyll file for GitHub Pages');
},
};
},
],
// Even if you don't use internationalization, you can use this field to set
// useful metadata like html lang. For example, if your site is Chinese, you
// may want to replace "en" with "zh-Hans".