claude-code-templates 1.10.1 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/bin/create-claude-config.js +1 -0
- package/package.json +2 -2
- package/src/analytics/core/ConversationAnalyzer.js +94 -20
- package/src/analytics/core/FileWatcher.js +146 -11
- package/src/analytics/data/DataCache.js +124 -19
- package/src/analytics/notifications/NotificationManager.js +37 -0
- package/src/analytics/notifications/WebSocketServer.js +1 -1
- package/src/analytics-web/FRONT_ARCHITECTURE.md +46 -0
- package/src/analytics-web/assets/js/{main.js → main.js.deprecated} +32 -3
- package/src/analytics-web/components/AgentsPage.js +2535 -0
- package/src/analytics-web/components/App.js +430 -0
- package/src/analytics-web/components/{Dashboard.js → Dashboard.js.deprecated} +23 -7
- package/src/analytics-web/components/DashboardPage.js +1527 -0
- package/src/analytics-web/components/Sidebar.js +197 -0
- package/src/analytics-web/components/ToolDisplay.js +539 -0
- package/src/analytics-web/index.html +3275 -1792
- package/src/analytics-web/services/DataService.js +89 -16
- package/src/analytics-web/services/StateService.js +9 -0
- package/src/analytics-web/services/WebSocketService.js +17 -5
- package/src/analytics.js +323 -35
- package/src/console-bridge.js +610 -0
- package/src/file-operations.js +143 -23
- package/src/index.js +24 -1
- package/src/templates.js +4 -0
- package/src/test-console-bridge.js +67 -0
|
@@ -0,0 +1,1527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DashboardPage - Analytics overview page without conversations
|
|
3
|
+
* Focuses on metrics, charts, and system performance data
|
|
4
|
+
*/
|
|
5
|
+
class DashboardPage {
|
|
6
|
+
constructor(container, services) {
|
|
7
|
+
this.container = container;
|
|
8
|
+
this.dataService = services.data;
|
|
9
|
+
this.stateService = services.state;
|
|
10
|
+
this.chartService = services.chart;
|
|
11
|
+
|
|
12
|
+
this.components = {};
|
|
13
|
+
this.refreshInterval = null;
|
|
14
|
+
this.isInitialized = false;
|
|
15
|
+
|
|
16
|
+
// Subscribe to state changes
|
|
17
|
+
this.unsubscribe = this.stateService.subscribe(this.handleStateChange.bind(this));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initialize the dashboard page
|
|
22
|
+
*/
|
|
23
|
+
async initialize() {
|
|
24
|
+
if (this.isInitialized) return;
|
|
25
|
+
|
|
26
|
+
console.log('📊 Initializing DashboardPage...');
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
console.log('📊 Step 1: Rendering dashboard...');
|
|
30
|
+
await this.render();
|
|
31
|
+
console.log('✅ Dashboard rendered');
|
|
32
|
+
|
|
33
|
+
// Now that DOM is ready, we can show loading
|
|
34
|
+
this.stateService.setLoading(true);
|
|
35
|
+
|
|
36
|
+
console.log('📊 Step 2: Loading initial data...');
|
|
37
|
+
await this.loadInitialData();
|
|
38
|
+
console.log('✅ Initial data loaded');
|
|
39
|
+
|
|
40
|
+
console.log('📊 Step 3: Initializing components with data...');
|
|
41
|
+
await this.initializeComponents();
|
|
42
|
+
console.log('✅ Components initialized');
|
|
43
|
+
|
|
44
|
+
console.log('📊 Step 4: Starting periodic refresh...');
|
|
45
|
+
this.startPeriodicRefresh();
|
|
46
|
+
console.log('✅ Periodic refresh started');
|
|
47
|
+
|
|
48
|
+
this.isInitialized = true;
|
|
49
|
+
console.log('🎉 DashboardPage fully initialized!');
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('❌ Error during dashboard initialization:', error);
|
|
52
|
+
// Even if there's an error, show the dashboard with fallback data
|
|
53
|
+
this.showFallbackDashboard();
|
|
54
|
+
} finally {
|
|
55
|
+
console.log('📊 Clearing loading state...');
|
|
56
|
+
this.stateService.setLoading(false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Show fallback dashboard when initialization fails
|
|
62
|
+
*/
|
|
63
|
+
showFallbackDashboard() {
|
|
64
|
+
console.log('🆘 Showing fallback dashboard...');
|
|
65
|
+
try {
|
|
66
|
+
const demoData = {
|
|
67
|
+
summary: {
|
|
68
|
+
totalConversations: 0,
|
|
69
|
+
claudeSessions: 0,
|
|
70
|
+
claudeSessionsDetail: 'no sessions',
|
|
71
|
+
totalTokens: 0,
|
|
72
|
+
activeProjects: 0,
|
|
73
|
+
dataSize: '0 MB'
|
|
74
|
+
},
|
|
75
|
+
detailedTokenUsage: {
|
|
76
|
+
inputTokens: 0,
|
|
77
|
+
outputTokens: 0,
|
|
78
|
+
cacheCreationTokens: 0,
|
|
79
|
+
cacheReadTokens: 0
|
|
80
|
+
},
|
|
81
|
+
conversations: []
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
this.updateSummaryDisplay(demoData.summary, demoData.detailedTokenUsage, demoData);
|
|
85
|
+
this.updateLastUpdateTime();
|
|
86
|
+
this.stateService.setError('Dashboard loaded in offline mode');
|
|
87
|
+
this.isInitialized = true;
|
|
88
|
+
} catch (fallbackError) {
|
|
89
|
+
console.error('❌ Fallback dashboard also failed:', fallbackError);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Handle state changes from StateService
|
|
95
|
+
* @param {Object} state - New state
|
|
96
|
+
* @param {string} action - Action that caused the change
|
|
97
|
+
*/
|
|
98
|
+
handleStateChange(state, action) {
|
|
99
|
+
switch (action) {
|
|
100
|
+
case 'update_conversations':
|
|
101
|
+
this.updateSummaryDisplay(state.summary);
|
|
102
|
+
break;
|
|
103
|
+
case 'update_conversation_states':
|
|
104
|
+
this.updateSystemStatus(state.conversationStates);
|
|
105
|
+
break;
|
|
106
|
+
case 'set_loading':
|
|
107
|
+
this.updateLoadingState(state.isLoading);
|
|
108
|
+
break;
|
|
109
|
+
case 'set_error':
|
|
110
|
+
this.updateErrorState(state.error);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Render the dashboard page structure
|
|
117
|
+
*/
|
|
118
|
+
async render() {
|
|
119
|
+
this.container.innerHTML = `
|
|
120
|
+
<div class="dashboard-page">
|
|
121
|
+
<!-- Page Header -->
|
|
122
|
+
<div class="page-header">
|
|
123
|
+
<div class="header-content">
|
|
124
|
+
<div class="header-left">
|
|
125
|
+
<div class="status-header">
|
|
126
|
+
<span class="session-timer-status-dot active" id="session-status-dot"></span>
|
|
127
|
+
<h1 class="page-title">
|
|
128
|
+
Claude Code Analytics Dashboard
|
|
129
|
+
<span class="version-badge">v1.10.1</span>
|
|
130
|
+
</h1>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="page-subtitle">
|
|
133
|
+
Real-time monitoring and analytics for Claude Code sessions
|
|
134
|
+
</div>
|
|
135
|
+
<div class="last-update-header">
|
|
136
|
+
<span class="last-update-label">last update:</span>
|
|
137
|
+
<span id="last-update-header-text">Never</span>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="header-right">
|
|
141
|
+
<div class="theme-switch-container" title="Toggle light/dark theme">
|
|
142
|
+
<div class="theme-switch" id="header-theme-switch">
|
|
143
|
+
<div class="theme-switch-track">
|
|
144
|
+
<div class="theme-switch-thumb" id="header-theme-switch-thumb">
|
|
145
|
+
<span class="theme-switch-icon">🌙</span>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
<a href="https://github.com/anthropics/claude-code-templates" target="_blank" class="github-link" title="Star on GitHub">
|
|
151
|
+
<span class="github-icon">⭐</span>
|
|
152
|
+
Star on GitHub
|
|
153
|
+
</a>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<!-- Action Buttons -->
|
|
159
|
+
<div class="action-buttons-container">
|
|
160
|
+
<button class="action-btn-small" id="refresh-dashboard" title="Refresh data">
|
|
161
|
+
<span class="btn-icon-small">🔄</span>
|
|
162
|
+
Refresh
|
|
163
|
+
</button>
|
|
164
|
+
<button class="action-btn-small" id="export-data" title="Export analytics data">
|
|
165
|
+
<span class="btn-icon-small">📤</span>
|
|
166
|
+
Export
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<!-- Loading State -->
|
|
171
|
+
<div class="loading-state" id="dashboard-loading" style="display: none;">
|
|
172
|
+
<div class="loading-spinner"></div>
|
|
173
|
+
<span class="loading-text">Loading dashboard...</span>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<!-- Error State -->
|
|
177
|
+
<div class="error-state" id="dashboard-error" style="display: none;">
|
|
178
|
+
<div class="error-content">
|
|
179
|
+
<span class="error-icon">⚠️</span>
|
|
180
|
+
<span class="error-message"></span>
|
|
181
|
+
<button class="error-retry" id="retry-load">Retry</button>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<!-- Main Dashboard Content -->
|
|
186
|
+
<div class="dashboard-content">
|
|
187
|
+
<!-- Key Metrics Cards -->
|
|
188
|
+
<div class="metrics-cards-container">
|
|
189
|
+
<!-- Conversations Card -->
|
|
190
|
+
<div class="metric-card">
|
|
191
|
+
<div class="metric-header">
|
|
192
|
+
<div class="metric-icon">💬</div>
|
|
193
|
+
<div class="metric-title">Conversations</div>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="metric-primary">
|
|
196
|
+
<span class="metric-primary-value" id="totalConversations">0</span>
|
|
197
|
+
<span class="metric-primary-label">Total</span>
|
|
198
|
+
</div>
|
|
199
|
+
<div class="metric-secondary">
|
|
200
|
+
<div class="metric-secondary-item">
|
|
201
|
+
<span class="metric-secondary-label">This Month:</span>
|
|
202
|
+
<span class="metric-secondary-value" id="conversationsMonth">0</span>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="metric-secondary-item">
|
|
205
|
+
<span class="metric-secondary-label">This Week:</span>
|
|
206
|
+
<span class="metric-secondary-value" id="conversationsWeek">0</span>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="metric-secondary-item">
|
|
209
|
+
<span class="metric-secondary-label">Active:</span>
|
|
210
|
+
<span class="metric-secondary-value" id="activeConversations">0</span>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<!-- Sessions Card -->
|
|
216
|
+
<div class="metric-card">
|
|
217
|
+
<div class="metric-header">
|
|
218
|
+
<div class="metric-icon">⚡</div>
|
|
219
|
+
<div class="metric-title">Sessions</div>
|
|
220
|
+
</div>
|
|
221
|
+
<div class="metric-primary">
|
|
222
|
+
<span class="metric-primary-value" id="claudeSessions">0</span>
|
|
223
|
+
<span class="metric-primary-label">Total</span>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="metric-secondary">
|
|
226
|
+
<div class="metric-secondary-item">
|
|
227
|
+
<span class="metric-secondary-label">This Month:</span>
|
|
228
|
+
<span class="metric-secondary-value" id="sessionsMonth">0</span>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="metric-secondary-item">
|
|
231
|
+
<span class="metric-secondary-label">This Week:</span>
|
|
232
|
+
<span class="metric-secondary-value" id="sessionsWeek">0</span>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="metric-secondary-item">
|
|
235
|
+
<span class="metric-secondary-label">Projects:</span>
|
|
236
|
+
<span class="metric-secondary-value" id="activeProjects">0</span>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<!-- Tokens Card -->
|
|
242
|
+
<div class="metric-card">
|
|
243
|
+
<div class="metric-header">
|
|
244
|
+
<div class="metric-icon">🔢</div>
|
|
245
|
+
<div class="metric-title">Tokens</div>
|
|
246
|
+
</div>
|
|
247
|
+
<div class="metric-primary">
|
|
248
|
+
<span class="metric-primary-value" id="totalTokens">0</span>
|
|
249
|
+
<span class="metric-primary-label">Total</span>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="metric-secondary">
|
|
252
|
+
<div class="metric-secondary-item">
|
|
253
|
+
<span class="metric-secondary-label">Input:</span>
|
|
254
|
+
<span class="metric-secondary-value" id="inputTokens">0</span>
|
|
255
|
+
</div>
|
|
256
|
+
<div class="metric-secondary-item">
|
|
257
|
+
<span class="metric-secondary-label">Output:</span>
|
|
258
|
+
<span class="metric-secondary-value" id="outputTokens">0</span>
|
|
259
|
+
</div>
|
|
260
|
+
<div class="metric-secondary-item">
|
|
261
|
+
<span class="metric-secondary-label">Cache:</span>
|
|
262
|
+
<span class="metric-secondary-value" id="cacheTokens">0</span>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<!-- Session Timer Section -->
|
|
269
|
+
<div class="session-timer-section">
|
|
270
|
+
<div class="section-title">
|
|
271
|
+
<h2>Current Session</h2>
|
|
272
|
+
</div>
|
|
273
|
+
<div id="session-timer-container">
|
|
274
|
+
<!-- SessionTimer component will be mounted here -->
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<!-- Date Range Controls -->
|
|
279
|
+
<div class="chart-controls">
|
|
280
|
+
<div class="chart-controls-left">
|
|
281
|
+
<label class="filter-label">date range:</label>
|
|
282
|
+
<input type="date" id="dateFrom" class="date-input">
|
|
283
|
+
<span class="date-separator">to</span>
|
|
284
|
+
<input type="date" id="dateTo" class="date-input">
|
|
285
|
+
<button class="filter-btn" id="applyDateFilter">apply</button>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<!-- Charts Container (2x2 Grid) -->
|
|
290
|
+
<div class="charts-container">
|
|
291
|
+
<div class="chart-card">
|
|
292
|
+
<div class="chart-title">
|
|
293
|
+
📊 token usage over time
|
|
294
|
+
</div>
|
|
295
|
+
<canvas id="tokenChart" class="chart-canvas"></canvas>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
<div class="chart-card">
|
|
299
|
+
<div class="chart-title">
|
|
300
|
+
🎯 project activity distribution
|
|
301
|
+
</div>
|
|
302
|
+
<canvas id="projectChart" class="chart-canvas"></canvas>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<div class="chart-card">
|
|
306
|
+
<div class="chart-title">
|
|
307
|
+
🛠️ tool usage trends
|
|
308
|
+
</div>
|
|
309
|
+
<canvas id="toolChart" class="chart-canvas"></canvas>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<div class="chart-card">
|
|
313
|
+
<div class="chart-title">
|
|
314
|
+
⚡ tool activity summary
|
|
315
|
+
</div>
|
|
316
|
+
<div id="toolSummary" class="tool-summary">
|
|
317
|
+
<!-- Tool summary will be loaded here -->
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
`;
|
|
324
|
+
|
|
325
|
+
this.bindEvents();
|
|
326
|
+
this.initializeTheme();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Initialize child components
|
|
331
|
+
*/
|
|
332
|
+
async initializeComponents() {
|
|
333
|
+
// Initialize SessionTimer if available
|
|
334
|
+
const sessionTimerContainer = this.container.querySelector('#session-timer-container');
|
|
335
|
+
if (sessionTimerContainer && typeof SessionTimer !== 'undefined') {
|
|
336
|
+
try {
|
|
337
|
+
this.components.sessionTimer = new SessionTimer(
|
|
338
|
+
sessionTimerContainer,
|
|
339
|
+
this.dataService,
|
|
340
|
+
this.stateService
|
|
341
|
+
);
|
|
342
|
+
await this.components.sessionTimer.initialize();
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.warn('SessionTimer initialization failed:', error);
|
|
345
|
+
// Show fallback content
|
|
346
|
+
sessionTimerContainer.innerHTML = `
|
|
347
|
+
<div class="session-timer-placeholder">
|
|
348
|
+
<p>Session timer not available</p>
|
|
349
|
+
</div>
|
|
350
|
+
`;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Initialize Charts with data if available
|
|
355
|
+
await this.initializeChartsAsync();
|
|
356
|
+
|
|
357
|
+
// Initialize Activity Feed
|
|
358
|
+
this.initializeActivityFeed();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Initialize charts asynchronously to prevent blocking main dashboard
|
|
363
|
+
*/
|
|
364
|
+
async initializeChartsAsync() {
|
|
365
|
+
try {
|
|
366
|
+
console.log('📊 Starting asynchronous chart initialization...');
|
|
367
|
+
await this.initializeCharts();
|
|
368
|
+
|
|
369
|
+
// Update charts with data if available
|
|
370
|
+
if (this.allData) {
|
|
371
|
+
console.log('📊 Updating charts with loaded data...');
|
|
372
|
+
this.updateChartData(this.allData);
|
|
373
|
+
console.log('✅ Charts updated with data');
|
|
374
|
+
}
|
|
375
|
+
} catch (error) {
|
|
376
|
+
console.error('❌ Chart initialization failed, dashboard will work without charts:', error);
|
|
377
|
+
// Dashboard continues to work without charts
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Initialize charts (Token Usage, Project Distribution, Tool Usage)
|
|
383
|
+
*/
|
|
384
|
+
async initializeCharts() {
|
|
385
|
+
// Destroy existing charts if they exist
|
|
386
|
+
if (this.components.tokenChart) {
|
|
387
|
+
this.components.tokenChart.destroy();
|
|
388
|
+
this.components.tokenChart = null;
|
|
389
|
+
}
|
|
390
|
+
if (this.components.projectChart) {
|
|
391
|
+
this.components.projectChart.destroy();
|
|
392
|
+
this.components.projectChart = null;
|
|
393
|
+
}
|
|
394
|
+
if (this.components.toolChart) {
|
|
395
|
+
this.components.toolChart.destroy();
|
|
396
|
+
this.components.toolChart = null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Longer delay to ensure DOM is fully ready and previous charts are destroyed
|
|
400
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
401
|
+
|
|
402
|
+
// Get canvas elements with strict validation
|
|
403
|
+
const tokenCanvas = this.container.querySelector('#tokenChart');
|
|
404
|
+
const projectCanvas = this.container.querySelector('#projectChart');
|
|
405
|
+
const toolCanvas = this.container.querySelector('#toolChart');
|
|
406
|
+
|
|
407
|
+
// Validate all canvas elements exist and are properly attached to DOM
|
|
408
|
+
if (!tokenCanvas || !projectCanvas || !toolCanvas) {
|
|
409
|
+
console.error('❌ Chart canvas elements not found in DOM');
|
|
410
|
+
console.log('Available elements:', {
|
|
411
|
+
tokenCanvas: !!tokenCanvas,
|
|
412
|
+
projectCanvas: !!projectCanvas,
|
|
413
|
+
toolCanvas: !!toolCanvas
|
|
414
|
+
});
|
|
415
|
+
return; // Don't initialize charts if canvas elements are missing
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Verify canvas elements are properly connected to the DOM
|
|
419
|
+
if (!document.body.contains(tokenCanvas) ||
|
|
420
|
+
!document.body.contains(projectCanvas) ||
|
|
421
|
+
!document.body.contains(toolCanvas)) {
|
|
422
|
+
console.error('❌ Chart canvas elements not properly attached to DOM');
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Force destroy any existing Chart instances
|
|
427
|
+
try {
|
|
428
|
+
if (Chart.getChart(tokenCanvas)) {
|
|
429
|
+
console.log('🧹 Destroying existing tokenChart instance');
|
|
430
|
+
Chart.getChart(tokenCanvas).destroy();
|
|
431
|
+
}
|
|
432
|
+
if (Chart.getChart(projectCanvas)) {
|
|
433
|
+
console.log('🧹 Destroying existing projectChart instance');
|
|
434
|
+
Chart.getChart(projectCanvas).destroy();
|
|
435
|
+
}
|
|
436
|
+
if (Chart.getChart(toolCanvas)) {
|
|
437
|
+
console.log('🧹 Destroying existing toolChart instance');
|
|
438
|
+
Chart.getChart(toolCanvas).destroy();
|
|
439
|
+
}
|
|
440
|
+
} catch (error) {
|
|
441
|
+
console.warn('Warning during chart cleanup:', error);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Validate canvas dimensions and ensure they're properly sized
|
|
445
|
+
const canvases = [tokenCanvas, projectCanvas, toolCanvas];
|
|
446
|
+
for (const canvas of canvases) {
|
|
447
|
+
if (canvas.offsetWidth === 0 || canvas.offsetHeight === 0) {
|
|
448
|
+
console.error('❌ Canvas has zero dimensions, waiting for layout...');
|
|
449
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
450
|
+
if (canvas.offsetWidth === 0 || canvas.offsetHeight === 0) {
|
|
451
|
+
console.error('❌ Canvas still has zero dimensions after wait');
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Token Usage Chart (Linear)
|
|
458
|
+
if (tokenCanvas) {
|
|
459
|
+
try {
|
|
460
|
+
console.log('📊 Creating token chart...');
|
|
461
|
+
this.components.tokenChart = new Chart(tokenCanvas, {
|
|
462
|
+
type: 'line',
|
|
463
|
+
data: {
|
|
464
|
+
labels: [],
|
|
465
|
+
datasets: [{
|
|
466
|
+
label: 'Tokens',
|
|
467
|
+
data: [],
|
|
468
|
+
borderColor: '#d57455',
|
|
469
|
+
backgroundColor: 'rgba(213, 116, 85, 0.1)',
|
|
470
|
+
tension: 0.4,
|
|
471
|
+
fill: true
|
|
472
|
+
}]
|
|
473
|
+
},
|
|
474
|
+
options: this.getTokenChartOptions()
|
|
475
|
+
});
|
|
476
|
+
console.log('✅ Token chart created successfully');
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.error('❌ Error creating token chart:', error);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Project Activity Distribution Chart (Pie)
|
|
483
|
+
if (projectCanvas) {
|
|
484
|
+
try {
|
|
485
|
+
console.log('📊 Creating project chart...');
|
|
486
|
+
this.components.projectChart = new Chart(projectCanvas, {
|
|
487
|
+
type: 'doughnut',
|
|
488
|
+
data: {
|
|
489
|
+
labels: [],
|
|
490
|
+
datasets: [{
|
|
491
|
+
data: [],
|
|
492
|
+
backgroundColor: [
|
|
493
|
+
'#d57455', '#3fb950', '#f97316', '#a5d6ff',
|
|
494
|
+
'#f85149', '#7d8590', '#ffd33d', '#bf91f3'
|
|
495
|
+
],
|
|
496
|
+
borderWidth: 0
|
|
497
|
+
}]
|
|
498
|
+
},
|
|
499
|
+
options: this.getProjectChartOptions()
|
|
500
|
+
});
|
|
501
|
+
console.log('✅ Project chart created successfully');
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.error('❌ Error creating project chart:', error);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Tool Usage Trends Chart (Bar)
|
|
508
|
+
if (toolCanvas) {
|
|
509
|
+
try {
|
|
510
|
+
console.log('📊 Creating tool chart...');
|
|
511
|
+
this.components.toolChart = new Chart(toolCanvas, {
|
|
512
|
+
type: 'bar',
|
|
513
|
+
data: {
|
|
514
|
+
labels: [],
|
|
515
|
+
datasets: [{
|
|
516
|
+
label: 'Usage Count',
|
|
517
|
+
data: [],
|
|
518
|
+
backgroundColor: [
|
|
519
|
+
'rgba(75, 192, 192, 0.6)', 'rgba(255, 99, 132, 0.6)',
|
|
520
|
+
'rgba(54, 162, 235, 0.6)', 'rgba(255, 206, 86, 0.6)',
|
|
521
|
+
'rgba(153, 102, 255, 0.6)', 'rgba(255, 159, 64, 0.6)'
|
|
522
|
+
],
|
|
523
|
+
borderColor: [
|
|
524
|
+
'rgba(75, 192, 192, 1)', 'rgba(255, 99, 132, 1)',
|
|
525
|
+
'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)',
|
|
526
|
+
'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)'
|
|
527
|
+
],
|
|
528
|
+
borderWidth: 1
|
|
529
|
+
}]
|
|
530
|
+
},
|
|
531
|
+
options: this.getToolChartOptions()
|
|
532
|
+
});
|
|
533
|
+
console.log('✅ Tool chart created successfully');
|
|
534
|
+
} catch (error) {
|
|
535
|
+
console.error('❌ Error creating tool chart:', error);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
console.log('🎉 All charts initialized successfully');
|
|
540
|
+
|
|
541
|
+
// Initialize date inputs
|
|
542
|
+
this.initializeDateInputs();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Get token chart options
|
|
547
|
+
*/
|
|
548
|
+
getTokenChartOptions() {
|
|
549
|
+
return {
|
|
550
|
+
responsive: true,
|
|
551
|
+
maintainAspectRatio: false,
|
|
552
|
+
interaction: {
|
|
553
|
+
mode: 'nearest',
|
|
554
|
+
axis: 'x',
|
|
555
|
+
intersect: false
|
|
556
|
+
},
|
|
557
|
+
plugins: {
|
|
558
|
+
legend: {
|
|
559
|
+
display: false
|
|
560
|
+
},
|
|
561
|
+
tooltip: {
|
|
562
|
+
enabled: true,
|
|
563
|
+
mode: 'nearest',
|
|
564
|
+
backgroundColor: '#161b22',
|
|
565
|
+
titleColor: '#d57455',
|
|
566
|
+
bodyColor: '#c9d1d9',
|
|
567
|
+
borderColor: '#30363d',
|
|
568
|
+
borderWidth: 1,
|
|
569
|
+
cornerRadius: 4,
|
|
570
|
+
displayColors: false,
|
|
571
|
+
animation: {
|
|
572
|
+
duration: 200
|
|
573
|
+
},
|
|
574
|
+
callbacks: {
|
|
575
|
+
title: function(context) {
|
|
576
|
+
return `Date: ${context[0].label}`;
|
|
577
|
+
},
|
|
578
|
+
label: function(context) {
|
|
579
|
+
return `Tokens: ${context.parsed.y.toLocaleString()}`;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
scales: {
|
|
585
|
+
x: {
|
|
586
|
+
grid: {
|
|
587
|
+
color: '#30363d'
|
|
588
|
+
},
|
|
589
|
+
ticks: {
|
|
590
|
+
color: '#7d8590'
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
y: {
|
|
594
|
+
beginAtZero: true,
|
|
595
|
+
grid: {
|
|
596
|
+
color: '#30363d'
|
|
597
|
+
},
|
|
598
|
+
ticks: {
|
|
599
|
+
color: '#7d8590',
|
|
600
|
+
callback: function(value) {
|
|
601
|
+
return value.toLocaleString();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Get project chart options
|
|
611
|
+
*/
|
|
612
|
+
getProjectChartOptions() {
|
|
613
|
+
return {
|
|
614
|
+
responsive: true,
|
|
615
|
+
maintainAspectRatio: false,
|
|
616
|
+
plugins: {
|
|
617
|
+
legend: {
|
|
618
|
+
position: 'bottom',
|
|
619
|
+
labels: {
|
|
620
|
+
color: '#c9d1d9',
|
|
621
|
+
padding: 15,
|
|
622
|
+
usePointStyle: true
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
tooltip: {
|
|
626
|
+
enabled: true,
|
|
627
|
+
backgroundColor: '#161b22',
|
|
628
|
+
titleColor: '#d57455',
|
|
629
|
+
bodyColor: '#c9d1d9',
|
|
630
|
+
borderColor: '#30363d',
|
|
631
|
+
borderWidth: 1,
|
|
632
|
+
cornerRadius: 4,
|
|
633
|
+
displayColors: false,
|
|
634
|
+
animation: {
|
|
635
|
+
duration: 200
|
|
636
|
+
},
|
|
637
|
+
callbacks: {
|
|
638
|
+
title: function(context) {
|
|
639
|
+
return `Project: ${context[0].label}`;
|
|
640
|
+
},
|
|
641
|
+
label: function(context) {
|
|
642
|
+
const total = context.dataset.data.reduce((sum, value) => sum + value, 0);
|
|
643
|
+
const percentage = ((context.parsed / total) * 100).toFixed(1);
|
|
644
|
+
return `${context.parsed.toLocaleString()} conversations (${percentage}%)`;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
cutout: '60%'
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Get tool chart options
|
|
655
|
+
*/
|
|
656
|
+
getToolChartOptions() {
|
|
657
|
+
return {
|
|
658
|
+
responsive: true,
|
|
659
|
+
maintainAspectRatio: false,
|
|
660
|
+
interaction: {
|
|
661
|
+
mode: 'nearest',
|
|
662
|
+
axis: 'x',
|
|
663
|
+
intersect: false
|
|
664
|
+
},
|
|
665
|
+
plugins: {
|
|
666
|
+
legend: {
|
|
667
|
+
display: false
|
|
668
|
+
},
|
|
669
|
+
tooltip: {
|
|
670
|
+
enabled: true,
|
|
671
|
+
mode: 'nearest',
|
|
672
|
+
backgroundColor: '#161b22',
|
|
673
|
+
titleColor: '#d57455',
|
|
674
|
+
bodyColor: '#c9d1d9',
|
|
675
|
+
borderColor: '#30363d',
|
|
676
|
+
borderWidth: 1,
|
|
677
|
+
cornerRadius: 4,
|
|
678
|
+
displayColors: false,
|
|
679
|
+
animation: {
|
|
680
|
+
duration: 200
|
|
681
|
+
},
|
|
682
|
+
callbacks: {
|
|
683
|
+
title: function(context) {
|
|
684
|
+
return `Tool: ${context[0].label}`;
|
|
685
|
+
},
|
|
686
|
+
label: function(context) {
|
|
687
|
+
return `Usage: ${context.parsed.y.toLocaleString()} times`;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
scales: {
|
|
693
|
+
x: {
|
|
694
|
+
grid: {
|
|
695
|
+
color: '#30363d'
|
|
696
|
+
},
|
|
697
|
+
ticks: {
|
|
698
|
+
color: '#7d8590',
|
|
699
|
+
maxRotation: 45
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
y: {
|
|
703
|
+
beginAtZero: true,
|
|
704
|
+
grid: {
|
|
705
|
+
color: '#30363d'
|
|
706
|
+
},
|
|
707
|
+
ticks: {
|
|
708
|
+
color: '#7d8590',
|
|
709
|
+
stepSize: 1
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Initialize activity feed
|
|
718
|
+
*/
|
|
719
|
+
initializeActivityFeed() {
|
|
720
|
+
const activityFeed = this.container.querySelector('#activity-feed');
|
|
721
|
+
|
|
722
|
+
// Check if activity feed element exists
|
|
723
|
+
if (!activityFeed) {
|
|
724
|
+
console.log('ℹ️ Activity feed element not found, skipping initialization');
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Sample activity data (would be replaced with real data)
|
|
729
|
+
const activities = [
|
|
730
|
+
{
|
|
731
|
+
type: 'session_start',
|
|
732
|
+
message: 'New Claude Code session started',
|
|
733
|
+
timestamp: new Date(),
|
|
734
|
+
icon: '🚀'
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
type: 'conversation_update',
|
|
738
|
+
message: 'Conversation state updated',
|
|
739
|
+
timestamp: new Date(Date.now() - 5 * 60 * 1000),
|
|
740
|
+
icon: '💬'
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
type: 'system_event',
|
|
744
|
+
message: 'Analytics server started',
|
|
745
|
+
timestamp: new Date(Date.now() - 10 * 60 * 1000),
|
|
746
|
+
icon: '⚡'
|
|
747
|
+
}
|
|
748
|
+
];
|
|
749
|
+
|
|
750
|
+
activityFeed.innerHTML = activities.map(activity => `
|
|
751
|
+
<div class="activity-item">
|
|
752
|
+
<div class="activity-icon">${activity.icon}</div>
|
|
753
|
+
<div class="activity-content">
|
|
754
|
+
<div class="activity-message">${activity.message}</div>
|
|
755
|
+
<div class="activity-time">${this.formatTimestamp(activity.timestamp)}</div>
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
`).join('');
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Format timestamp for display
|
|
763
|
+
* @param {Date} timestamp - Timestamp to format
|
|
764
|
+
* @returns {string} Formatted timestamp
|
|
765
|
+
*/
|
|
766
|
+
formatTimestamp(timestamp) {
|
|
767
|
+
const now = new Date();
|
|
768
|
+
const diff = now - timestamp;
|
|
769
|
+
const minutes = Math.floor(diff / (1000 * 60));
|
|
770
|
+
|
|
771
|
+
if (minutes < 1) return 'Just now';
|
|
772
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
773
|
+
|
|
774
|
+
const hours = Math.floor(minutes / 60);
|
|
775
|
+
if (hours < 24) return `${hours}h ago`;
|
|
776
|
+
|
|
777
|
+
return timestamp.toLocaleDateString();
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Bind event listeners
|
|
782
|
+
*/
|
|
783
|
+
bindEvents() {
|
|
784
|
+
// Refresh button
|
|
785
|
+
const refreshBtn = this.container.querySelector('#refresh-dashboard');
|
|
786
|
+
if (refreshBtn) {
|
|
787
|
+
refreshBtn.addEventListener('click', () => this.refreshData());
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Export button
|
|
791
|
+
const exportBtn = this.container.querySelector('#export-data');
|
|
792
|
+
if (exportBtn) {
|
|
793
|
+
exportBtn.addEventListener('click', () => this.exportData());
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Date filter controls
|
|
797
|
+
const applyDateFilter = this.container.querySelector('#applyDateFilter');
|
|
798
|
+
if (applyDateFilter) {
|
|
799
|
+
applyDateFilter.addEventListener('click', () => this.applyDateFilter());
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Token popover events
|
|
803
|
+
const totalTokens = this.container.querySelector('#totalTokens');
|
|
804
|
+
if (totalTokens) {
|
|
805
|
+
totalTokens.addEventListener('mouseenter', () => this.showTokenPopover());
|
|
806
|
+
totalTokens.addEventListener('mouseleave', () => this.hideTokenPopover());
|
|
807
|
+
totalTokens.addEventListener('click', () => this.showTokenPopover());
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Error retry
|
|
811
|
+
const retryBtn = this.container.querySelector('#retry-load');
|
|
812
|
+
if (retryBtn) {
|
|
813
|
+
retryBtn.addEventListener('click', () => this.loadInitialData());
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Theme toggle (header)
|
|
817
|
+
const headerThemeSwitch = this.container.querySelector('#header-theme-switch');
|
|
818
|
+
if (headerThemeSwitch) {
|
|
819
|
+
headerThemeSwitch.addEventListener('click', () => this.toggleTheme());
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Load initial data
|
|
825
|
+
*/
|
|
826
|
+
async loadInitialData() {
|
|
827
|
+
try {
|
|
828
|
+
const [conversationsData, statesData] = await Promise.all([
|
|
829
|
+
this.dataService.getConversations(),
|
|
830
|
+
this.dataService.getConversationStates()
|
|
831
|
+
]);
|
|
832
|
+
|
|
833
|
+
this.stateService.updateConversations(conversationsData.conversations);
|
|
834
|
+
this.stateService.updateSummary(conversationsData.summary);
|
|
835
|
+
this.stateService.updateConversationStates(statesData);
|
|
836
|
+
|
|
837
|
+
// Update dashboard with original format
|
|
838
|
+
this.updateSummaryDisplay(
|
|
839
|
+
conversationsData.summary,
|
|
840
|
+
conversationsData.detailedTokenUsage,
|
|
841
|
+
conversationsData
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
this.updateLastUpdateTime();
|
|
845
|
+
this.updateChartData(conversationsData);
|
|
846
|
+
} catch (error) {
|
|
847
|
+
console.error('Error loading initial data:', error);
|
|
848
|
+
|
|
849
|
+
// Try to provide fallback demo data
|
|
850
|
+
const demoData = {
|
|
851
|
+
summary: {
|
|
852
|
+
totalConversations: 0,
|
|
853
|
+
claudeSessions: 0,
|
|
854
|
+
claudeSessionsDetail: 'no sessions',
|
|
855
|
+
totalTokens: 0,
|
|
856
|
+
activeProjects: 0,
|
|
857
|
+
dataSize: '0 MB'
|
|
858
|
+
},
|
|
859
|
+
detailedTokenUsage: {
|
|
860
|
+
inputTokens: 0,
|
|
861
|
+
outputTokens: 0,
|
|
862
|
+
cacheCreationTokens: 0,
|
|
863
|
+
cacheReadTokens: 0
|
|
864
|
+
},
|
|
865
|
+
conversations: []
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
this.updateSummaryDisplay(demoData.summary, demoData.detailedTokenUsage, demoData);
|
|
869
|
+
this.updateLastUpdateTime();
|
|
870
|
+
this.stateService.setError('Using offline mode - server connection failed');
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Refresh all data
|
|
876
|
+
*/
|
|
877
|
+
async refreshData() {
|
|
878
|
+
const refreshBtn = this.container.querySelector('#refresh-dashboard');
|
|
879
|
+
if (!refreshBtn) return;
|
|
880
|
+
|
|
881
|
+
refreshBtn.disabled = true;
|
|
882
|
+
refreshBtn.classList.add('loading');
|
|
883
|
+
|
|
884
|
+
const btnIcon = refreshBtn.querySelector('.btn-icon-small');
|
|
885
|
+
if (btnIcon) {
|
|
886
|
+
btnIcon.classList.add('spin');
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
try {
|
|
890
|
+
this.dataService.clearCache();
|
|
891
|
+
await this.loadInitialData();
|
|
892
|
+
} catch (error) {
|
|
893
|
+
console.error('Error refreshing data:', error);
|
|
894
|
+
this.stateService.setError('Failed to refresh data');
|
|
895
|
+
} finally {
|
|
896
|
+
refreshBtn.disabled = false;
|
|
897
|
+
refreshBtn.classList.remove('loading');
|
|
898
|
+
|
|
899
|
+
if (btnIcon) {
|
|
900
|
+
btnIcon.classList.remove('spin');
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Update summary display (New Cards format)
|
|
907
|
+
* @param {Object} summary - Summary data
|
|
908
|
+
* @param {Object} detailedTokenUsage - Detailed token breakdown
|
|
909
|
+
* @param {Object} allData - Complete dataset
|
|
910
|
+
*/
|
|
911
|
+
updateSummaryDisplay(summary, detailedTokenUsage, allData) {
|
|
912
|
+
if (!summary) return;
|
|
913
|
+
|
|
914
|
+
// Calculate additional metrics
|
|
915
|
+
const now = new Date();
|
|
916
|
+
const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
917
|
+
const thisWeek = new Date(now.setDate(now.getDate() - now.getDay()));
|
|
918
|
+
|
|
919
|
+
// Update primary metrics
|
|
920
|
+
const totalConversations = this.container.querySelector('#totalConversations');
|
|
921
|
+
const claudeSessions = this.container.querySelector('#claudeSessions');
|
|
922
|
+
const totalTokens = this.container.querySelector('#totalTokens');
|
|
923
|
+
|
|
924
|
+
if (totalConversations) totalConversations.textContent = summary.totalConversations?.toLocaleString() || '0';
|
|
925
|
+
if (claudeSessions) claudeSessions.textContent = summary.claudeSessions?.toLocaleString() || '0';
|
|
926
|
+
if (totalTokens) totalTokens.textContent = summary.totalTokens?.toLocaleString() || '0';
|
|
927
|
+
|
|
928
|
+
// Update conversation secondary metrics
|
|
929
|
+
const conversationsMonth = this.container.querySelector('#conversationsMonth');
|
|
930
|
+
const conversationsWeek = this.container.querySelector('#conversationsWeek');
|
|
931
|
+
const activeConversations = this.container.querySelector('#activeConversations');
|
|
932
|
+
|
|
933
|
+
if (conversationsMonth) conversationsMonth.textContent = this.calculateTimeRangeCount(allData?.conversations, thisMonth).toLocaleString();
|
|
934
|
+
if (conversationsWeek) conversationsWeek.textContent = this.calculateTimeRangeCount(allData?.conversations, thisWeek).toLocaleString();
|
|
935
|
+
if (activeConversations) activeConversations.textContent = summary.activeConversations?.toLocaleString() || '0';
|
|
936
|
+
|
|
937
|
+
// Update session secondary metrics
|
|
938
|
+
const sessionsMonth = this.container.querySelector('#sessionsMonth');
|
|
939
|
+
const sessionsWeek = this.container.querySelector('#sessionsWeek');
|
|
940
|
+
const activeProjects = this.container.querySelector('#activeProjects');
|
|
941
|
+
|
|
942
|
+
if (sessionsMonth) sessionsMonth.textContent = Math.max(1, Math.floor((summary.claudeSessions || 0) * 0.3)).toLocaleString();
|
|
943
|
+
if (sessionsWeek) sessionsWeek.textContent = Math.max(1, Math.floor((summary.claudeSessions || 0) * 0.1)).toLocaleString();
|
|
944
|
+
if (activeProjects) activeProjects.textContent = summary.activeProjects?.toLocaleString() || '0';
|
|
945
|
+
|
|
946
|
+
// Update token secondary metrics
|
|
947
|
+
if (detailedTokenUsage) {
|
|
948
|
+
this.updateTokenBreakdown(detailedTokenUsage);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Store data for chart updates
|
|
952
|
+
this.allData = allData;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Calculate count of items within a time range
|
|
957
|
+
* @param {Array} items - Items with lastModified property
|
|
958
|
+
* @param {Date} fromDate - Start date
|
|
959
|
+
* @returns {number} Count of items
|
|
960
|
+
*/
|
|
961
|
+
calculateTimeRangeCount(items, fromDate) {
|
|
962
|
+
if (!items || !Array.isArray(items)) return 0;
|
|
963
|
+
|
|
964
|
+
return items.filter(item => {
|
|
965
|
+
if (!item.lastModified) return false;
|
|
966
|
+
const itemDate = new Date(item.lastModified);
|
|
967
|
+
return itemDate >= fromDate;
|
|
968
|
+
}).length;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Update token breakdown in cards
|
|
973
|
+
* @param {Object} tokenUsage - Detailed token usage
|
|
974
|
+
*/
|
|
975
|
+
updateTokenBreakdown(tokenUsage) {
|
|
976
|
+
const inputTokens = this.container.querySelector('#inputTokens');
|
|
977
|
+
const outputTokens = this.container.querySelector('#outputTokens');
|
|
978
|
+
const cacheTokens = this.container.querySelector('#cacheTokens');
|
|
979
|
+
|
|
980
|
+
if (inputTokens) inputTokens.textContent = tokenUsage.inputTokens?.toLocaleString() || '0';
|
|
981
|
+
if (outputTokens) outputTokens.textContent = tokenUsage.outputTokens?.toLocaleString() || '0';
|
|
982
|
+
|
|
983
|
+
// Combine cache creation and read tokens
|
|
984
|
+
const totalCache = (tokenUsage.cacheCreationTokens || 0) + (tokenUsage.cacheReadTokens || 0);
|
|
985
|
+
if (cacheTokens) cacheTokens.textContent = totalCache.toLocaleString();
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Show token popover
|
|
990
|
+
*/
|
|
991
|
+
showTokenPopover() {
|
|
992
|
+
const popover = this.container.querySelector('#tokenPopover');
|
|
993
|
+
if (popover) {
|
|
994
|
+
popover.style.display = 'block';
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Hide token popover
|
|
1000
|
+
*/
|
|
1001
|
+
hideTokenPopover() {
|
|
1002
|
+
const popover = this.container.querySelector('#tokenPopover');
|
|
1003
|
+
if (popover) {
|
|
1004
|
+
popover.style.display = 'none';
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Initialize date inputs
|
|
1010
|
+
*/
|
|
1011
|
+
initializeDateInputs() {
|
|
1012
|
+
const dateFrom = this.container.querySelector('#dateFrom');
|
|
1013
|
+
const dateTo = this.container.querySelector('#dateTo');
|
|
1014
|
+
|
|
1015
|
+
if (!dateFrom || !dateTo) return;
|
|
1016
|
+
|
|
1017
|
+
const today = new Date();
|
|
1018
|
+
const sevenDaysAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
1019
|
+
|
|
1020
|
+
dateFrom.value = sevenDaysAgo.toISOString().split('T')[0];
|
|
1021
|
+
dateTo.value = today.toISOString().split('T')[0];
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Get date range from inputs
|
|
1026
|
+
*/
|
|
1027
|
+
getDateRange() {
|
|
1028
|
+
const dateFrom = this.container.querySelector('#dateFrom');
|
|
1029
|
+
const dateTo = this.container.querySelector('#dateTo');
|
|
1030
|
+
|
|
1031
|
+
let fromDate = new Date();
|
|
1032
|
+
fromDate.setDate(fromDate.getDate() - 7); // Default to 7 days ago
|
|
1033
|
+
let toDate = new Date();
|
|
1034
|
+
|
|
1035
|
+
if (dateFrom && dateFrom.value) {
|
|
1036
|
+
fromDate = new Date(dateFrom.value);
|
|
1037
|
+
}
|
|
1038
|
+
if (dateTo && dateTo.value) {
|
|
1039
|
+
toDate = new Date(dateTo.value);
|
|
1040
|
+
toDate.setHours(23, 59, 59, 999); // Include full day
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
return { fromDate, toDate };
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Apply date filter
|
|
1048
|
+
*/
|
|
1049
|
+
applyDateFilter() {
|
|
1050
|
+
if (this.allData) {
|
|
1051
|
+
this.updateChartData(this.allData);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Refresh charts
|
|
1057
|
+
*/
|
|
1058
|
+
async refreshCharts() {
|
|
1059
|
+
const refreshBtn = this.container.querySelector('#refreshCharts');
|
|
1060
|
+
if (refreshBtn) {
|
|
1061
|
+
refreshBtn.disabled = true;
|
|
1062
|
+
refreshBtn.textContent = 'refreshing...';
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
try {
|
|
1066
|
+
await this.loadInitialData();
|
|
1067
|
+
} finally {
|
|
1068
|
+
if (refreshBtn) {
|
|
1069
|
+
refreshBtn.disabled = false;
|
|
1070
|
+
refreshBtn.textContent = 'refresh charts';
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Update system status
|
|
1077
|
+
* @param {Object} states - Conversation states
|
|
1078
|
+
*/
|
|
1079
|
+
updateSystemStatus(states) {
|
|
1080
|
+
const activeCount = Object.values(states).filter(state => state === 'active').length;
|
|
1081
|
+
|
|
1082
|
+
// Update WebSocket status
|
|
1083
|
+
const wsStatus = this.container.querySelector('#websocket-status');
|
|
1084
|
+
if (wsStatus) {
|
|
1085
|
+
const indicator = wsStatus.querySelector('.status-indicator');
|
|
1086
|
+
indicator.className = `status-indicator ${activeCount > 0 ? 'connected' : 'disconnected'}`;
|
|
1087
|
+
wsStatus.lastChild.textContent = activeCount > 0 ? 'Connected' : 'Disconnected';
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Update chart data with real analytics
|
|
1093
|
+
* @param {Object} data - Analytics data
|
|
1094
|
+
*/
|
|
1095
|
+
updateChartData(data) {
|
|
1096
|
+
if (!data || !data.conversations) return;
|
|
1097
|
+
|
|
1098
|
+
this.updateTokenChart(data.conversations);
|
|
1099
|
+
this.updateProjectChart(data.conversations);
|
|
1100
|
+
this.updateToolChart(data.conversations);
|
|
1101
|
+
this.updateToolSummary(data.conversations);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Update token usage chart
|
|
1106
|
+
*/
|
|
1107
|
+
updateTokenChart(conversations) {
|
|
1108
|
+
if (!this.components.tokenChart) {
|
|
1109
|
+
console.warn('Token chart not initialized');
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const { fromDate, toDate } = this.getDateRange();
|
|
1114
|
+
const filteredConversations = conversations.filter(conv => {
|
|
1115
|
+
const convDate = new Date(conv.lastModified);
|
|
1116
|
+
return convDate >= fromDate && convDate <= toDate;
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// Group by date and sum tokens
|
|
1120
|
+
const tokensByDate = {};
|
|
1121
|
+
filteredConversations.forEach(conv => {
|
|
1122
|
+
const date = new Date(conv.lastModified).toDateString();
|
|
1123
|
+
tokensByDate[date] = (tokensByDate[date] || 0) + (conv.tokens || 0);
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
const sortedDates = Object.keys(tokensByDate).sort((a, b) => new Date(a) - new Date(b));
|
|
1127
|
+
const labels = sortedDates.map(date => new Date(date).toLocaleDateString());
|
|
1128
|
+
const data = sortedDates.map(date => tokensByDate[date]);
|
|
1129
|
+
|
|
1130
|
+
this.components.tokenChart.data.labels = labels;
|
|
1131
|
+
this.components.tokenChart.data.datasets[0].data = data;
|
|
1132
|
+
this.components.tokenChart.update();
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Update project distribution chart
|
|
1137
|
+
*/
|
|
1138
|
+
updateProjectChart(conversations) {
|
|
1139
|
+
if (!this.components.projectChart) {
|
|
1140
|
+
console.warn('Project chart not initialized');
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const { fromDate, toDate } = this.getDateRange();
|
|
1145
|
+
const filteredConversations = conversations.filter(conv => {
|
|
1146
|
+
const convDate = new Date(conv.lastModified);
|
|
1147
|
+
return convDate >= fromDate && convDate <= toDate;
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
// Group by project and sum tokens
|
|
1151
|
+
const projectTokens = {};
|
|
1152
|
+
filteredConversations.forEach(conv => {
|
|
1153
|
+
const project = conv.project || 'Unknown';
|
|
1154
|
+
projectTokens[project] = (projectTokens[project] || 0) + (conv.tokens || 0);
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
const labels = Object.keys(projectTokens);
|
|
1158
|
+
const data = Object.values(projectTokens);
|
|
1159
|
+
|
|
1160
|
+
this.components.projectChart.data.labels = labels;
|
|
1161
|
+
this.components.projectChart.data.datasets[0].data = data;
|
|
1162
|
+
this.components.projectChart.update();
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Update tool usage chart
|
|
1167
|
+
*/
|
|
1168
|
+
updateToolChart(conversations) {
|
|
1169
|
+
if (!this.components.toolChart) {
|
|
1170
|
+
console.warn('Tool chart not initialized');
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const { fromDate, toDate } = this.getDateRange();
|
|
1175
|
+
const toolStats = {};
|
|
1176
|
+
|
|
1177
|
+
conversations.forEach(conv => {
|
|
1178
|
+
if (conv.toolUsage && conv.toolUsage.toolTimeline) {
|
|
1179
|
+
conv.toolUsage.toolTimeline.forEach(entry => {
|
|
1180
|
+
const entryDate = new Date(entry.timestamp);
|
|
1181
|
+
if (entryDate >= fromDate && entryDate <= toDate) {
|
|
1182
|
+
toolStats[entry.tool] = (toolStats[entry.tool] || 0) + 1;
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
const sortedTools = Object.entries(toolStats)
|
|
1189
|
+
.sort((a, b) => b[1] - a[1])
|
|
1190
|
+
.slice(0, 10);
|
|
1191
|
+
|
|
1192
|
+
const labels = sortedTools.map(([tool]) => tool.length > 15 ? tool.substring(0, 15) + '...' : tool);
|
|
1193
|
+
const data = sortedTools.map(([, count]) => count);
|
|
1194
|
+
|
|
1195
|
+
this.components.toolChart.data.labels = labels;
|
|
1196
|
+
this.components.toolChart.data.datasets[0].data = data;
|
|
1197
|
+
this.components.toolChart.update();
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Update tool summary panel
|
|
1202
|
+
*/
|
|
1203
|
+
updateToolSummary(conversations) {
|
|
1204
|
+
const toolSummary = this.container.querySelector('#toolSummary');
|
|
1205
|
+
if (!toolSummary) return;
|
|
1206
|
+
|
|
1207
|
+
const { fromDate, toDate } = this.getDateRange();
|
|
1208
|
+
const toolStats = {};
|
|
1209
|
+
let totalToolCalls = 0;
|
|
1210
|
+
let conversationsWithTools = 0;
|
|
1211
|
+
|
|
1212
|
+
conversations.forEach(conv => {
|
|
1213
|
+
if (conv.toolUsage && conv.toolUsage.toolTimeline) {
|
|
1214
|
+
let convHasTools = false;
|
|
1215
|
+
conv.toolUsage.toolTimeline.forEach(entry => {
|
|
1216
|
+
const entryDate = new Date(entry.timestamp);
|
|
1217
|
+
if (entryDate >= fromDate && entryDate <= toDate) {
|
|
1218
|
+
toolStats[entry.tool] = (toolStats[entry.tool] || 0) + 1;
|
|
1219
|
+
totalToolCalls++;
|
|
1220
|
+
convHasTools = true;
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
if (convHasTools) conversationsWithTools++;
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
const uniqueTools = Object.keys(toolStats).length;
|
|
1228
|
+
const topTool = Object.entries(toolStats).sort((a, b) => b[1] - a[1])[0];
|
|
1229
|
+
|
|
1230
|
+
toolSummary.innerHTML = `
|
|
1231
|
+
<div class="tool-stat">
|
|
1232
|
+
<span class="tool-stat-label">Total Tool Calls</span>
|
|
1233
|
+
<span class="tool-stat-value">${totalToolCalls.toLocaleString()}</span>
|
|
1234
|
+
</div>
|
|
1235
|
+
<div class="tool-stat">
|
|
1236
|
+
<span class="tool-stat-label">Unique Tools Used</span>
|
|
1237
|
+
<span class="tool-stat-value">${uniqueTools}</span>
|
|
1238
|
+
</div>
|
|
1239
|
+
<div class="tool-stat">
|
|
1240
|
+
<span class="tool-stat-label">Conversation Coverage</span>
|
|
1241
|
+
<span class="tool-stat-value">${Math.round((conversationsWithTools / conversations.length) * 100)}%</span>
|
|
1242
|
+
</div>
|
|
1243
|
+
${topTool ? `
|
|
1244
|
+
<div class="tool-top-tool">
|
|
1245
|
+
<div class="tool-icon">🛠️</div>
|
|
1246
|
+
<div class="tool-info">
|
|
1247
|
+
<div class="tool-name">${topTool[0]}</div>
|
|
1248
|
+
<div class="tool-usage">${topTool[1]} calls</div>
|
|
1249
|
+
</div>
|
|
1250
|
+
</div>
|
|
1251
|
+
` : ''}
|
|
1252
|
+
`;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Update usage chart
|
|
1257
|
+
* @param {string} period - Time period
|
|
1258
|
+
*/
|
|
1259
|
+
updateUsageChart(period) {
|
|
1260
|
+
console.log('Updating usage chart period to:', period);
|
|
1261
|
+
// Implementation would update chart with new period data
|
|
1262
|
+
this.updateChartData();
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* Update performance chart
|
|
1267
|
+
* @param {string} type - Chart type
|
|
1268
|
+
*/
|
|
1269
|
+
updatePerformanceChart(type) {
|
|
1270
|
+
console.log('Updating performance chart type to:', type);
|
|
1271
|
+
// Implementation would update chart with new metric type
|
|
1272
|
+
this.updateChartData();
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Show all activity
|
|
1277
|
+
*/
|
|
1278
|
+
showAllActivity() {
|
|
1279
|
+
console.log('Showing all activity');
|
|
1280
|
+
// Implementation would show expanded activity view
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Export data
|
|
1285
|
+
*/
|
|
1286
|
+
exportData() {
|
|
1287
|
+
const exportBtn = this.container.querySelector('#export-data');
|
|
1288
|
+
if (!exportBtn) return;
|
|
1289
|
+
|
|
1290
|
+
// Show loading state
|
|
1291
|
+
exportBtn.disabled = true;
|
|
1292
|
+
exportBtn.classList.add('loading');
|
|
1293
|
+
|
|
1294
|
+
const btnIcon = exportBtn.querySelector('.btn-icon-small');
|
|
1295
|
+
if (btnIcon) {
|
|
1296
|
+
btnIcon.classList.add('spin');
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
try {
|
|
1300
|
+
const dashboardData = {
|
|
1301
|
+
summary: this.stateService.getStateProperty('summary'),
|
|
1302
|
+
states: this.stateService.getStateProperty('conversationStates'),
|
|
1303
|
+
exportDate: new Date().toISOString(),
|
|
1304
|
+
type: 'dashboard_analytics'
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
const dataStr = JSON.stringify(dashboardData, null, 2);
|
|
1308
|
+
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
1309
|
+
const url = URL.createObjectURL(dataBlob);
|
|
1310
|
+
|
|
1311
|
+
const link = document.createElement('a');
|
|
1312
|
+
link.href = url;
|
|
1313
|
+
link.download = `dashboard-analytics-${new Date().toISOString().split('T')[0]}.json`;
|
|
1314
|
+
link.click();
|
|
1315
|
+
|
|
1316
|
+
URL.revokeObjectURL(url);
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
console.error('Error exporting data:', error);
|
|
1319
|
+
this.stateService.setError('Failed to export data');
|
|
1320
|
+
} finally {
|
|
1321
|
+
// Restore button state after short delay to show completion
|
|
1322
|
+
setTimeout(() => {
|
|
1323
|
+
exportBtn.disabled = false;
|
|
1324
|
+
exportBtn.classList.remove('loading');
|
|
1325
|
+
|
|
1326
|
+
if (btnIcon) {
|
|
1327
|
+
btnIcon.classList.remove('spin');
|
|
1328
|
+
}
|
|
1329
|
+
}, 500);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Initialize theme from localStorage
|
|
1335
|
+
*/
|
|
1336
|
+
initializeTheme() {
|
|
1337
|
+
const savedTheme = localStorage.getItem('claude-analytics-theme') || 'dark';
|
|
1338
|
+
const body = document.body;
|
|
1339
|
+
const headerThumb = this.container.querySelector('#header-theme-switch-thumb');
|
|
1340
|
+
const headerIcon = headerThumb?.querySelector('.theme-switch-icon');
|
|
1341
|
+
|
|
1342
|
+
body.setAttribute('data-theme', savedTheme);
|
|
1343
|
+
if (headerThumb && headerIcon) {
|
|
1344
|
+
if (savedTheme === 'light') {
|
|
1345
|
+
headerThumb.classList.add('light');
|
|
1346
|
+
headerIcon.textContent = '☀️';
|
|
1347
|
+
} else {
|
|
1348
|
+
headerThumb.classList.remove('light');
|
|
1349
|
+
headerIcon.textContent = '🌙';
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Toggle theme between light and dark
|
|
1356
|
+
*/
|
|
1357
|
+
toggleTheme() {
|
|
1358
|
+
const body = document.body;
|
|
1359
|
+
const headerThumb = this.container.querySelector('#header-theme-switch-thumb');
|
|
1360
|
+
const headerIcon = headerThumb?.querySelector('.theme-switch-icon');
|
|
1361
|
+
|
|
1362
|
+
// Also sync with global theme switch
|
|
1363
|
+
const globalThumb = document.getElementById('themeSwitchThumb');
|
|
1364
|
+
const globalIcon = globalThumb?.querySelector('.theme-switch-icon');
|
|
1365
|
+
|
|
1366
|
+
const isLight = body.getAttribute('data-theme') === 'light';
|
|
1367
|
+
const newTheme = isLight ? 'dark' : 'light';
|
|
1368
|
+
|
|
1369
|
+
body.setAttribute('data-theme', newTheme);
|
|
1370
|
+
|
|
1371
|
+
// Update header theme switch
|
|
1372
|
+
if (headerThumb && headerIcon) {
|
|
1373
|
+
headerThumb.classList.toggle('light', newTheme === 'light');
|
|
1374
|
+
headerIcon.textContent = newTheme === 'light' ? '☀️' : '🌙';
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Sync with global theme switch
|
|
1378
|
+
if (globalThumb && globalIcon) {
|
|
1379
|
+
globalThumb.classList.toggle('light', newTheme === 'light');
|
|
1380
|
+
globalIcon.textContent = newTheme === 'light' ? '☀️' : '🌙';
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
localStorage.setItem('claude-analytics-theme', newTheme);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* Update last update time
|
|
1388
|
+
*/
|
|
1389
|
+
updateLastUpdateTime() {
|
|
1390
|
+
const currentTime = new Date().toLocaleTimeString();
|
|
1391
|
+
|
|
1392
|
+
// Update both locations
|
|
1393
|
+
const lastUpdateText = this.container.querySelector('#last-update-text');
|
|
1394
|
+
const lastUpdateHeaderText = this.container.querySelector('#last-update-header-text');
|
|
1395
|
+
|
|
1396
|
+
if (lastUpdateText) {
|
|
1397
|
+
lastUpdateText.textContent = currentTime;
|
|
1398
|
+
}
|
|
1399
|
+
if (lastUpdateHeaderText) {
|
|
1400
|
+
lastUpdateHeaderText.textContent = currentTime;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* Start periodic refresh
|
|
1406
|
+
*/
|
|
1407
|
+
startPeriodicRefresh() {
|
|
1408
|
+
this.refreshInterval = setInterval(async () => {
|
|
1409
|
+
try {
|
|
1410
|
+
const statesData = await this.dataService.getConversationStates();
|
|
1411
|
+
this.stateService.updateConversationStates(statesData);
|
|
1412
|
+
this.updateLastUpdateTime();
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
console.error('Error during periodic refresh:', error);
|
|
1415
|
+
}
|
|
1416
|
+
}, 30000); // Refresh every 30 seconds
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Stop periodic refresh
|
|
1421
|
+
*/
|
|
1422
|
+
stopPeriodicRefresh() {
|
|
1423
|
+
if (this.refreshInterval) {
|
|
1424
|
+
clearInterval(this.refreshInterval);
|
|
1425
|
+
this.refreshInterval = null;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* Update loading state
|
|
1431
|
+
* @param {boolean} isLoading - Loading state
|
|
1432
|
+
*/
|
|
1433
|
+
updateLoadingState(isLoading) {
|
|
1434
|
+
console.log(`🔄 Updating loading state to: ${isLoading}`);
|
|
1435
|
+
const loadingState = this.container.querySelector('#dashboard-loading');
|
|
1436
|
+
if (loadingState) {
|
|
1437
|
+
loadingState.style.display = isLoading ? 'flex' : 'none';
|
|
1438
|
+
console.log(`✅ Loading state updated successfully to: ${isLoading ? 'visible' : 'hidden'}`);
|
|
1439
|
+
} else {
|
|
1440
|
+
console.warn('⚠️ Loading element #dashboard-loading not found');
|
|
1441
|
+
// Fallback: show/hide global loading instead
|
|
1442
|
+
const globalLoading = document.querySelector('#global-loading');
|
|
1443
|
+
if (globalLoading) {
|
|
1444
|
+
globalLoading.style.display = isLoading ? 'flex' : 'none';
|
|
1445
|
+
console.log(`✅ Global loading fallback updated to: ${isLoading ? 'visible' : 'hidden'}`);
|
|
1446
|
+
} else {
|
|
1447
|
+
console.warn('⚠️ Global loading element #global-loading also not found');
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* Update error state
|
|
1454
|
+
* @param {Error|string} error - Error object or message
|
|
1455
|
+
*/
|
|
1456
|
+
updateErrorState(error) {
|
|
1457
|
+
const errorState = this.container.querySelector('#dashboard-error');
|
|
1458
|
+
const errorMessage = this.container.querySelector('.error-message');
|
|
1459
|
+
|
|
1460
|
+
if (error) {
|
|
1461
|
+
if (errorMessage) {
|
|
1462
|
+
errorMessage.textContent = error.message || error;
|
|
1463
|
+
}
|
|
1464
|
+
if (errorState) {
|
|
1465
|
+
errorState.style.display = 'flex';
|
|
1466
|
+
}
|
|
1467
|
+
} else {
|
|
1468
|
+
if (errorState) {
|
|
1469
|
+
errorState.style.display = 'none';
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Destroy dashboard page
|
|
1476
|
+
*/
|
|
1477
|
+
destroy() {
|
|
1478
|
+
this.stopPeriodicRefresh();
|
|
1479
|
+
|
|
1480
|
+
// Cleanup Chart.js instances specifically
|
|
1481
|
+
if (this.components.tokenChart) {
|
|
1482
|
+
this.components.tokenChart.destroy();
|
|
1483
|
+
this.components.tokenChart = null;
|
|
1484
|
+
}
|
|
1485
|
+
if (this.components.projectChart) {
|
|
1486
|
+
this.components.projectChart.destroy();
|
|
1487
|
+
this.components.projectChart = null;
|
|
1488
|
+
}
|
|
1489
|
+
if (this.components.toolChart) {
|
|
1490
|
+
this.components.toolChart.destroy();
|
|
1491
|
+
this.components.toolChart = null;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// Force cleanup any remaining Chart.js instances on canvas elements
|
|
1495
|
+
if (this.container) {
|
|
1496
|
+
const canvases = this.container.querySelectorAll('canvas');
|
|
1497
|
+
canvases.forEach(canvas => {
|
|
1498
|
+
const existingChart = Chart.getChart(canvas);
|
|
1499
|
+
if (existingChart) {
|
|
1500
|
+
existingChart.destroy();
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// Cleanup other components
|
|
1506
|
+
Object.values(this.components).forEach(component => {
|
|
1507
|
+
if (component && component.destroy && typeof component.destroy === 'function') {
|
|
1508
|
+
component.destroy();
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
// Clear components object
|
|
1513
|
+
this.components = {};
|
|
1514
|
+
|
|
1515
|
+
// Unsubscribe from state changes
|
|
1516
|
+
if (this.unsubscribe) {
|
|
1517
|
+
this.unsubscribe();
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
this.isInitialized = false;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// Export for module use
|
|
1525
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
1526
|
+
module.exports = DashboardPage;
|
|
1527
|
+
}
|