claude-code-templates
Version:
CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects
437 lines (392 loc) • 13.6 kB
JavaScript
/**
* ConversationTable - Handles the conversations table display and interactions
* Part of the modular frontend architecture
*/
class ConversationTable {
constructor(container, dataService, stateService) {
this.container = container;
this.dataService = dataService;
this.stateService = stateService;
this.conversations = [];
this.currentFilter = 'all';
this.currentSort = { field: 'lastModified', direction: 'desc' };
this.pageSize = 50;
this.currentPage = 1;
// Subscribe to state changes
this.unsubscribe = this.stateService.subscribe(this.handleStateChange.bind(this));
}
/**
* Initialize the conversation table
*/
async initialize() {
this.render();
this.bindEvents();
await this.loadConversations();
}
/**
* Handle state changes from StateService
* @param {Object} state - New state
* @param {string} action - Action that caused the change
*/
handleStateChange(state, action) {
if (action === 'update_conversations' || action === 'update_conversation_states') {
this.conversations = state.conversations;
this.updateTable();
}
}
/**
* Render the conversation table structure
*/
render() {
this.container.innerHTML = `
<div class="conversation-table-container">
<div class="table-header">
<div class="table-controls">
<div class="filter-controls">
<select class="filter-select" id="status-filter">
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="waiting">Waiting</option>
<option value="idle">Idle</option>
<option value="completed">Completed</option>
</select>
<input type="text" class="search-input" placeholder="Search conversations..." id="search-input">
</div>
<div class="pagination-info">
<span id="pagination-text">Showing 0 of 0 conversations</span>
</div>
</div>
</div>
<div class="table-wrapper">
<table class="conversation-table">
<thead>
<tr>
<th class="sortable" data-field="status">Status</th>
<th class="sortable" data-field="id">ID</th>
<th class="sortable" data-field="project">Project</th>
<th class="sortable" data-field="tokens">Tokens</th>
<th class="sortable" data-field="lastModified">Last Modified</th>
<th class="sortable" data-field="messages">Messages</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="conversation-tbody">
<tr class="loading-row">
<td colspan="7" class="loading-cell">Loading conversations...</td>
</tr>
</tbody>
</table>
</div>
<div class="table-footer">
<div class="pagination-controls">
<button class="pagination-btn" id="prev-page" disabled>Previous</button>
<span id="page-info">Page 1 of 1</span>
<button class="pagination-btn" id="next-page" disabled>Next</button>
</div>
</div>
</div>
`;
}
/**
* Bind event listeners
*/
bindEvents() {
// Filter and search
const statusFilter = this.container.querySelector('#status-filter');
const searchInput = this.container.querySelector('#search-input');
statusFilter.addEventListener('change', (e) => {
this.currentFilter = e.target.value;
this.currentPage = 1;
this.updateTable();
});
searchInput.addEventListener('input', (e) => {
this.searchTerm = e.target.value.toLowerCase();
this.currentPage = 1;
this.updateTable();
});
// Sorting
const sortHeaders = this.container.querySelectorAll('.sortable');
sortHeaders.forEach(header => {
header.addEventListener('click', () => {
const field = header.dataset.field;
this.handleSort(field);
});
});
// Pagination
const prevBtn = this.container.querySelector('#prev-page');
const nextBtn = this.container.querySelector('#next-page');
prevBtn.addEventListener('click', () => {
if (this.currentPage > 1) {
this.currentPage--;
this.updateTable();
}
});
nextBtn.addEventListener('click', () => {
const totalPages = Math.ceil(this.getFilteredConversations().length / this.pageSize);
if (this.currentPage < totalPages) {
this.currentPage++;
this.updateTable();
}
});
}
/**
* Load conversations from data service
*/
async loadConversations() {
try {
const data = await this.dataService.getConversations();
this.conversations = data.conversations || [];
this.updateTable();
} catch (error) {
console.error('Error loading conversations:', error);
this.showError('Failed to load conversations');
}
}
/**
* Handle sorting
* @param {string} field - Field to sort by
*/
handleSort(field) {
if (this.currentSort.field === field) {
this.currentSort.direction = this.currentSort.direction === 'asc' ? 'desc' : 'asc';
} else {
this.currentSort.field = field;
this.currentSort.direction = 'asc';
}
this.updateTable();
}
/**
* Get filtered conversations based on current filter and search
* @returns {Array} Filtered conversations
*/
getFilteredConversations() {
let filtered = [...this.conversations];
// Apply status filter
if (this.currentFilter !== 'all') {
filtered = filtered.filter(conv => conv.status === this.currentFilter);
}
// Apply search filter
if (this.searchTerm) {
filtered = filtered.filter(conv =>
conv.id.toLowerCase().includes(this.searchTerm) ||
conv.project.toLowerCase().includes(this.searchTerm) ||
conv.filename.toLowerCase().includes(this.searchTerm)
);
}
// Apply sorting
filtered.sort((a, b) => {
let aValue = a[this.currentSort.field];
let bValue = b[this.currentSort.field];
// Handle different data types
if (this.currentSort.field === 'lastModified') {
aValue = new Date(aValue).getTime();
bValue = new Date(bValue).getTime();
} else if (typeof aValue === 'string') {
aValue = aValue.toLowerCase();
bValue = bValue.toLowerCase();
}
if (this.currentSort.direction === 'asc') {
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
} else {
return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
}
});
return filtered;
}
/**
* Update the table display
*/
updateTable() {
const filtered = this.getFilteredConversations();
const startIndex = (this.currentPage - 1) * this.pageSize;
const endIndex = startIndex + this.pageSize;
const pageData = filtered.slice(startIndex, endIndex);
this.renderTableBody(pageData);
this.updatePagination(filtered.length);
this.updateSortHeaders();
}
/**
* Render table body with conversation data
* @param {Array} conversations - Conversations to display
*/
renderTableBody(conversations) {
const tbody = this.container.querySelector('#conversation-tbody');
if (conversations.length === 0) {
tbody.innerHTML = `
<tr class="no-data-row">
<td colspan="7" class="no-data-cell">
${this.currentFilter === 'all' ? 'No conversations found' : `No ${this.currentFilter} conversations found`}
</td>
</tr>
`;
return;
}
tbody.innerHTML = conversations.map(conv => `
<tr class="conversation-row" data-id="${conv.id}">
<td class="status-cell">
<span class="status-badge status-${conv.status}">${conv.status}</span>
</td>
<td class="id-cell">
<span class="conversation-id" title="${conv.id}">${conv.id.slice(0, 8)}...</span>
</td>
<td class="project-cell">
<span class="project-name">${conv.project}</span>
</td>
<td class="tokens-cell">
<span class="token-count">${conv.tokens.toLocaleString()}</span>
</td>
<td class="modified-cell">
<span class="modified-time" title="${conv.lastModified}">
${this.formatRelativeTime(conv.lastModified)}
</span>
</td>
<td class="messages-cell">
<span class="message-count">${conv.messages}</span>
</td>
<td class="actions-cell">
<button class="action-btn view-btn" data-id="${conv.id}" title="View details">
👁️
</button>
<button class="action-btn refresh-btn" data-id="${conv.id}" title="Refresh status">
🔄
</button>
</td>
</tr>
`).join('');
// Bind action buttons
this.bindActionButtons();
}
/**
* Bind action button events
*/
bindActionButtons() {
const viewBtns = this.container.querySelectorAll('.view-btn');
const refreshBtns = this.container.querySelectorAll('.refresh-btn');
viewBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
const conversationId = e.target.dataset.id;
this.viewConversation(conversationId);
});
});
refreshBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
const conversationId = e.target.dataset.id;
this.refreshConversation(conversationId);
});
});
}
/**
* View conversation details
* @param {string} conversationId - ID of conversation to view
*/
viewConversation(conversationId) {
const conversation = this.conversations.find(conv => conv.id === conversationId);
if (conversation) {
this.stateService.setSelectedConversation(conversation);
// Could trigger a modal or navigation to detail view
console.log('Viewing conversation:', conversation);
}
}
/**
* Refresh conversation status
* @param {string} conversationId - ID of conversation to refresh
*/
async refreshConversation(conversationId) {
try {
// Force refresh of conversation states
this.dataService.clearCacheEntry('/api/conversation-state');
const states = await this.dataService.getConversationStates();
this.stateService.updateConversationStates(states);
} catch (error) {
console.error('Error refreshing conversation:', error);
}
}
/**
* Update pagination controls
* @param {number} totalItems - Total number of items
*/
updatePagination(totalItems) {
const totalPages = Math.ceil(totalItems / this.pageSize);
const startItem = (this.currentPage - 1) * this.pageSize + 1;
const endItem = Math.min(this.currentPage * this.pageSize, totalItems);
// Update pagination text
const paginationText = this.container.querySelector('#pagination-text');
paginationText.textContent = `Showing ${startItem}-${endItem} of ${totalItems} conversations`;
// Update page info
const pageInfo = this.container.querySelector('#page-info');
pageInfo.textContent = `Page ${this.currentPage} of ${totalPages}`;
// Update button states
const prevBtn = this.container.querySelector('#prev-page');
const nextBtn = this.container.querySelector('#next-page');
prevBtn.disabled = this.currentPage === 1;
nextBtn.disabled = this.currentPage === totalPages || totalPages === 0;
}
/**
* Update sort headers visual state
*/
updateSortHeaders() {
const headers = this.container.querySelectorAll('.sortable');
headers.forEach(header => {
header.classList.remove('sort-asc', 'sort-desc');
if (header.dataset.field === this.currentSort.field) {
header.classList.add(`sort-${this.currentSort.direction}`);
}
});
}
/**
* Format relative time
* @param {string} dateString - Date string to format
* @returns {string} Formatted relative time
*/
formatRelativeTime(dateString) {
const date = new Date(dateString);
const now = new Date();
const diff = now - date;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}d ago`;
if (hours > 0) return `${hours}h ago`;
if (minutes > 0) return `${minutes}m ago`;
return 'Just now';
}
/**
* Show error message
* @param {string} message - Error message to display
*/
showError(message) {
const tbody = this.container.querySelector('#conversation-tbody');
tbody.innerHTML = `
<tr class="error-row">
<td colspan="7" class="error-cell">
<span class="error-message">⚠️ ${message}</span>
</td>
</tr>
`;
}
/**
* Update conversation state in real-time
* @param {string} conversationId - ID of conversation
* @param {string} newState - New state
*/
updateConversationState(conversationId, newState) {
const conversation = this.conversations.find(conv => conv.id === conversationId);
if (conversation) {
conversation.status = newState;
this.updateTable();
}
}
/**
* Cleanup event listeners
*/
destroy() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
}
// Export for module use
if (typeof module !== 'undefined' && module.exports) {
module.exports = ConversationTable;
}