claude-code-templates 1.8.0 → 1.8.2
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 +246 -0
- package/package.json +26 -12
- package/src/analytics/core/ConversationAnalyzer.js +754 -0
- package/src/analytics/core/FileWatcher.js +285 -0
- package/src/analytics/core/ProcessDetector.js +242 -0
- package/src/analytics/core/SessionAnalyzer.js +631 -0
- package/src/analytics/core/StateCalculator.js +190 -0
- package/src/analytics/data/DataCache.js +550 -0
- package/src/analytics/notifications/NotificationManager.js +448 -0
- package/src/analytics/notifications/WebSocketServer.js +526 -0
- package/src/analytics/utils/PerformanceMonitor.js +455 -0
- package/src/analytics-web/assets/js/main.js +312 -0
- package/src/analytics-web/components/Charts.js +114 -0
- package/src/analytics-web/components/ConversationTable.js +437 -0
- package/src/analytics-web/components/Dashboard.js +573 -0
- package/src/analytics-web/components/SessionTimer.js +596 -0
- package/src/analytics-web/index.html +882 -49
- package/src/analytics-web/index.html.original +1939 -0
- package/src/analytics-web/services/DataService.js +357 -0
- package/src/analytics-web/services/StateService.js +276 -0
- package/src/analytics-web/services/WebSocketService.js +523 -0
- package/src/analytics.js +641 -2311
- package/src/analytics.log +0 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConversationTable - Handles the conversations table display and interactions
|
|
3
|
+
* Part of the modular frontend architecture
|
|
4
|
+
*/
|
|
5
|
+
class ConversationTable {
|
|
6
|
+
constructor(container, dataService, stateService) {
|
|
7
|
+
this.container = container;
|
|
8
|
+
this.dataService = dataService;
|
|
9
|
+
this.stateService = stateService;
|
|
10
|
+
this.conversations = [];
|
|
11
|
+
this.currentFilter = 'all';
|
|
12
|
+
this.currentSort = { field: 'lastModified', direction: 'desc' };
|
|
13
|
+
this.pageSize = 50;
|
|
14
|
+
this.currentPage = 1;
|
|
15
|
+
|
|
16
|
+
// Subscribe to state changes
|
|
17
|
+
this.unsubscribe = this.stateService.subscribe(this.handleStateChange.bind(this));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initialize the conversation table
|
|
22
|
+
*/
|
|
23
|
+
async initialize() {
|
|
24
|
+
this.render();
|
|
25
|
+
this.bindEvents();
|
|
26
|
+
await this.loadConversations();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Handle state changes from StateService
|
|
31
|
+
* @param {Object} state - New state
|
|
32
|
+
* @param {string} action - Action that caused the change
|
|
33
|
+
*/
|
|
34
|
+
handleStateChange(state, action) {
|
|
35
|
+
if (action === 'update_conversations' || action === 'update_conversation_states') {
|
|
36
|
+
this.conversations = state.conversations;
|
|
37
|
+
this.updateTable();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Render the conversation table structure
|
|
43
|
+
*/
|
|
44
|
+
render() {
|
|
45
|
+
this.container.innerHTML = `
|
|
46
|
+
<div class="conversation-table-container">
|
|
47
|
+
<div class="table-header">
|
|
48
|
+
<div class="table-controls">
|
|
49
|
+
<div class="filter-controls">
|
|
50
|
+
<select class="filter-select" id="status-filter">
|
|
51
|
+
<option value="all">All Status</option>
|
|
52
|
+
<option value="active">Active</option>
|
|
53
|
+
<option value="waiting">Waiting</option>
|
|
54
|
+
<option value="idle">Idle</option>
|
|
55
|
+
<option value="completed">Completed</option>
|
|
56
|
+
</select>
|
|
57
|
+
<input type="text" class="search-input" placeholder="Search conversations..." id="search-input">
|
|
58
|
+
</div>
|
|
59
|
+
<div class="pagination-info">
|
|
60
|
+
<span id="pagination-text">Showing 0 of 0 conversations</span>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="table-wrapper">
|
|
66
|
+
<table class="conversation-table">
|
|
67
|
+
<thead>
|
|
68
|
+
<tr>
|
|
69
|
+
<th class="sortable" data-field="status">Status</th>
|
|
70
|
+
<th class="sortable" data-field="id">ID</th>
|
|
71
|
+
<th class="sortable" data-field="project">Project</th>
|
|
72
|
+
<th class="sortable" data-field="tokens">Tokens</th>
|
|
73
|
+
<th class="sortable" data-field="lastModified">Last Modified</th>
|
|
74
|
+
<th class="sortable" data-field="messages">Messages</th>
|
|
75
|
+
<th>Actions</th>
|
|
76
|
+
</tr>
|
|
77
|
+
</thead>
|
|
78
|
+
<tbody id="conversation-tbody">
|
|
79
|
+
<tr class="loading-row">
|
|
80
|
+
<td colspan="7" class="loading-cell">Loading conversations...</td>
|
|
81
|
+
</tr>
|
|
82
|
+
</tbody>
|
|
83
|
+
</table>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="table-footer">
|
|
87
|
+
<div class="pagination-controls">
|
|
88
|
+
<button class="pagination-btn" id="prev-page" disabled>Previous</button>
|
|
89
|
+
<span id="page-info">Page 1 of 1</span>
|
|
90
|
+
<button class="pagination-btn" id="next-page" disabled>Next</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Bind event listeners
|
|
99
|
+
*/
|
|
100
|
+
bindEvents() {
|
|
101
|
+
// Filter and search
|
|
102
|
+
const statusFilter = this.container.querySelector('#status-filter');
|
|
103
|
+
const searchInput = this.container.querySelector('#search-input');
|
|
104
|
+
|
|
105
|
+
statusFilter.addEventListener('change', (e) => {
|
|
106
|
+
this.currentFilter = e.target.value;
|
|
107
|
+
this.currentPage = 1;
|
|
108
|
+
this.updateTable();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
searchInput.addEventListener('input', (e) => {
|
|
112
|
+
this.searchTerm = e.target.value.toLowerCase();
|
|
113
|
+
this.currentPage = 1;
|
|
114
|
+
this.updateTable();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Sorting
|
|
118
|
+
const sortHeaders = this.container.querySelectorAll('.sortable');
|
|
119
|
+
sortHeaders.forEach(header => {
|
|
120
|
+
header.addEventListener('click', () => {
|
|
121
|
+
const field = header.dataset.field;
|
|
122
|
+
this.handleSort(field);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Pagination
|
|
127
|
+
const prevBtn = this.container.querySelector('#prev-page');
|
|
128
|
+
const nextBtn = this.container.querySelector('#next-page');
|
|
129
|
+
|
|
130
|
+
prevBtn.addEventListener('click', () => {
|
|
131
|
+
if (this.currentPage > 1) {
|
|
132
|
+
this.currentPage--;
|
|
133
|
+
this.updateTable();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
nextBtn.addEventListener('click', () => {
|
|
138
|
+
const totalPages = Math.ceil(this.getFilteredConversations().length / this.pageSize);
|
|
139
|
+
if (this.currentPage < totalPages) {
|
|
140
|
+
this.currentPage++;
|
|
141
|
+
this.updateTable();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Load conversations from data service
|
|
148
|
+
*/
|
|
149
|
+
async loadConversations() {
|
|
150
|
+
try {
|
|
151
|
+
const data = await this.dataService.getConversations();
|
|
152
|
+
this.conversations = data.conversations || [];
|
|
153
|
+
this.updateTable();
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('Error loading conversations:', error);
|
|
156
|
+
this.showError('Failed to load conversations');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Handle sorting
|
|
162
|
+
* @param {string} field - Field to sort by
|
|
163
|
+
*/
|
|
164
|
+
handleSort(field) {
|
|
165
|
+
if (this.currentSort.field === field) {
|
|
166
|
+
this.currentSort.direction = this.currentSort.direction === 'asc' ? 'desc' : 'asc';
|
|
167
|
+
} else {
|
|
168
|
+
this.currentSort.field = field;
|
|
169
|
+
this.currentSort.direction = 'asc';
|
|
170
|
+
}
|
|
171
|
+
this.updateTable();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get filtered conversations based on current filter and search
|
|
176
|
+
* @returns {Array} Filtered conversations
|
|
177
|
+
*/
|
|
178
|
+
getFilteredConversations() {
|
|
179
|
+
let filtered = [...this.conversations];
|
|
180
|
+
|
|
181
|
+
// Apply status filter
|
|
182
|
+
if (this.currentFilter !== 'all') {
|
|
183
|
+
filtered = filtered.filter(conv => conv.status === this.currentFilter);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Apply search filter
|
|
187
|
+
if (this.searchTerm) {
|
|
188
|
+
filtered = filtered.filter(conv =>
|
|
189
|
+
conv.id.toLowerCase().includes(this.searchTerm) ||
|
|
190
|
+
conv.project.toLowerCase().includes(this.searchTerm) ||
|
|
191
|
+
conv.filename.toLowerCase().includes(this.searchTerm)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Apply sorting
|
|
196
|
+
filtered.sort((a, b) => {
|
|
197
|
+
let aValue = a[this.currentSort.field];
|
|
198
|
+
let bValue = b[this.currentSort.field];
|
|
199
|
+
|
|
200
|
+
// Handle different data types
|
|
201
|
+
if (this.currentSort.field === 'lastModified') {
|
|
202
|
+
aValue = new Date(aValue).getTime();
|
|
203
|
+
bValue = new Date(bValue).getTime();
|
|
204
|
+
} else if (typeof aValue === 'string') {
|
|
205
|
+
aValue = aValue.toLowerCase();
|
|
206
|
+
bValue = bValue.toLowerCase();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (this.currentSort.direction === 'asc') {
|
|
210
|
+
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
|
|
211
|
+
} else {
|
|
212
|
+
return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return filtered;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Update the table display
|
|
221
|
+
*/
|
|
222
|
+
updateTable() {
|
|
223
|
+
const filtered = this.getFilteredConversations();
|
|
224
|
+
const startIndex = (this.currentPage - 1) * this.pageSize;
|
|
225
|
+
const endIndex = startIndex + this.pageSize;
|
|
226
|
+
const pageData = filtered.slice(startIndex, endIndex);
|
|
227
|
+
|
|
228
|
+
this.renderTableBody(pageData);
|
|
229
|
+
this.updatePagination(filtered.length);
|
|
230
|
+
this.updateSortHeaders();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Render table body with conversation data
|
|
235
|
+
* @param {Array} conversations - Conversations to display
|
|
236
|
+
*/
|
|
237
|
+
renderTableBody(conversations) {
|
|
238
|
+
const tbody = this.container.querySelector('#conversation-tbody');
|
|
239
|
+
|
|
240
|
+
if (conversations.length === 0) {
|
|
241
|
+
tbody.innerHTML = `
|
|
242
|
+
<tr class="no-data-row">
|
|
243
|
+
<td colspan="7" class="no-data-cell">
|
|
244
|
+
${this.currentFilter === 'all' ? 'No conversations found' : `No ${this.currentFilter} conversations found`}
|
|
245
|
+
</td>
|
|
246
|
+
</tr>
|
|
247
|
+
`;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
tbody.innerHTML = conversations.map(conv => `
|
|
252
|
+
<tr class="conversation-row" data-id="${conv.id}">
|
|
253
|
+
<td class="status-cell">
|
|
254
|
+
<span class="status-badge status-${conv.status}">${conv.status}</span>
|
|
255
|
+
</td>
|
|
256
|
+
<td class="id-cell">
|
|
257
|
+
<span class="conversation-id" title="${conv.id}">${conv.id.slice(0, 8)}...</span>
|
|
258
|
+
</td>
|
|
259
|
+
<td class="project-cell">
|
|
260
|
+
<span class="project-name">${conv.project}</span>
|
|
261
|
+
</td>
|
|
262
|
+
<td class="tokens-cell">
|
|
263
|
+
<span class="token-count">${conv.tokens.toLocaleString()}</span>
|
|
264
|
+
</td>
|
|
265
|
+
<td class="modified-cell">
|
|
266
|
+
<span class="modified-time" title="${conv.lastModified}">
|
|
267
|
+
${this.formatRelativeTime(conv.lastModified)}
|
|
268
|
+
</span>
|
|
269
|
+
</td>
|
|
270
|
+
<td class="messages-cell">
|
|
271
|
+
<span class="message-count">${conv.messages}</span>
|
|
272
|
+
</td>
|
|
273
|
+
<td class="actions-cell">
|
|
274
|
+
<button class="action-btn view-btn" data-id="${conv.id}" title="View details">
|
|
275
|
+
👁️
|
|
276
|
+
</button>
|
|
277
|
+
<button class="action-btn refresh-btn" data-id="${conv.id}" title="Refresh status">
|
|
278
|
+
🔄
|
|
279
|
+
</button>
|
|
280
|
+
</td>
|
|
281
|
+
</tr>
|
|
282
|
+
`).join('');
|
|
283
|
+
|
|
284
|
+
// Bind action buttons
|
|
285
|
+
this.bindActionButtons();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Bind action button events
|
|
290
|
+
*/
|
|
291
|
+
bindActionButtons() {
|
|
292
|
+
const viewBtns = this.container.querySelectorAll('.view-btn');
|
|
293
|
+
const refreshBtns = this.container.querySelectorAll('.refresh-btn');
|
|
294
|
+
|
|
295
|
+
viewBtns.forEach(btn => {
|
|
296
|
+
btn.addEventListener('click', (e) => {
|
|
297
|
+
const conversationId = e.target.dataset.id;
|
|
298
|
+
this.viewConversation(conversationId);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
refreshBtns.forEach(btn => {
|
|
303
|
+
btn.addEventListener('click', (e) => {
|
|
304
|
+
const conversationId = e.target.dataset.id;
|
|
305
|
+
this.refreshConversation(conversationId);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* View conversation details
|
|
312
|
+
* @param {string} conversationId - ID of conversation to view
|
|
313
|
+
*/
|
|
314
|
+
viewConversation(conversationId) {
|
|
315
|
+
const conversation = this.conversations.find(conv => conv.id === conversationId);
|
|
316
|
+
if (conversation) {
|
|
317
|
+
this.stateService.setSelectedConversation(conversation);
|
|
318
|
+
// Could trigger a modal or navigation to detail view
|
|
319
|
+
console.log('Viewing conversation:', conversation);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Refresh conversation status
|
|
325
|
+
* @param {string} conversationId - ID of conversation to refresh
|
|
326
|
+
*/
|
|
327
|
+
async refreshConversation(conversationId) {
|
|
328
|
+
try {
|
|
329
|
+
// Force refresh of conversation states
|
|
330
|
+
this.dataService.clearCacheEntry('/api/conversation-state');
|
|
331
|
+
const states = await this.dataService.getConversationStates();
|
|
332
|
+
this.stateService.updateConversationStates(states);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
console.error('Error refreshing conversation:', error);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Update pagination controls
|
|
340
|
+
* @param {number} totalItems - Total number of items
|
|
341
|
+
*/
|
|
342
|
+
updatePagination(totalItems) {
|
|
343
|
+
const totalPages = Math.ceil(totalItems / this.pageSize);
|
|
344
|
+
const startItem = (this.currentPage - 1) * this.pageSize + 1;
|
|
345
|
+
const endItem = Math.min(this.currentPage * this.pageSize, totalItems);
|
|
346
|
+
|
|
347
|
+
// Update pagination text
|
|
348
|
+
const paginationText = this.container.querySelector('#pagination-text');
|
|
349
|
+
paginationText.textContent = `Showing ${startItem}-${endItem} of ${totalItems} conversations`;
|
|
350
|
+
|
|
351
|
+
// Update page info
|
|
352
|
+
const pageInfo = this.container.querySelector('#page-info');
|
|
353
|
+
pageInfo.textContent = `Page ${this.currentPage} of ${totalPages}`;
|
|
354
|
+
|
|
355
|
+
// Update button states
|
|
356
|
+
const prevBtn = this.container.querySelector('#prev-page');
|
|
357
|
+
const nextBtn = this.container.querySelector('#next-page');
|
|
358
|
+
|
|
359
|
+
prevBtn.disabled = this.currentPage === 1;
|
|
360
|
+
nextBtn.disabled = this.currentPage === totalPages || totalPages === 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Update sort headers visual state
|
|
365
|
+
*/
|
|
366
|
+
updateSortHeaders() {
|
|
367
|
+
const headers = this.container.querySelectorAll('.sortable');
|
|
368
|
+
headers.forEach(header => {
|
|
369
|
+
header.classList.remove('sort-asc', 'sort-desc');
|
|
370
|
+
if (header.dataset.field === this.currentSort.field) {
|
|
371
|
+
header.classList.add(`sort-${this.currentSort.direction}`);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Format relative time
|
|
378
|
+
* @param {string} dateString - Date string to format
|
|
379
|
+
* @returns {string} Formatted relative time
|
|
380
|
+
*/
|
|
381
|
+
formatRelativeTime(dateString) {
|
|
382
|
+
const date = new Date(dateString);
|
|
383
|
+
const now = new Date();
|
|
384
|
+
const diff = now - date;
|
|
385
|
+
const seconds = Math.floor(diff / 1000);
|
|
386
|
+
const minutes = Math.floor(seconds / 60);
|
|
387
|
+
const hours = Math.floor(minutes / 60);
|
|
388
|
+
const days = Math.floor(hours / 24);
|
|
389
|
+
|
|
390
|
+
if (days > 0) return `${days}d ago`;
|
|
391
|
+
if (hours > 0) return `${hours}h ago`;
|
|
392
|
+
if (minutes > 0) return `${minutes}m ago`;
|
|
393
|
+
return 'Just now';
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Show error message
|
|
398
|
+
* @param {string} message - Error message to display
|
|
399
|
+
*/
|
|
400
|
+
showError(message) {
|
|
401
|
+
const tbody = this.container.querySelector('#conversation-tbody');
|
|
402
|
+
tbody.innerHTML = `
|
|
403
|
+
<tr class="error-row">
|
|
404
|
+
<td colspan="7" class="error-cell">
|
|
405
|
+
<span class="error-message">⚠️ ${message}</span>
|
|
406
|
+
</td>
|
|
407
|
+
</tr>
|
|
408
|
+
`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Update conversation state in real-time
|
|
413
|
+
* @param {string} conversationId - ID of conversation
|
|
414
|
+
* @param {string} newState - New state
|
|
415
|
+
*/
|
|
416
|
+
updateConversationState(conversationId, newState) {
|
|
417
|
+
const conversation = this.conversations.find(conv => conv.id === conversationId);
|
|
418
|
+
if (conversation) {
|
|
419
|
+
conversation.status = newState;
|
|
420
|
+
this.updateTable();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Cleanup event listeners
|
|
426
|
+
*/
|
|
427
|
+
destroy() {
|
|
428
|
+
if (this.unsubscribe) {
|
|
429
|
+
this.unsubscribe();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Export for module use
|
|
435
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
436
|
+
module.exports = ConversationTable;
|
|
437
|
+
}
|