Skip to main content
Glama
juanqui
by juanqui
SearchInterface.js14.7 kB
/** * SearchInterface Component * Handles document search functionality and results display */ class SearchInterface { constructor(app) { this.app = app; this.searchInput = document.getElementById('search-input'); this.searchButton = document.getElementById('search-button'); this.searchLimit = document.getElementById('search-limit'); this.minScore = document.getElementById('min-score'); this.minScoreValue = document.getElementById('min-score-value'); this.resultsContainer = document.getElementById('search-results'); this.loadingState = document.getElementById('search-results-loading'); this.emptyState = document.getElementById('search-empty'); this.currentQuery = ''; this.searchOptions = { limit: 10, min_score: 0.0, include_chunks: true }; this.init(); } /** * Initialize the component */ init() { this.setupEventListeners(); this.updateMinScoreDisplay(); // Register with app this.app.components.searchInterface = this; console.log('SearchInterface component initialized'); } /** * Set up event listeners */ setupEventListeners() { // Search input - trigger on Enter key if (this.searchInput) { this.searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this.performSearch(); } }); // Real-time search suggestions (debounced) this.searchInput.addEventListener('input', this.app.debounce((e) => { this.handleSearchInput(e.target.value); }, 500)); } // Search button if (this.searchButton) { this.searchButton.addEventListener('click', () => { this.performSearch(); }); } // Search options if (this.searchLimit) { this.searchLimit.addEventListener('change', (e) => { this.searchOptions.limit = parseInt(e.target.value); if (this.currentQuery) { this.performSearch(); } }); } if (this.minScore) { this.minScore.addEventListener('input', (e) => { this.searchOptions.min_score = parseFloat(e.target.value); this.updateMinScoreDisplay(); if (this.currentQuery) { this.performSearch(); } }); } } /** * Handle search input changes */ handleSearchInput(value) { // Could implement search suggestions here // For now, just store the value this.currentQuery = value.trim(); // Clear results if input is empty if (!this.currentQuery) { this.clearResults(); } } /** * Perform search */ async performSearch() { const query = this.searchInput?.value?.trim(); if (!query) { this.showEmptyMessage('Please enter a search query'); return; } this.currentQuery = query; this.showLoading(); try { const results = await this.app.searchDocuments(query, this.searchOptions); // Results are handled by updateResults method called from app } catch (error) { console.error('Search failed:', error); this.showEmptyMessage('Search failed. Please try again.'); this.app.showToast('error', 'Search Error', 'Failed to search documents'); } } /** * Update search results display */ updateResults(results) { if (!this.resultsContainer) return; this.hideLoading(); if (!results || results.length === 0) { this.showEmptyMessage('No results found for your search'); return; } this.showResults(); this.renderResults(results); } /** * Render search results */ renderResults(results) { if (!this.resultsContainer) return; this.resultsContainer.innerHTML = ''; results.forEach((result, index) => { const resultCard = this.createResultCard(result, index); this.resultsContainer.appendChild(resultCard); }); } /** * Create a search result card */ createResultCard(result, index) { const card = document.createElement('div'); card.className = 'search-result'; card.setAttribute('data-result-index', index); card.setAttribute('data-document-id', result.document_id); // Highlight search terms in the content const highlightedContent = this.highlightSearchTerms(result.chunk_text, this.currentQuery); card.innerHTML = ` <div class="search-result-header"> <div> <h3 class="search-result-title">${this.escapeHtml(result.document_title || 'Untitled Document')}</h3> <p class="search-result-path">${this.escapeHtml(result.document_path)}</p> </div> <div class="search-result-score" title="Relevance Score"> ${(result.score * 100).toFixed(1)}% </div> </div> <div class="search-result-content"> ${highlightedContent} </div> <div class="search-result-meta"> ${result.page_number ? `<span>Page ${result.page_number}</span>` : ''} <span>Chunk ${result.chunk_index + 1}</span> <span>Document ID: ${result.document_id}</span> </div> `; // Add click handler to view document detail card.addEventListener('click', () => { this.viewDocumentFromResult(result); }); return card; } /** * View document from search result */ async viewDocumentFromResult(result) { try { // Find document in app's documents array let document = this.app.documents.find(d => d.id === result.document_id); // If not found, try to load it from API if (!document) { const response = await this.app.getDocumentDetail(result.document_id, false); document = response.document; } if (document && this.app.components.documentDetail) { this.app.currentDocument = document; // Load full details with chunks const detailResponse = await this.app.getDocumentDetail(result.document_id, true); this.app.components.documentDetail.render(detailResponse, { highlightChunk: result.chunk_id, searchQuery: this.currentQuery }); this.app.navigateTo('document-detail'); } } catch (error) { console.error('Failed to view document from search result:', error); this.app.showToast('error', 'Error', 'Failed to load document details'); } } /** * Highlight search terms in text */ highlightSearchTerms(text, query) { if (!query || !text) return this.escapeHtml(text); // Split query into individual terms const terms = query.toLowerCase().split(/\s+/).filter(term => term.length > 0); let highlightedText = this.escapeHtml(text); // Highlight each term terms.forEach(term => { const regex = new RegExp(`(${this.escapeRegex(term)})`, 'gi'); highlightedText = highlightedText.replace(regex, '<mark>$1</mark>'); }); return highlightedText; } /** * Escape regex special characters */ escapeRegex(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Get search suggestions (placeholder for future implementation) */ async getSearchSuggestions(query) { try { const response = await this.app.apiRequest(`/search/suggestions?query=${encodeURIComponent(query)}`); return response.suggestions || []; } catch (error) { console.error('Failed to get search suggestions:', error); return []; } } /** * Show search suggestions (placeholder for future implementation) */ showSearchSuggestions(suggestions) { // Could implement a dropdown with suggestions console.log('Search suggestions:', suggestions); } /** * Update min score display */ updateMinScoreDisplay() { if (this.minScoreValue && this.minScore) { this.minScoreValue.textContent = this.minScore.value; } } /** * Show loading state */ showLoading() { if (this.loadingState) this.loadingState.classList.remove('hidden'); if (this.resultsContainer) this.resultsContainer.classList.add('hidden'); if (this.emptyState) this.emptyState.classList.add('hidden'); } /** * Hide loading state */ hideLoading() { if (this.loadingState) this.loadingState.classList.add('hidden'); } /** * Show results container */ showResults() { if (this.resultsContainer) this.resultsContainer.classList.remove('hidden'); if (this.emptyState) this.emptyState.classList.add('hidden'); } /** * Show empty state with message */ showEmptyMessage(message) { this.hideLoading(); if (this.resultsContainer) this.resultsContainer.classList.add('hidden'); if (this.emptyState) { this.emptyState.classList.remove('hidden'); // Update empty state message const emptyText = this.emptyState.querySelector('p'); if (emptyText) { emptyText.textContent = message; } } } /** * Clear search results */ clearResults() { if (this.resultsContainer) { this.resultsContainer.innerHTML = ''; this.resultsContainer.classList.add('hidden'); } if (this.emptyState) this.emptyState.classList.add('hidden'); if (this.loadingState) this.loadingState.classList.add('hidden'); this.currentQuery = ''; } /** * Export search results (placeholder for future implementation) */ async exportResults() { if (!this.app.searchResults || this.app.searchResults.length === 0) { this.app.showToast('warning', 'No Results', 'No search results to export'); return; } try { // Create CSV content const csvContent = this.createCSVFromResults(this.app.searchResults); // Download CSV file this.downloadCSV(csvContent, `search_results_${this.currentQuery}.csv`); this.app.showToast('success', 'Export Complete', 'Search results exported successfully'); } catch (error) { console.error('Failed to export search results:', error); this.app.showToast('error', 'Export Failed', 'Failed to export search results'); } } /** * Create CSV content from search results */ createCSVFromResults(results) { const headers = ['Document Title', 'Document Path', 'Score', 'Page', 'Chunk Index', 'Content']; const csvRows = [headers.join(',')]; results.forEach(result => { const row = [ this.escapeCsvField(result.document_title || 'Untitled'), this.escapeCsvField(result.document_path), (result.score * 100).toFixed(2) + '%', result.page_number || '', result.chunk_index + 1, this.escapeCsvField(result.chunk_text) ]; csvRows.push(row.join(',')); }); return csvRows.join('\n'); } /** * Escape CSV field */ escapeCsvField(field) { if (field === null || field === undefined) return ''; const stringField = String(field); // If field contains comma, quote, or newline, wrap in quotes and escape quotes if (stringField.includes(',') || stringField.includes('"') || stringField.includes('\n')) { return '"' + stringField.replace(/"/g, '""') + '"'; } return stringField; } /** * Download CSV file */ downloadCSV(csvContent, filename) { const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); if (link.download !== undefined) { const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } } /** * Reset search form */ reset() { if (this.searchInput) this.searchInput.value = ''; if (this.searchLimit) this.searchLimit.value = '10'; if (this.minScore) { this.minScore.value = '0'; this.updateMinScoreDisplay(); } this.searchOptions = { limit: 10, min_score: 0.0, include_chunks: true }; this.clearResults(); } /** * Set search query and perform search */ async searchFor(query) { if (this.searchInput) { this.searchInput.value = query; } this.currentQuery = query; await this.performSearch(); } /** * Handle real-time search events from WebSocket */ onSearchPerformed(data) { // Could update search analytics or recent searches console.log('Search performed:', data); } /** * 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 SearchInterface(window.pdfkbApp); } else { setTimeout(initComponent, 100); } }; initComponent(); }); // Export for module usage if (typeof module !== 'undefined' && module.exports) { module.exports = SearchInterface; }

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