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.
@@ -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
+ }