mirror of
https://github.com/davila7/claude-code-templates.git
synced 2026-10-02 03:14:35 +08:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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".
|
||||
|
||||
Reference in New Issue
Block a user