Skip to main content
Glama
juanqui
by juanqui
DocumentList.js16.2 kB
/** * DocumentList Component * Handles document listing, filtering, and management */ class DocumentList { constructor(app) { this.app = app; this.container = document.getElementById('documents-list'); this.searchInput = document.getElementById('documents-search'); this.refreshBtn = document.getElementById('refresh-documents'); this.paginationContainer = document.getElementById('documents-pagination'); this.filteredDocuments = []; this.currentFilter = ''; this.init(); } /** * Initialize the component */ init() { this.setupEventListeners(); // Register with app this.app.components.documentList = this; console.log('DocumentList component initialized'); } /** * Set up event listeners */ setupEventListeners() { // Search/filter input if (this.searchInput) { this.searchInput.addEventListener('input', this.app.debounce((e) => { this.filterDocuments(e.target.value); }, 300)); } // Pagination controls const prevBtn = document.getElementById('pagination-prev'); const nextBtn = document.getElementById('pagination-next'); if (prevBtn) { prevBtn.addEventListener('click', () => this.goToPreviousPage()); } if (nextBtn) { nextBtn.addEventListener('click', () => this.goToNextPage()); } } /** * Render documents list */ render(documents) { if (!this.container) return; this.filteredDocuments = this.currentFilter ? this.filterDocumentsByTerm(documents, this.currentFilter) : documents; if (this.filteredDocuments.length === 0) { this.renderEmptyState(); return; } this.container.innerHTML = ''; this.filteredDocuments.forEach(document => { const documentCard = this.createDocumentCard(document); this.container.appendChild(documentCard); }); } /** * Create a document card element */ createDocumentCard(doc) { const card = document.createElement('div'); card.className = 'document-card'; card.setAttribute('data-document-id', doc.id); // Status indicator const status = this.getDocumentStatus(doc); const statusClass = status.toLowerCase().replace(/\s+/g, '-'); card.innerHTML = ` <div class="document-header"> <div> <h3 class="document-title">${this.escapeHtml(doc.title || doc.filename)}</h3> <p class="document-path">${this.escapeHtml(doc.path)}</p> </div> <div class="document-status ${statusClass}"> ${this.getStatusIcon(status)} <span>${status}</span> </div> </div> <div class="document-meta"> <div class="document-meta-item"> <div class="document-meta-label">File Size</div> <div class="document-meta-value">${this.app.formatFileSize(doc.file_size)}</div> </div> <div class="document-meta-item"> <div class="document-meta-label">Pages</div> <div class="document-meta-value">${doc.page_count}</div> </div> <div class="document-meta-item"> <div class="document-meta-label">Chunks</div> <div class="document-meta-value">${doc.chunk_count}</div> </div> <div class="document-meta-item"> <div class="document-meta-label">Added</div> <div class="document-meta-value">${this.app.formatDate(doc.added_at)}</div> </div> </div> <div class="document-actions"> <button class="document-action" data-action="view" data-document-id="${doc.id}"> View Details </button> <button class="document-action" data-action="preview" data-document-id="${doc.id}"> Preview </button> <button class="document-action danger" data-action="remove" data-document-id="${doc.id}"> Remove </button> </div> `; // Add click handler for the card (navigate to detail view) card.addEventListener('click', (e) => { // Don't trigger if clicking on action buttons if (e.target.matches('.document-action') || e.target.closest('.document-action')) { return; } this.viewDocumentDetail(doc); }); // Add action button handlers this.setupCardActions(card); return card; } /** * Set up action button handlers for a document card */ setupCardActions(card) { const actions = card.querySelectorAll('.document-action'); actions.forEach(button => { button.addEventListener('click', async (e) => { e.stopPropagation(); // Prevent card click const action = button.dataset.action; const documentId = button.dataset.documentId; const document = this.app.documents.find(d => d.id === documentId); if (!document) return; switch (action) { case 'view': this.viewDocumentDetail(document); break; case 'preview': await this.previewDocument(document); break; case 'remove': this.confirmRemoveDocument(document); break; } }); }); } /** * View document detail */ async viewDocumentDetail(doc) { try { this.app.currentDocument = doc; // Load full document details if not already loaded const detailResponse = await this.app.getDocumentDetail(doc.id, true); if (this.app.components.documentDetail) { this.app.components.documentDetail.render(detailResponse); } this.app.navigateTo('document-detail'); } catch (error) { console.error('Failed to load document detail:', error); this.app.showToast('error', 'Error', 'Failed to load document details'); } } /** * Preview document content */ async previewDocument(doc) { try { const previewResponse = await this.app.apiRequest(`/documents/${doc.id}/preview`); // Create a simple preview modal or panel this.showPreviewModal(doc, previewResponse.content); } catch (error) { console.error('Failed to load document preview:', error); this.app.showToast('error', 'Error', 'Failed to load document preview'); } } /** * Show preview modal */ showPreviewModal(doc, content) { // Create modal HTML const modal = document.createElement('div'); modal.className = 'modal active'; modal.innerHTML = ` <div class="modal-backdrop"></div> <div class="modal-content" style="max-width: 800px; max-height: 80vh;"> <div class="modal-header"> <h3>Preview: ${this.escapeHtml(doc.filename)}</h3> <button class="modal-close preview-modal-close"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <line x1="18" y1="6" x2="6" y2="18"/> <line x1="6" y1="6" x2="18" y2="18"/> </svg> </button> </div> <div class="modal-body" style="overflow-y: auto; max-height: 60vh;"> <pre style="white-space: pre-wrap; font-family: inherit; font-size: 0.9rem; line-height: 1.6; color: var(--color-text-secondary);">${this.escapeHtml(content)}</pre> </div> <div class="modal-footer"> <button class="btn btn-secondary preview-modal-close">Close</button> <button class="btn btn-primary" onclick="window.pdfkbApp.components.documentList.viewDocumentDetail(window.pdfkbApp.documents.find(d => d.id === '${doc.id}'))"> View Full Details </button> </div> </div> `; // Add close handlers const closeButtons = modal.querySelectorAll('.preview-modal-close'); const backdrop = modal.querySelector('.modal-backdrop'); const closeModal = () => { modal.remove(); }; closeButtons.forEach(btn => btn.addEventListener('click', closeModal)); backdrop.addEventListener('click', closeModal); // Add to document document.body.appendChild(modal); // ESC key handler const escHandler = (e) => { if (e.key === 'Escape') { closeModal(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); } /** * Confirm document removal */ confirmRemoveDocument(doc) { const title = 'Remove Document'; const message = `Are you sure you want to remove "${doc.filename}" from the knowledgebase? This action cannot be undone.`; this.app.showConfirmModal(title, message, async () => { await this.removeDocument(doc); }); } /** * Remove document */ async removeDocument(doc) { try { await this.app.removeDocument(doc.id); // Note: Don't show toast here - the app.removeDocument() handles the client-initiated action marking // and prevents duplicate notifications from WebSocket events } catch (error) { console.error('Failed to remove document:', error); this.app.showToast('error', 'Remove Failed', 'Failed to remove document. Please try again.'); } } /** * Filter documents by search term */ filterDocuments(searchTerm) { this.currentFilter = searchTerm.toLowerCase().trim(); this.render(this.app.documents); } /** * Filter documents array by search term */ filterDocumentsByTerm(documents, term) { if (!term) return documents; return documents.filter(doc => { const searchFields = [ doc.title, doc.filename, doc.path, ].filter(Boolean); return searchFields.some(field => field.toLowerCase().includes(term) ); }); } /** * Get document processing status */ getDocumentStatus(document) { // Use the processing_status field from the API if available if (document.processing_status) { switch (document.processing_status.toLowerCase()) { case 'completed': return 'Indexed'; case 'processing': return 'Processing'; case 'failed': return 'Error'; default: // Fall through to legacy logic break; } } // Legacy logic for backward compatibility if (!document.has_embeddings) { return 'Processing'; } if (document.chunk_count === 0) { return 'Error'; } return 'Indexed'; } /** * Get status icon SVG */ getStatusIcon(status) { const icons = { 'Indexed': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20,6 9,17 4,12"/></svg>', 'Processing': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12c0 4.97-4.03 9-9 9s-9-4.03-9-9 4.03-9 9-9c1.08 0 2.13.19 3.1.54"/></svg>', 'Error': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>' }; return icons[status] || icons['Error']; } /** * Render empty state */ renderEmptyState() { if (!this.container) return; const emptyMessage = this.currentFilter ? `No documents match "${this.currentFilter}"` : 'No documents found'; this.container.innerHTML = ` <div class="empty-state" style="grid-column: 1 / -1;"> <svg class="empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/> <polyline points="14,2 14,8 20,8"/> <line x1="16" y1="13" x2="8" y2="13"/> <line x1="16" y1="17" x2="8" y2="17"/> <polyline points="10,9 9,9 8,9"/> </svg> <h3>${emptyMessage}</h3> <p>${this.currentFilter ? 'Try adjusting your search terms' : 'Upload your first PDF document to get started'}</p> ${!this.currentFilter ? '<button class="btn btn-primary" onclick="window.pdfkbApp.navigateTo(\'upload\')">Upload Document</button>' : ''} </div> `; } /** * Pagination methods */ goToPreviousPage() { if (this.app.currentPage > 1) { this.app.goToPage(this.app.currentPage - 1); } } goToNextPage() { this.app.goToPage(this.app.currentPage + 1); } /** * Refresh documents list */ async refresh() { await this.app.loadDocuments(true); } /** * Update display when documents change */ onDocumentsUpdated(documents) { this.render(documents); } /** * Handle real-time document updates */ onDocumentAdded(documentData) { // Refresh the list to show new document this.refresh(); } onDocumentRemoved(documentId) { // Remove from current display const card = this.container?.querySelector(`[data-document-id="${documentId}"]`); if (card) { card.remove(); } // Update empty state if no documents left if (this.app.documents.length === 0) { this.renderEmptyState(); } } onDocumentUpdated(documentData) { // Find and update the specific document card const card = this.container?.querySelector(`[data-document-id="${documentData.document_id}"]`); if (card) { const document = this.app.documents.find(d => d.id === documentData.document_id); if (document) { // Update document data Object.assign(document, documentData); // Re-render the card const newCard = this.createDocumentCard(document); card.replaceWith(newCard); } } } /** * Utility methods */ escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } // Initialize when app is ready document.addEventListener('DOMContentLoaded', () => { // Wait for app to be initialized const initComponent = () => { if (window.pdfkbApp) { new DocumentList(window.pdfkbApp); } else { setTimeout(initComponent, 100); } }; initComponent(); }); // Export for module usage if (typeof module !== 'undefined' && module.exports) { module.exports = DocumentList; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/juanqui/pdfkb-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server