Skip to main content
Glama

Interactive Feedback MCP

by zivhdinfo
smart-file-picker.js14.1 kB
/** * Smart File Picker for Intelligent File Suggestions * Integrates context analysis and file scoring for intelligent autocomplete */ class SmartFilePicker { constructor() { this.contextAnalyzer = new ContextAnalyzer(); this.fileScorer = new FileScorer(); // Configuration options this.config = { minRelevanceScore: 30, // Minimum score to show suggestion maxSuggestions: 15, // Maximum number of suggestions to show enableLearning: true, // Enable usage learning showScoreDebug: false, // Show scores in UI (for debugging) groupByCategory: true, // Group suggestions by category highlightMatches: true // Highlight matching keywords in UI }; // Category definitions for grouping this.categories = { 'high': { name: 'High Relevance', icon: '🔥', minScore: 80 }, 'medium': { name: 'Medium Relevance', icon: '⭐', minScore: 50 }, 'low': { name: 'Low Relevance', icon: '💡', minScore: 30 } }; // Cache for performance this.cache = new Map(); this.cacheTimeout = 5000; // 5 seconds } /** * Get intelligent file suggestions based on context * @param {string} textBeforeAt - Text before @ symbol * @param {Array} allFiles - Array of all available files * @returns {Promise<Object>} Suggestion result with categorized files */ async getIntelligentSuggestions(textBeforeAt, allFiles) { try { // Check cache first const cacheKey = this.generateCacheKey(textBeforeAt, allFiles.length); const cached = this.getCachedResult(cacheKey); if (cached) { return cached; } // Analyze context const context = this.contextAnalyzer.analyzeContext(textBeforeAt); // Score all files const scoredFiles = await this.scoreAllFiles(allFiles, context); // Filter and sort suggestions const filteredSuggestions = this.filterSuggestions(scoredFiles); // Group suggestions by category const categorizedSuggestions = this.categorizeSuggestions(filteredSuggestions); // Prepare final result const result = { context: context, suggestions: categorizedSuggestions, totalCount: filteredSuggestions.length, hasIntelligentMatches: filteredSuggestions.length > 0, fallbackToAll: filteredSuggestions.length === 0 }; // Cache result this.setCachedResult(cacheKey, result); return result; } catch (error) { console.error('Error getting intelligent suggestions:', error); return this.getFallbackSuggestions(allFiles); } } /** * Score all files against the context * @param {Array} allFiles - Array of all files * @param {Object} context - Context analysis result * @returns {Promise<Array>} Array of scored files */ async scoreAllFiles(allFiles, context) { const scoredFiles = []; // Process files in batches for better performance const batchSize = 50; for (let i = 0; i < allFiles.length; i += batchSize) { const batch = allFiles.slice(i, i + batchSize); const batchResults = batch.map(file => { return this.fileScorer.scoreFile(file, context); }); scoredFiles.push(...batchResults); // Allow UI to remain responsive if (i + batchSize < allFiles.length) { await this.sleep(1); } } return scoredFiles; } /** * Filter suggestions based on minimum score and limits * @param {Array} scoredFiles - Array of scored files * @returns {Array} Filtered suggestions */ filterSuggestions(scoredFiles) { return scoredFiles .filter(result => result.totalScore >= this.config.minRelevanceScore) .sort((a, b) => b.totalScore - a.totalScore) .slice(0, this.config.maxSuggestions); } /** * Categorize suggestions by relevance level * @param {Array} suggestions - Filtered suggestions * @returns {Object} Categorized suggestions */ categorizeSuggestions(suggestions) { if (!this.config.groupByCategory) { return { all: suggestions }; } const categorized = { high: [], medium: [], low: [] }; suggestions.forEach(suggestion => { const score = suggestion.totalScore; if (score >= this.categories.high.minScore) { categorized.high.push(suggestion); } else if (score >= this.categories.medium.minScore) { categorized.medium.push(suggestion); } else { categorized.low.push(suggestion); } }); return categorized; } /** * Get fallback suggestions when intelligent matching fails * @param {Array} allFiles - Array of all files * @returns {Object} Fallback suggestion result */ getFallbackSuggestions(allFiles) { // Sort by common project files and recent usage const fallbackFiles = allFiles .map(file => ({ file: file, totalScore: this.calculateFallbackScore(file), scores: { fallback: true }, relevanceReason: 'fallback suggestion', matchedKeywords: [] })) .sort((a, b) => b.totalScore - a.totalScore) .slice(0, this.config.maxSuggestions); return { context: this.contextAnalyzer.getDefaultContext(), suggestions: { all: fallbackFiles }, totalCount: fallbackFiles.length, hasIntelligentMatches: false, fallbackToAll: true }; } /** * Calculate fallback score for files when no context is available * @param {Object} file - File object * @returns {number} Fallback score */ calculateFallbackScore(file) { let score = 0; // Bonus for common project files const commonFiles = ['package.json', 'README.md', 'index.js', 'main.js', 'app.js']; if (commonFiles.some(common => file.name.toLowerCase().includes(common.toLowerCase()))) { score += 50; } // Bonus for recent usage score += this.fileScorer.calculateUsageScore(file); // Bonus for shorter paths (likely more important files) const pathDepth = file.path.split('/').length; score += Math.max(0, 20 - pathDepth * 2); return score; } /** * Record file selection for learning * @param {string} textBeforeAt - Context text * @param {Object} selectedFile - Selected file * @param {Array} allSuggestions - All suggestions that were shown */ recordSelection(textBeforeAt, selectedFile, allSuggestions) { if (!this.config.enableLearning) return; try { // Record usage in file scorer this.fileScorer.recordUsage(selectedFile.path); // Analyze what made this selection good const context = this.contextAnalyzer.analyzeContext(textBeforeAt); const selectedScore = this.fileScorer.scoreFile(selectedFile, context); // Log for future algorithm improvements this.logSelectionData({ context: context, selectedFile: selectedFile, selectedScore: selectedScore, allSuggestions: allSuggestions, timestamp: Date.now() }); } catch (error) { console.error('Error recording selection:', error); } } /** * Generate cache key for suggestions * @param {string} textBeforeAt - Context text * @param {number} fileCount - Number of files * @returns {string} Cache key */ generateCacheKey(textBeforeAt, fileCount) { const contextHash = this.simpleHash(textBeforeAt.trim().toLowerCase()); return `${contextHash}_${fileCount}`; } /** * Get cached result if available and not expired * @param {string} cacheKey - Cache key * @returns {Object|null} Cached result or null */ getCachedResult(cacheKey) { const cached = this.cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { return cached.result; } return null; } /** * Set cached result * @param {string} cacheKey - Cache key * @param {Object} result - Result to cache */ setCachedResult(cacheKey, result) { this.cache.set(cacheKey, { result: result, timestamp: Date.now() }); // Clean old cache entries if (this.cache.size > 100) { const oldestKey = this.cache.keys().next().value; this.cache.delete(oldestKey); } } /** * Simple hash function for cache keys * @param {string} str - String to hash * @returns {string} Hash value */ simpleHash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return hash.toString(36); } /** * Sleep function for async processing * @param {number} ms - Milliseconds to sleep * @returns {Promise} Promise that resolves after delay */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Log selection data for algorithm improvement * @param {Object} selectionData - Selection data to log */ logSelectionData(selectionData) { // In a real implementation, this would send data to analytics if (this.config.showScoreDebug) { console.log('Selection Data:', selectionData); } } /** * Update configuration * @param {Object} newConfig - New configuration options */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } /** * Clear cache */ clearCache() { this.cache.clear(); } /** * Get statistics about suggestions * @returns {Object} Statistics object */ getStatistics() { return { cacheSize: this.cache.size, usageStats: this.fileScorer.getUsageStats(), config: this.config }; } /** * Format suggestion for display in UI * @param {Object} suggestion - Suggestion object * @returns {Object} Formatted suggestion for UI */ formatSuggestionForUI(suggestion) { const file = suggestion.file; const score = suggestion.totalScore; const reason = suggestion.relevanceReason; const keywords = suggestion.matchedKeywords; return { path: file.path, name: file.name, type: file.type, score: this.config.showScoreDebug ? score : undefined, reason: reason, keywords: keywords, category: this.getScoreCategory(score), displayName: this.generateDisplayName(file, keywords) }; } /** * Get score category for a given score * @param {number} score - Relevance score * @returns {string} Category name */ getScoreCategory(score) { if (score >= this.categories.high.minScore) return 'high'; if (score >= this.categories.medium.minScore) return 'medium'; return 'low'; } /** * Generate display name with highlighted keywords * @param {Object} file - File object * @param {Array} keywords - Matched keywords * @returns {string} Display name with highlights */ generateDisplayName(file, keywords) { if (!this.config.highlightMatches || keywords.length === 0) { return file.name; } let displayName = file.name; // Highlight matched keywords keywords.forEach(keyword => { const regex = new RegExp(`(${keyword})`, 'gi'); displayName = displayName.replace(regex, '<mark>$1</mark>'); }); return displayName; } /** * Debug method to analyze suggestion quality * @param {string} textBeforeAt - Context text * @param {Array} allFiles - All files */ async debugSuggestions(textBeforeAt, allFiles) { console.group('Smart File Picker Debug'); const result = await this.getIntelligentSuggestions(textBeforeAt, allFiles); console.log('Input Text:', textBeforeAt); console.log('Context Analysis:', result.context); console.log('Total Files:', allFiles.length); console.log('Suggestions Found:', result.totalCount); console.log('Has Intelligent Matches:', result.hasIntelligentMatches); console.log('Categorized Suggestions:', result.suggestions); if (result.suggestions.high?.length > 0) { console.log('Top Suggestion:', result.suggestions.high[0]); } console.groupEnd(); return result; } } // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = SmartFilePicker; }

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/zivhdinfo/interactive-feedback-mcp-nodejs'

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