// CodexLens Manager - Configuration, Model Management, and Semantic Dependencies // Extracted from cli-manager.js for better maintainability // ============================================================ // UTILITY FUNCTIONS // ============================================================ /** * Escape HTML special characters to prevent XSS */ function escapeHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // ============================================================ // CODEXLENS CONFIGURATION MODAL // ============================================================ /** * Show CodexLens configuration modal */ async function showCodexLensConfigModal() { try { showRefreshToast(t('codexlens.loadingConfig'), 'info'); // Fetch current config const response = await fetch('/api/codexlens/config'); const config = await response.json(); const modalHtml = buildCodexLensConfigContent(config); // Create and show modal const tempContainer = document.createElement('div'); tempContainer.innerHTML = modalHtml; const modal = tempContainer.firstElementChild; document.body.appendChild(modal); // Initialize icons if (window.lucide) lucide.createIcons(); // Initialize event handlers initCodexLensConfigEvents(config); } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Build CodexLens configuration modal content - Tabbed Layout */ function buildCodexLensConfigContent(config) { const indexDir = config.index_dir || '~/.codexlens/indexes'; const indexCount = config.index_count || 0; const isInstalled = window.cliToolsStatus?.codexlens?.installed || false; const embeddingCoverage = config.embedding_coverage || 0; const apiMaxWorkers = config.api_max_workers || 4; const apiBatchSize = config.api_batch_size || 8; return ''; container.innerHTML = html; if (window.lucide) lucide.createIcons(); return; } // Show models for local backend if (embeddingBackend !== 'litellm') { var models = result.result.models; models.forEach(function(model) { var statusIcon = model.installed ? '' : ''; var sizeText = model.installed ? model.actual_size_mb.toFixed(0) + ' MB' : '~' + model.estimated_size_mb + ' MB'; var actionBtn = model.installed ? '' : ''; html += '
' + '
' + statusIcon + '' + model.profile + '' + '' + model.dimensions + 'd' + '
' + '
' + '' + sizeText + '' + actionBtn + '
' + '
'; }); } else { // LiteLLM backend - show API info html += '
' + '' + '
Using API embeddings
' + '
Model configured via CODEXLENS_EMBEDDING_MODEL
' + '
'; } html += ''; container.innerHTML = html; if (window.lucide) lucide.createIcons(); } catch (err) { container.innerHTML = '
' + escapeHtml(err.message) + '
'; } } /** * Download model (simplified version) */ async function downloadModel(profile) { var modelCard = document.getElementById('model-' + profile); if (!modelCard) return; var originalHTML = modelCard.innerHTML; // Show loading state modelCard.innerHTML = '
' + '
' + '
' + 'Downloading ' + profile + '...' + '
' + '' + '
'; try { var response = await fetch('/api/codexlens/models/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profile: profile }) }); var result = await response.json(); if (result.success) { showRefreshToast('Model downloaded: ' + profile, 'success'); loadModelList(); } else { showRefreshToast('Download failed: ' + result.error, 'error'); modelCard.innerHTML = originalHTML; if (window.lucide) lucide.createIcons(); } } catch (err) { showRefreshToast('Error: ' + err.message, 'error'); modelCard.innerHTML = originalHTML; if (window.lucide) lucide.createIcons(); } } /** * Delete model (simplified) */ async function deleteModel(profile) { if (!confirm('Delete model ' + profile + '?')) { return; } var modelCard = document.getElementById('model-' + profile); if (!modelCard) return; var originalHTML = modelCard.innerHTML; modelCard.innerHTML = '
' + '
' + '
' + 'Deleting...' + '
' + '
'; try { var response = await fetch('/api/codexlens/models/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profile: profile }) }); var result = await response.json(); if (result.success) { showRefreshToast('Model deleted: ' + profile, 'success'); loadModelList(); } else { showRefreshToast('Delete failed: ' + result.error, 'error'); modelCard.innerHTML = originalHTML; if (window.lucide) lucide.createIcons(); } } catch (err) { showRefreshToast('Error: ' + err.message, 'error'); modelCard.innerHTML = originalHTML; if (window.lucide) lucide.createIcons(); } } // ============================================================ // RERANKER MODEL MANAGEMENT // ============================================================ // Available reranker models (fastembed TextCrossEncoder) var RERANKER_MODELS = [ { id: 'ms-marco-mini', name: 'Xenova/ms-marco-MiniLM-L-6-v2', size: 80, desc: 'Fast, lightweight' }, { id: 'ms-marco-12', name: 'Xenova/ms-marco-MiniLM-L-12-v2', size: 120, desc: 'Better accuracy' }, { id: 'bge-base', name: 'BAAI/bge-reranker-base', size: 1040, desc: 'High quality' }, { id: 'jina-tiny', name: 'jinaai/jina-reranker-v1-tiny-en', size: 130, desc: 'Tiny, fast' }, { id: 'jina-turbo', name: 'jinaai/jina-reranker-v1-turbo-en', size: 150, desc: 'Balanced' } ]; /** * Load reranker model list */ async function loadRerankerModelList() { var container = document.getElementById('rerankerModelListContainer'); if (!container) return; try { // Get current reranker config var response = await fetch('/api/codexlens/reranker/config'); var config = await response.json(); var currentModel = config.model_name || 'Xenova/ms-marco-MiniLM-L-6-v2'; var currentBackend = config.backend || 'fastembed'; var html = '
'; // Show current backend status var backendLabel = currentBackend === 'litellm' ? 'API (LiteLLM)' : 'Local (FastEmbed)'; var backendIcon = currentBackend === 'litellm' ? 'cloud' : 'hard-drive'; html += '
' + '
' + '' + '' + backendLabel + '' + '
' + 'via Environment Variables' + '
'; // Show models for local backend only if (currentBackend === 'fastembed' || currentBackend === 'onnx') { RERANKER_MODELS.forEach(function(model) { var isActive = currentModel === model.name; var statusIcon = isActive ? '' : ''; var actionBtn = isActive ? 'Active' : ''; html += '
' + '
' + statusIcon + '' + model.id + '' + '' + model.desc + '' + '
' + '
' + '~' + model.size + ' MB' + actionBtn + '
' + '
'; }); } else { // LiteLLM backend - show API info html += '
' + '' + '
Using API reranker
' + '
Model configured via CODEXLENS_RERANKER_MODEL
' + '
'; } html += '
'; container.innerHTML = html; if (window.lucide) lucide.createIcons(); } catch (err) { container.innerHTML = '
' + escapeHtml(err.message) + '
'; } } /** * Update reranker backend */ async function updateRerankerBackend(backend) { try { var response = await fetch('/api/codexlens/reranker/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ backend: backend }) }); var result = await response.json(); if (result.success) { showRefreshToast('Reranker backend updated: ' + backend, 'success'); loadRerankerModelList(); } else { showRefreshToast('Failed to update: ' + (result.error || 'Unknown error'), 'error'); } } catch (err) { showRefreshToast('Error: ' + err.message, 'error'); } } /** * Select reranker model */ async function selectRerankerModel(modelName) { try { var response = await fetch('/api/codexlens/reranker/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_name: modelName }) }); var result = await response.json(); if (result.success) { showRefreshToast('Reranker model selected: ' + modelName.split('/').pop(), 'success'); loadRerankerModelList(); } else { showRefreshToast('Failed to select: ' + (result.error || 'Unknown error'), 'error'); } } catch (err) { showRefreshToast('Error: ' + err.message, 'error'); } } // ============================================================ // MODEL TAB & MODE MANAGEMENT // ============================================================ /** * Switch between Embedding and Reranker tabs */ function switchModelTab(tabName) { console.log('[CodexLens] Switching to tab:', tabName); // Update tab buttons using direct style manipulation for reliability var tabs = document.querySelectorAll('.model-tab'); tabs.forEach(function(tab) { var isActive = tab.getAttribute('data-tab') === tabName; if (isActive) { tab.className = 'model-tab flex-1 px-4 py-2.5 text-sm font-medium border-b-2 border-primary text-primary'; tab.style.backgroundColor = 'rgba(var(--primary), 0.05)'; } else { tab.className = 'model-tab flex-1 px-4 py-2.5 text-sm font-medium border-b-2 border-transparent text-muted-foreground'; tab.style.backgroundColor = ''; } }); // Update tab content var embeddingContent = document.getElementById('embeddingTabContent'); var rerankerContent = document.getElementById('rerankerTabContent'); if (embeddingContent && rerankerContent) { if (tabName === 'embedding') { embeddingContent.style.display = 'block'; rerankerContent.style.display = 'none'; } else { embeddingContent.style.display = 'none'; rerankerContent.style.display = 'block'; } } } /** * Update model mode (Local vs API) */ function updateModelMode(mode) { var gpuContainer = document.getElementById('gpuSelectContainer'); var modeSelect = document.getElementById('modelModeSelect'); // Show/hide GPU selector based on mode if (gpuContainer) { if (mode === 'local') { gpuContainer.classList.remove('hidden'); loadGpuDevicesForModeSelector(); } else { gpuContainer.classList.add('hidden'); } } // Store mode preference (will be saved when locked) if (modeSelect) { modeSelect.setAttribute('data-current-mode', mode); } } /** * Load GPU devices for mode selector */ async function loadGpuDevicesForModeSelector() { var gpuSelect = document.getElementById('gpuDeviceSelect'); if (!gpuSelect) return; try { var response = await fetch('/api/codexlens/gpu/devices'); var result = await response.json(); var html = ''; if (result.devices && result.devices.length > 0) { result.devices.forEach(function(device, index) { html += ''; }); } gpuSelect.innerHTML = html; } catch (err) { console.error('Failed to load GPU devices:', err); } } /** * Toggle model mode lock (save configuration) */ async function toggleModelModeLock() { var lockBtn = document.getElementById('modelModeLockBtn'); var modeSelect = document.getElementById('modelModeSelect'); var gpuSelect = document.getElementById('gpuDeviceSelect'); if (!lockBtn || !modeSelect) return; var isLocked = lockBtn.getAttribute('data-locked') === 'true'; if (isLocked) { // Unlock - enable editing lockBtn.setAttribute('data-locked', 'false'); lockBtn.innerHTML = 'Lock'; lockBtn.classList.remove('btn-primary'); lockBtn.classList.add('btn-outline'); modeSelect.disabled = false; if (gpuSelect) gpuSelect.disabled = false; if (window.lucide) lucide.createIcons(); } else { // Lock - save configuration var mode = modeSelect.value; var gpuDevice = gpuSelect ? gpuSelect.value : 'auto'; try { // Save embedding backend preference var embeddingBackend = mode === 'local' ? 'fastembed' : 'litellm'; await fetch('/api/codexlens/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ embedding_backend: embeddingBackend, gpu_device: gpuDevice }) }); // Save reranker backend preference var rerankerBackend = mode === 'local' ? 'fastembed' : 'litellm'; await fetch('/api/codexlens/reranker/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ backend: rerankerBackend }) }); // Update UI to locked state lockBtn.setAttribute('data-locked', 'true'); lockBtn.innerHTML = 'Locked'; lockBtn.classList.remove('btn-outline'); lockBtn.classList.add('btn-primary'); modeSelect.disabled = true; if (gpuSelect) gpuSelect.disabled = true; if (window.lucide) lucide.createIcons(); showRefreshToast('Configuration saved: ' + (mode === 'local' ? 'Local (FastEmbed)' : 'API (LiteLLM)'), 'success'); // Refresh model lists to reflect new backend loadModelList(); loadRerankerModelList(); } catch (err) { showRefreshToast('Failed to save configuration: ' + err.message, 'error'); } } } /** * Initialize model mode from saved config */ async function initModelModeFromConfig() { var modeSelect = document.getElementById('modelModeSelect'); var gpuContainer = document.getElementById('gpuSelectContainer'); if (!modeSelect) return; try { var response = await fetch('/api/codexlens/config'); var config = await response.json(); var embeddingBackend = config.embedding_backend || 'fastembed'; var mode = embeddingBackend === 'litellm' ? 'api' : 'local'; modeSelect.value = mode; modeSelect.setAttribute('data-current-mode', mode); // Show GPU selector for local mode if (gpuContainer) { if (mode === 'local') { gpuContainer.classList.remove('hidden'); loadGpuDevicesForModeSelector(); } else { gpuContainer.classList.add('hidden'); } } } catch (err) { console.error('Failed to load model mode config:', err); } } /** * Update compact semantic status badge in header */ async function updateSemanticStatusBadge() { var badge = document.getElementById('semanticStatusBadge'); if (!badge) return; try { var response = await fetch('/api/codexlens/semantic/status'); var result = await response.json(); if (result.available) { var accelerator = result.accelerator || 'CPU'; var badgeClass = 'bg-success/20 text-success'; var icon = 'check-circle'; if (accelerator === 'CUDA') { badgeClass = 'bg-green-500/20 text-green-600'; icon = 'zap'; } else if (accelerator === 'DirectML') { badgeClass = 'bg-blue-500/20 text-blue-600'; icon = 'cpu'; } badge.innerHTML = '' + '' + accelerator + ''; } else { badge.innerHTML = '' + '' + 'Not Ready' + ''; } if (window.lucide) lucide.createIcons(); } catch (err) { badge.innerHTML = 'Error'; } } // ============================================================ // CODEXLENS ACTIONS // ============================================================ /** * Initialize CodexLens index with bottom floating progress bar * @param {string} indexType - 'vector' (with embeddings), 'normal' (FTS only), or 'full' (FTS + Vector) * @param {string} embeddingModel - Model profile: 'code', 'fast' * @param {string} embeddingBackend - Backend: 'fastembed' (local) or 'litellm' (API) * @param {number} maxWorkers - Max concurrent API calls for embedding generation (default: 1) */ async function initCodexLensIndex(indexType, embeddingModel, embeddingBackend, maxWorkers) { indexType = indexType || 'vector'; embeddingModel = embeddingModel || 'code'; embeddingBackend = embeddingBackend || 'fastembed'; maxWorkers = maxWorkers || 1; // For vector/full index with local backend, check if semantic dependencies are available // LiteLLM backend uses remote embeddings and does not require fastembed/ONNX deps. if ((indexType === 'vector' || indexType === 'full') && embeddingBackend !== 'litellm') { try { var semanticResponse = await fetch('/api/codexlens/semantic/status'); var semanticStatus = await semanticResponse.json(); if (!semanticStatus.available) { // Semantic deps not installed - show confirmation dialog var installDeps = confirm( (t('codexlens.semanticNotInstalled') || 'Semantic search dependencies are not installed.') + '\n\n' + (t('codexlens.installDepsPrompt') || 'Would you like to install them now? (This may take a few minutes)\n\nClick "Cancel" to create FTS index only.') ); if (installDeps) { // Install semantic dependencies first showRefreshToast(t('codexlens.installingDeps') || 'Installing semantic dependencies...', 'info'); try { var installResponse = await fetch('/api/codexlens/semantic/install', { method: 'POST' }); var installResult = await installResponse.json(); if (!installResult.success) { showRefreshToast((t('codexlens.depsInstallFailed') || 'Failed to install dependencies') + ': ' + installResult.error, 'error'); // Fall back to FTS only indexType = 'normal'; } else { showRefreshToast(t('codexlens.depsInstalled') || 'Dependencies installed successfully', 'success'); } } catch (err) { showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error'); indexType = 'normal'; } } else { // User chose to skip - create FTS only indexType = 'normal'; } } } catch (err) { console.warn('[CodexLens] Could not check semantic status:', err); // Continue with requested type, backend will handle fallback } } // Remove existing progress bar if any closeCodexLensIndexModal(); // Create bottom floating progress bar var progressBar = document.createElement('div'); progressBar.id = 'codexlensIndexFloating'; progressBar.className = 'fixed bottom-0 left-0 right-0 z-50 bg-card border-t border-border shadow-lg transform transition-transform duration-300'; // Determine display label var indexTypeLabel; if (indexType === 'full') { indexTypeLabel = 'FTS + Vector'; } else if (indexType === 'vector') { indexTypeLabel = 'Vector'; } else { indexTypeLabel = 'FTS'; } // Add model info for vector indexes var modelLabel = ''; if (indexType !== 'normal') { var modelNames = { code: 'Code', fast: 'Fast' }; var backendLabel = embeddingBackend === 'litellm' ? 'API: ' : ''; modelLabel = ' [' + backendLabel + (modelNames[embeddingModel] || embeddingModel) + ']'; } progressBar.innerHTML = '
' + '
' + '
' + '
' + '
' + '
' + '' + t('codexlens.indexing') + ' (' + indexTypeLabel + modelLabel + ')' + '0%' + '
' + '
' + t('codexlens.preparingIndex') + '
' + '
' + '
' + '' + '' + '' + '
' + '
'; document.body.appendChild(progressBar); if (window.lucide) lucide.createIcons(); // For 'full' type, use 'vector' in the API (it creates FTS + embeddings) var apiIndexType = (indexType === 'full') ? 'vector' : indexType; // Start indexing with specified type and model startCodexLensIndexing(apiIndexType, embeddingModel, embeddingBackend, maxWorkers); } /** * Start the indexing process * @param {string} indexType - 'vector' or 'normal' * @param {string} embeddingModel - Model profile: 'code', 'fast' * @param {string} embeddingBackend - Backend: 'fastembed' (local) or 'litellm' (API) * @param {number} maxWorkers - Max concurrent API calls for embedding generation (default: 1) */ async function startCodexLensIndexing(indexType, embeddingModel, embeddingBackend, maxWorkers) { indexType = indexType || 'vector'; embeddingModel = embeddingModel || 'code'; embeddingBackend = embeddingBackend || 'fastembed'; maxWorkers = maxWorkers || 1; var statusText = document.getElementById('codexlensIndexStatus'); var progressBar = document.getElementById('codexlensIndexProgressBar'); var percentText = document.getElementById('codexlensIndexPercent'); var spinner = document.getElementById('codexlensIndexSpinner'); // Setup WebSocket listener for progress events window.codexlensIndexProgressHandler = function(data) { var payload = data.payload || data; console.log('[CodexLens] Progress event received:', payload); if (statusText) statusText.textContent = payload.message || t('codexlens.indexing'); if (progressBar) progressBar.style.width = (payload.percent || 0) + '%'; if (percentText) percentText.textContent = (payload.percent || 0) + '%'; // Handle completion if (payload.stage === 'complete') { handleIndexComplete(true, payload.message); } else if (payload.stage === 'error') { handleIndexComplete(false, payload.message); } }; // Register with notification system if (typeof registerWsEventHandler === 'function') { registerWsEventHandler('CODEXLENS_INDEX_PROGRESS', window.codexlensIndexProgressHandler); console.log('[CodexLens] Registered WebSocket progress handler'); } else { console.warn('[CodexLens] registerWsEventHandler not available'); } try { console.log('[CodexLens] Starting index for:', projectPath, 'type:', indexType, 'model:', embeddingModel, 'backend:', embeddingBackend, 'maxWorkers:', maxWorkers); var response = await fetch('/api/codexlens/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: projectPath, indexType: indexType, embeddingModel: embeddingModel, embeddingBackend: embeddingBackend, maxWorkers: maxWorkers }) }); var result = await response.json(); console.log('[CodexLens] Init result:', result); // Check if completed successfully (WebSocket might have already reported) if (result.success) { // For vector index, check if embeddings were actually generated var embeddingsResult = result.result && result.result.embeddings; if (indexType === 'vector' && embeddingsResult && !embeddingsResult.generated) { // FTS succeeded but embeddings failed - show partial success var errorMsg = embeddingsResult.error || t('codexlens.embeddingsFailed'); handleIndexComplete(false, t('codexlens.ftsSuccessEmbeddingsFailed') || 'FTS index created, but embeddings failed: ' + errorMsg); } else { handleIndexComplete(true, t('codexlens.indexComplete')); } } else if (!result.success) { handleIndexComplete(false, result.error || t('common.unknownError')); } } catch (err) { console.error('[CodexLens] Init error:', err); handleIndexComplete(false, err.message); } } /** * Handle index completion */ function handleIndexComplete(success, message) { var statusText = document.getElementById('codexlensIndexStatus'); var progressBar = document.getElementById('codexlensIndexProgressBar'); var percentText = document.getElementById('codexlensIndexPercent'); var spinner = document.getElementById('codexlensIndexSpinner'); var floatingBar = document.getElementById('codexlensIndexFloating'); // Unregister WebSocket handler if (typeof unregisterWsEventHandler === 'function' && window.codexlensIndexProgressHandler) { unregisterWsEventHandler('CODEXLENS_INDEX_PROGRESS', window.codexlensIndexProgressHandler); } if (success) { if (progressBar) progressBar.style.width = '100%'; if (percentText) percentText.textContent = '100%'; if (statusText) statusText.textContent = t('codexlens.indexComplete'); if (spinner) { spinner.classList.remove('animate-spin', 'border-primary'); spinner.classList.add('border-green-500'); spinner.innerHTML = ''; if (window.lucide) lucide.createIcons(); } if (floatingBar) { floatingBar.classList.add('bg-green-500/10'); } showRefreshToast(t('codexlens.indexSuccess'), 'success'); // Auto-close after 3 seconds setTimeout(function() { closeCodexLensIndexModal(); // Refresh status if (typeof loadCodexLensStatus === 'function') { loadCodexLensStatus().then(function() { renderToolsSection(); if (window.lucide) lucide.createIcons(); }); } }, 3000); } else { if (progressBar) { progressBar.classList.remove('bg-primary'); progressBar.classList.add('bg-destructive'); } if (statusText) statusText.textContent = message || t('codexlens.indexFailed'); if (spinner) { spinner.classList.remove('animate-spin', 'border-primary'); spinner.innerHTML = ''; if (window.lucide) lucide.createIcons(); } if (floatingBar) { floatingBar.classList.add('bg-destructive/10'); } showRefreshToast(t('codexlens.indexFailed') + ': ' + message, 'error'); } } /** * Close floating progress bar */ function closeCodexLensIndexModal() { var floatingBar = document.getElementById('codexlensIndexFloating'); if (floatingBar) { floatingBar.classList.add('translate-y-full'); setTimeout(function() { floatingBar.remove(); }, 300); } // Unregister WebSocket handler if (typeof unregisterWsEventHandler === 'function' && window.codexlensIndexProgressHandler) { unregisterWsEventHandler('CODEXLENS_INDEX_PROGRESS', window.codexlensIndexProgressHandler); } } /** * Cancel the running indexing process */ async function cancelCodexLensIndexing() { var cancelBtn = document.getElementById('codexlensIndexCancelBtn'); var statusText = document.getElementById('codexlensIndexStatus'); // Disable button to prevent double-click if (cancelBtn) { cancelBtn.disabled = true; cancelBtn.textContent = t('common.canceling') || 'Canceling...'; } try { var response = await fetch('/api/codexlens/cancel', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); var result = await response.json(); if (result.success) { if (statusText) statusText.textContent = t('codexlens.indexCanceled') || 'Indexing canceled'; showRefreshToast(t('codexlens.indexCanceled') || 'Indexing canceled', 'info'); // Close the modal after a short delay setTimeout(function() { closeCodexLensIndexModal(); // Refresh status if (typeof loadCodexLensStatus === 'function') { loadCodexLensStatus().then(function() { renderToolsSection(); if (window.lucide) lucide.createIcons(); }); } }, 1000); } else { showRefreshToast(t('codexlens.cancelFailed') + ': ' + result.error, 'error'); // Re-enable button on failure if (cancelBtn) { cancelBtn.disabled = false; cancelBtn.textContent = t('common.cancel'); } } } catch (err) { console.error('[CodexLens] Cancel error:', err); showRefreshToast(t('common.error') + ': ' + err.message, 'error'); // Re-enable button on error if (cancelBtn) { cancelBtn.disabled = false; cancelBtn.textContent = t('common.cancel'); } } } /** * Install CodexLens * Note: Uses CodexLens-specific install wizard from cli-status.js * which calls /api/codexlens/bootstrap (Python venv), not the generic * CLI install that uses npm install -g (NPM packages) */ function installCodexLensFromManager() { // Use the CodexLens-specific install wizard from cli-status.js if (typeof openCodexLensInstallWizard === 'function') { openCodexLensInstallWizard(); } else { // Fallback: inline install wizard if cli-status.js not loaded showCodexLensInstallDialog(); } } /** * Fallback install dialog when cli-status.js is not loaded */ function showCodexLensInstallDialog() { var modal = document.createElement('div'); modal.id = 'codexlensInstallModalFallback'; modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50'; modal.innerHTML = '
' + '
' + '
' + '
' + '' + '
' + '
' + '

' + t('codexlens.installCodexLens') + '

' + '

' + t('codexlens.installDesc') + '

' + '
' + '
' + '
' + '
' + '

' + t('codexlens.whatWillBeInstalled') + '

' + '
    ' + '
  • ' + '' + '' + t('codexlens.pythonVenv') + ' - ' + t('codexlens.pythonVenvDesc') + '' + '
  • ' + '
  • ' + '' + '' + t('codexlens.codexlensPackage') + ' - ' + t('codexlens.codexlensPackageDesc') + '' + '
  • ' + '
  • ' + '' + 'SQLite FTS5 - ' + t('codexlens.sqliteFtsDesc') + '' + '
  • ' + '
' + '
' + '
' + '
' + '' + '
' + '

' + t('codexlens.installLocation') + '

' + '

~/.codexlens/venv

' + '

' + t('codexlens.installTime') + '

' + '
' + '
' + '
' + '' + '
' + '
' + '
' + '' + '' + '
' + '
'; document.body.appendChild(modal); if (window.lucide) lucide.createIcons(); } function closeCodexLensInstallDialogFallback() { var modal = document.getElementById('codexlensInstallModalFallback'); if (modal) modal.remove(); } async function startCodexLensInstallFallback() { var progressDiv = document.getElementById('codexlensInstallProgressFallback'); var installBtn = document.getElementById('codexlensInstallBtnFallback'); var statusText = document.getElementById('codexlensInstallStatusFallback'); var progressBar = document.getElementById('codexlensInstallProgressBarFallback'); progressDiv.classList.remove('hidden'); installBtn.disabled = true; installBtn.innerHTML = '' + t('codexlens.installing') + ''; var stages = [ { progress: 10, text: t('codexlens.creatingVenv') }, { progress: 30, text: t('codexlens.installingPip') }, { progress: 50, text: t('codexlens.installingPackage') }, { progress: 70, text: t('codexlens.settingUpDeps') }, { progress: 90, text: t('codexlens.finalizing') } ]; var currentStage = 0; var progressInterval = setInterval(function() { if (currentStage < stages.length) { statusText.textContent = stages[currentStage].text; progressBar.style.width = stages[currentStage].progress + '%'; currentStage++; } }, 1500); try { var response = await fetch('/api/codexlens/bootstrap', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); clearInterval(progressInterval); var result = await response.json(); if (result.success) { progressBar.style.width = '100%'; statusText.textContent = t('codexlens.installComplete'); setTimeout(function() { closeCodexLensInstallDialogFallback(); showRefreshToast(t('codexlens.installSuccess'), 'success'); // Refresh the page to update status if (typeof loadCodexLensStatus === 'function') { loadCodexLensStatus().then(function() { if (typeof renderCodexLensManager === 'function') renderCodexLensManager(); }); } else { location.reload(); } }, 1000); } else { statusText.textContent = t('common.error') + ': ' + result.error; progressBar.classList.add('bg-destructive'); installBtn.disabled = false; installBtn.innerHTML = ' ' + t('common.retry'); if (window.lucide) lucide.createIcons(); } } catch (err) { clearInterval(progressInterval); statusText.textContent = t('common.error') + ': ' + err.message; progressBar.classList.add('bg-destructive'); installBtn.disabled = false; installBtn.innerHTML = ' ' + t('common.retry'); if (window.lucide) lucide.createIcons(); } } /** * Uninstall CodexLens * Note: Uses CodexLens-specific uninstall wizard from cli-status.js * which calls /api/codexlens/uninstall (Python venv), not the generic * CLI uninstall that uses /api/cli/uninstall (NPM packages) */ function uninstallCodexLensFromManager() { // Use the CodexLens-specific uninstall wizard from cli-status.js if (typeof openCodexLensUninstallWizard === 'function') { openCodexLensUninstallWizard(); } else { // Fallback: inline uninstall wizard if cli-status.js not loaded showCodexLensUninstallDialog(); } } /** * Fallback uninstall dialog when cli-status.js is not loaded */ function showCodexLensUninstallDialog() { var modal = document.createElement('div'); modal.id = 'codexlensUninstallModalFallback'; modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50'; modal.innerHTML = '
' + '
' + '
' + '
' + '' + '
' + '
' + '

' + t('codexlens.uninstall') + '

' + '

' + t('codexlens.uninstallDesc') + '

' + '
' + '
' + '
' + '
' + '

' + t('codexlens.whatWillBeRemoved') + '

' + '
    ' + '
  • ' + '' + '' + t('codexlens.removeVenv') + '' + '
  • ' + '
  • ' + '' + '' + t('codexlens.removeData') + '' + '
  • ' + '
  • ' + '' + '' + t('codexlens.removeConfig') + '' + '
  • ' + '
' + '
' + '' + '
' + '
' + '
' + '' + '' + '
' + '
'; document.body.appendChild(modal); if (window.lucide) lucide.createIcons(); } function closeCodexLensUninstallDialogFallback() { var modal = document.getElementById('codexlensUninstallModalFallback'); if (modal) modal.remove(); } async function startCodexLensUninstallFallback() { var progressDiv = document.getElementById('codexlensUninstallProgressFallback'); var uninstallBtn = document.getElementById('codexlensUninstallBtnFallback'); var statusText = document.getElementById('codexlensUninstallStatusFallback'); var progressBar = document.getElementById('codexlensUninstallProgressBarFallback'); progressDiv.classList.remove('hidden'); uninstallBtn.disabled = true; uninstallBtn.innerHTML = '' + t('codexlens.uninstalling') + ''; var stages = [ { progress: 25, text: t('codexlens.removingVenv') }, { progress: 50, text: t('codexlens.removingData') }, { progress: 75, text: t('codexlens.removingConfig') }, { progress: 90, text: t('codexlens.finalizing') } ]; var currentStage = 0; var progressInterval = setInterval(function() { if (currentStage < stages.length) { statusText.textContent = stages[currentStage].text; progressBar.style.width = stages[currentStage].progress + '%'; currentStage++; } }, 500); try { var response = await fetch('/api/codexlens/uninstall', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); clearInterval(progressInterval); var result = await response.json(); if (result.success) { progressBar.style.width = '100%'; statusText.textContent = t('codexlens.uninstallComplete'); setTimeout(function() { closeCodexLensUninstallDialogFallback(); showRefreshToast(t('codexlens.uninstallSuccess'), 'success'); // Refresh the page to update status if (typeof loadCodexLensStatus === 'function') { loadCodexLensStatus().then(function() { if (typeof renderCodexLensManager === 'function') renderCodexLensManager(); }); } else { location.reload(); } }, 1000); } else { statusText.textContent = t('common.error') + ': ' + result.error; progressBar.classList.add('bg-destructive'); uninstallBtn.disabled = false; uninstallBtn.innerHTML = ' ' + t('common.retry'); if (window.lucide) lucide.createIcons(); } } catch (err) { clearInterval(progressInterval); statusText.textContent = t('common.error') + ': ' + err.message; progressBar.classList.add('bg-destructive'); uninstallBtn.disabled = false; uninstallBtn.innerHTML = ' ' + t('common.retry'); if (window.lucide) lucide.createIcons(); } } /** * Clean current workspace index */ async function cleanCurrentWorkspaceIndex() { if (!confirm(t('codexlens.cleanCurrentWorkspaceConfirm'))) { return; } try { showRefreshToast(t('codexlens.cleaning'), 'info'); // Get current workspace path (projectPath is a global variable from state.js) var workspacePath = projectPath; var response = await fetch('/api/codexlens/clean', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: workspacePath }) }); var result = await response.json(); if (result.success) { showRefreshToast(t('codexlens.cleanCurrentWorkspaceSuccess'), 'success'); // Refresh status if (typeof loadCodexLensStatus === 'function') { await loadCodexLensStatus(); renderToolsSection(); if (window.lucide) lucide.createIcons(); } } else { showRefreshToast(t('codexlens.cleanFailed') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Clean all CodexLens indexes */ async function cleanCodexLensIndexes() { if (!confirm(t('codexlens.cleanConfirm'))) { return; } try { showRefreshToast(t('codexlens.cleaning'), 'info'); var response = await fetch('/api/codexlens/clean', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ all: true }) }); var result = await response.json(); if (result.success) { showRefreshToast(t('codexlens.cleanSuccess'), 'success'); // Refresh status if (typeof loadCodexLensStatus === 'function') { await loadCodexLensStatus(); renderToolsSection(); if (window.lucide) lucide.createIcons(); } } else { showRefreshToast(t('codexlens.cleanFailed') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } // ============================================================ // CODEXLENS MANAGER PAGE (Independent View) // ============================================================ /** * Render CodexLens Manager as an independent page view */ async function renderCodexLensManager() { var container = document.getElementById('mainContent'); if (!container) return; // Hide stats grid and search var statsGrid = document.getElementById('statsGrid'); var searchContainer = document.querySelector('.search-container'); if (statsGrid) statsGrid.style.display = 'none'; if (searchContainer) searchContainer.style.display = 'none'; container.innerHTML = '
' + t('common.loading') + '
'; try { // Use aggregated endpoint for faster page load (single API call) var dashboardData = null; var config = { index_dir: '~/.codexlens/indexes', index_count: 0 }; if (typeof loadCodexLensDashboardInit === 'function') { console.log('[CodexLens] Using aggregated dashboard-init endpoint...'); dashboardData = await loadCodexLensDashboardInit(); if (dashboardData && dashboardData.config) { config = dashboardData.config; console.log('[CodexLens] Dashboard init loaded, config:', config); } } else if (typeof loadCodexLensStatus === 'function') { // Fallback to legacy individual calls console.log('[CodexLens] Fallback to legacy loadCodexLensStatus...'); await loadCodexLensStatus(); var response = await fetch('/api/codexlens/config'); config = await response.json(); } // Load LiteLLM API config for embedding backend options (parallel with page render) var litellmPromise = (async () => { try { console.log('[CodexLens] Loading LiteLLM config...'); var litellmResponse = await fetch('/api/litellm-api/config'); if (litellmResponse.ok) { window.litellmApiConfig = await litellmResponse.json(); console.log('[CodexLens] LiteLLM config loaded, providers:', window.litellmApiConfig?.providers?.length || 0); } } catch (e) { console.warn('[CodexLens] Could not load LiteLLM config:', e); } })(); container.innerHTML = buildCodexLensManagerPage(config); if (window.lucide) lucide.createIcons(); initCodexLensManagerPageEvents(config); // Load additional data in parallel (non-blocking) var isInstalled = window.cliToolsStatus?.codexlens?.installed || dashboardData?.installed; // Wait for LiteLLM config before loading semantic deps (it may need provider info) await litellmPromise; // Always load semantic deps status - it needs GPU detection and device list // which are not included in the aggregated endpoint loadSemanticDepsStatus(); loadModelList(); loadRerankerModelList(); // Initialize model mode and semantic status badge updateSemanticStatusBadge(); loadGpuDevicesForModeSelector(); // Initialize file watcher status initWatcherStatus(); // Load index stats for the Index Manager section if (isInstalled) { loadIndexStatsForPage(); // Check index health based on git history checkIndexHealth(); } } catch (err) { container.innerHTML = '

' + t('common.error') + ': ' + escapeHtml(err.message) + '

'; if (window.lucide) lucide.createIcons(); } } /** * Build CodexLens Manager page content */ function buildCodexLensManagerPage(config) { var indexDir = config.index_dir || '~/.codexlens/indexes'; var indexCount = config.index_count || 0; var isInstalled = window.cliToolsStatus?.codexlens?.installed || false; return '
' + // Header with status '
' + '
' + '
' + '
' + '' + '
' + '
' + '

' + t('codexlens.config') + '

' + '

' + t('codexlens.configDesc') + '

' + '
' + '
' + '
' + (isInstalled ? ' ' + t('codexlens.installed') + '' : ' ' + t('codexlens.notInstalled') + '') + '
' + '' + t('codexlens.indexes') + ':' + '' + indexCount + '' + '
' + '
' + '
' + '
' + (isInstalled ? // Installed: Show full management UI '
' + // Left Column '
' + // Create Index Section - Simplified (model config in Environment Variables) '
' + '

' + t('codexlens.createIndex') + '

' + '
' + // Index Actions - Primary buttons '
' + '' + '' + '
' + // Incremental Update button '' + '

' + t('codexlens.indexTypeHint') + ' Configure embedding model in Environment Variables below.

' + '
' + '
' + // Storage Path Section '
' + '

' + t('codexlens.indexStoragePath') + '

' + '
' + '
' + '' + '
' + indexDir + '
' + '
' + '
' + '' + '
' + '' + '' + '
' + '

' + t('codexlens.pathInfo') + '

' + '
' + '
' + '
' + // Environment Variables Section '
' + '
' + '

Environment Variables

' + '' + '
' + '
' + '
Click Load to view/edit ~/.codexlens/.env
' + '
' + '
' + // Maintenance Section '
' + '

' + t('codexlens.maintenance') + '

' + '
' + '' + '' + '' + '
' + '
' + '
' + // Right Column '
' + // Combined: Semantic Status + Model Management with Tabs '
' + // Compact Header with Semantic Status '
' + '
' + '

' + t('codexlens.modelManagement') + '

' + '
' + 'Checking...' + '
' + '
' + '
' + // GPU Config Section (for local mode) '
' + '
' + 'GPU:' + '' + '
' + '

Backend configured in Environment Variables below

' + '
' + // Tabs for Embedding / Reranker '
' + '
' + '' + '' + '
' + '
' + // Tab Content '
' + // Embedding Tab Content '
' + '
' + '
' + '
' + t('codexlens.loadingModels') + '
' + '
' + '
' + // Reranker Tab Content '' + '
' + '
' + // File Watcher Card '
' + '
' + '
' + '
' + '' + '

File Watcher

' + '
' + '
' + 'Stopped' + '' + '
' + '
' + '
' + '
' + '

Monitor file changes and auto-update index

' + // Stats row '
' + '
' + '
-
' + '
Files
' + '
' + '
' + '
0
' + '
Changes
' + '
' + '
' + '
-
' + '
Uptime
' + '
' + '
' + // Recent activity log '
' + '
' + 'Recent Activity' + '' + '
' + '
' + '
No activity yet. Start watcher to monitor files.
' + '
' + '
' + '
' + '
' + '
' + '
' + // Index Manager Section '
' + '
' + '
' + '' + '' + t('index.manager') + '' + '-' + '...' + '
' + '
' + '' + '
' + '
' + // Index Health Details '' + '
' + '
' + '' + '' + indexDir + '' + '
' + '
' + '
' + '
-
' + '
' + t('index.projects') + '
' + '
' + '
' + '
-
' + '
' + t('index.totalSize') + '
' + '
' + '
' + '
-
' + '
' + t('index.vectorIndexes') + '
' + '
' + '
' + '
-
' + '
' + t('index.ftsIndexes') + '
' + '
' + '
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + t('index.projectId') + '' + t('index.size') + '' + t('index.type') + '' + t('index.lastModified') + '
' + t('common.loading') + '
' + '
' + '
' + '' + '
' + '
' + '
' + // Test Search Section '
' + '

' + t('codexlens.testSearch') + '

' + '
' + '
' + '' + '' + '
' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '
' + '' + '' + '
' + '' + '
' + '
' : // Not installed: Show install prompt '
' + '
' + '
' + '' + '
' + '

' + t('codexlens.installCodexLens') + '

' + '

' + t('codexlens.installFirst') + '

' + '' + '
' + '
' ) + '
'; } /** * Build model select options for the page */ function buildModelSelectOptionsForPage() { var installedModels = window.cliToolsStatus?.codexlens?.installedModels || []; var allModels = window.cliToolsStatus?.codexlens?.allModels || []; if (allModels.length === 0) { // Fallback to default models if not loaded return '' + ''; } var options = ''; allModels.forEach(function(model) { var isInstalled = model.installed || installedModels.includes(model.profile); var label = model.profile + (isInstalled ? ' ✓' : ''); var selected = model.profile === 'code' ? ' selected' : ''; options += ''; }); return options; } /** * Validate concurrency input value (min 1, no max limit) */ function validateConcurrencyInput(input) { var value = parseInt(input.value, 10); if (isNaN(value) || value < 1) { input.value = 1; } } /** * Handle embedding backend change */ function onEmbeddingBackendChange() { var backendSelect = document.getElementById('pageBackendSelect'); var modelSelect = document.getElementById('pageModelSelect'); var concurrencySelector = document.getElementById('concurrencySelector'); var rotationSection = document.getElementById('rotationSection'); if (!backendSelect || !modelSelect) { console.warn('[CodexLens] Backend or model select not found'); return; } var backend = backendSelect.value; console.log('[CodexLens] Backend changed to:', backend); console.log('[CodexLens] Current litellmApiConfig:', window.litellmApiConfig); if (backend === 'litellm') { // Load LiteLLM embedding models console.log('[CodexLens] Building LiteLLM model options...'); var options = buildLiteLLMModelOptions(); console.log('[CodexLens] Built options HTML:', options); modelSelect.innerHTML = options; // Show concurrency selector for API backend if (concurrencySelector) { concurrencySelector.classList.remove('hidden'); } // Show rotation section and load status if (rotationSection) { rotationSection.classList.remove('hidden'); loadRotationStatus(); } } else { // Load local fastembed models modelSelect.innerHTML = buildModelSelectOptionsForPage(); // Hide concurrency selector for local backend if (concurrencySelector) { concurrencySelector.classList.add('hidden'); } // Hide rotation section for local backend if (rotationSection) { rotationSection.classList.add('hidden'); } } } /** * Build LiteLLM model options from config */ function buildLiteLLMModelOptions() { var litellmConfig = window.litellmApiConfig || {}; console.log('[CodexLens] litellmApiConfig:', litellmConfig); var providers = litellmConfig.providers || []; console.log('[CodexLens] providers count:', providers.length); var options = ''; providers.forEach(function(provider) { console.log('[CodexLens] Processing provider:', provider.id, 'enabled:', provider.enabled); if (!provider.enabled) return; // Check embeddingModels array (config structure) var models = provider.embeddingModels || provider.models || []; console.log('[CodexLens] Provider', provider.id, 'embeddingModels:', models.length, models); models.forEach(function(model) { console.log('[CodexLens] Processing model:', model.id, 'type:', model.type, 'enabled:', model.enabled); // Accept embedding type or models from embeddingModels array if (model.type && model.type !== 'embedding') return; if (!model.enabled) return; var label = model.name || model.id; var providerName = provider.name || provider.id; var selected = options === '' ? ' selected' : ''; options += ''; console.log('[CodexLens] Added option:', label, 'from', providerName); }); }); if (options === '') { console.warn('[CodexLens] No embedding models found in LiteLLM config'); options = ''; } return options; } // Make functions globally accessible window.onEmbeddingBackendChange = onEmbeddingBackendChange; /** * Initialize index from page - uses env-based config * Model/backend configured in Environment Variables section */ function initCodexLensIndexFromPage(indexType) { // For FTS-only index, no embedding config needed if (indexType === 'normal') { initCodexLensIndex(indexType); } else { // Use litellm backend with env-configured model (default 4 workers) // The CLI will read EMBEDDING_MODEL/LITELLM_MODEL from env initCodexLensIndex(indexType, null, 'litellm', 4); } } /** * Run incremental update on the current workspace index */ window.runIncrementalUpdate = async function runIncrementalUpdate() { var projectPath = window.CCW_PROJECT_ROOT || '.'; showRefreshToast('Starting incremental update...', 'info'); try { var response = await fetch('/api/codexlens/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: projectPath }) }); var result = await response.json(); if (result.success) { showRefreshToast('Incremental update completed', 'success'); } else { showRefreshToast('Update failed: ' + (result.error || 'Unknown error'), 'error'); } } catch (err) { showRefreshToast('Update error: ' + err.message, 'error'); } } /** * Toggle file watcher (watchdog) on/off */ window.toggleWatcher = async function toggleWatcher() { console.log('[CodexLens] toggleWatcher called'); // Debug: uncomment to test if function is called // alert('toggleWatcher called!'); var projectPath = window.CCW_PROJECT_ROOT || '.'; console.log('[CodexLens] Project path:', projectPath); // Check current status first try { console.log('[CodexLens] Checking watcher status...'); var statusResponse = await fetch('/api/codexlens/watch/status'); var statusResult = await statusResponse.json(); console.log('[CodexLens] Status result:', statusResult); var isRunning = statusResult.success && statusResult.running; // Toggle: if running, stop; if stopped, start var action = isRunning ? 'stop' : 'start'; console.log('[CodexLens] Action:', action); var response = await fetch('/api/codexlens/watch/' + action, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: projectPath }) }); var result = await response.json(); console.log('[CodexLens] Action result:', result); if (result.success) { var newRunning = action === 'start'; updateWatcherUI(newRunning); showRefreshToast('File watcher ' + (newRunning ? 'started' : 'stopped'), 'success'); } else { showRefreshToast('Watcher ' + action + ' failed: ' + (result.error || 'Unknown error'), 'error'); } } catch (err) { console.error('[CodexLens] Watcher error:', err); showRefreshToast('Watcher error: ' + err.message, 'error'); } } /** * Update watcher UI state */ function updateWatcherUI(running, stats) { var statusBadge = document.getElementById('watcherStatusBadge'); if (statusBadge) { var badgeClass = running ? 'bg-success/20 text-success' : 'bg-muted text-muted-foreground'; var badgeText = running ? 'Running' : 'Stopped'; var iconName = running ? 'pause' : 'play'; statusBadge.innerHTML = '' + badgeText + '' + ''; if (window.lucide) lucide.createIcons(); } // Update stats if provided if (stats) { var filesCount = document.getElementById('watcherFilesCount'); var changesCount = document.getElementById('watcherChangesCount'); var uptimeDisplay = document.getElementById('watcherUptimeDisplay'); if (filesCount) filesCount.textContent = stats.files_watched || '-'; if (changesCount) changesCount.textContent = stats.changes_detected || '0'; if (uptimeDisplay) uptimeDisplay.textContent = formatUptime(stats.uptime_seconds); } // Start or stop polling based on running state if (running) { startWatcherPolling(); } else { stopWatcherPolling(); } } // Watcher polling interval var watcherPollInterval = null; var watcherStartTime = null; var watcherChangesCount = 0; /** * Format uptime in human readable format */ function formatUptime(seconds) { if (!seconds || seconds < 0) return '-'; if (seconds < 60) return Math.floor(seconds) + 's'; if (seconds < 3600) return Math.floor(seconds / 60) + 'm'; var hours = Math.floor(seconds / 3600); var mins = Math.floor((seconds % 3600) / 60); return hours + 'h ' + mins + 'm'; } /** * Start polling watcher status */ function startWatcherPolling() { if (watcherPollInterval) return; // Already polling watcherStartTime = Date.now(); watcherPollInterval = setInterval(async function() { try { var response = await fetch('/api/codexlens/watch/status'); var result = await response.json(); if (result.success && result.running) { // Update uptime var uptimeDisplay = document.getElementById('watcherUptimeDisplay'); if (uptimeDisplay) { var uptime = (Date.now() - watcherStartTime) / 1000; uptimeDisplay.textContent = formatUptime(uptime); } // Update files count if available if (result.files_watched !== undefined) { var filesCount = document.getElementById('watcherFilesCount'); if (filesCount) filesCount.textContent = result.files_watched; } // Check for new events if (result.recent_events && result.recent_events.length > 0) { result.recent_events.forEach(function(event) { addWatcherLogEntry(event.type, event.path); }); } } else if (!result.running) { // Watcher stopped externally updateWatcherUI(false); stopWatcherPolling(); } } catch (err) { console.warn('[Watcher] Poll error:', err); } }, 3000); // Poll every 3 seconds } /** * Stop polling watcher status */ function stopWatcherPolling() { if (watcherPollInterval) { clearInterval(watcherPollInterval); watcherPollInterval = null; } watcherStartTime = null; } /** * Add entry to watcher activity log */ function addWatcherLogEntry(type, path) { var logContainer = document.getElementById('watcherActivityLog'); if (!logContainer) return; // Clear "no activity" message if present var noActivity = logContainer.querySelector('.text-muted-foreground:only-child'); if (noActivity && noActivity.textContent.includes('No activity')) { logContainer.innerHTML = ''; } // Increment changes count watcherChangesCount++; var changesCount = document.getElementById('watcherChangesCount'); if (changesCount) changesCount.textContent = watcherChangesCount; // Create log entry var timestamp = new Date().toLocaleTimeString(); var typeColors = { 'created': 'text-success', 'modified': 'text-warning', 'deleted': 'text-destructive', 'renamed': 'text-primary' }; var typeIcons = { 'created': '+', 'modified': '~', 'deleted': '-', 'renamed': '→' }; var colorClass = typeColors[type] || 'text-muted-foreground'; var icon = typeIcons[type] || '•'; // Get just the filename var filename = path.split(/[/\\]/).pop(); var entry = document.createElement('div'); entry.className = 'flex items-center gap-2 py-0.5'; entry.innerHTML = '' + timestamp + '' + '' + icon + '' + '' + escapeHtml(filename) + ''; // Add to top of log logContainer.insertBefore(entry, logContainer.firstChild); // Keep only last 50 entries while (logContainer.children.length > 50) { logContainer.removeChild(logContainer.lastChild); } } /** * Clear watcher activity log */ function clearWatcherLog() { var logContainer = document.getElementById('watcherActivityLog'); if (logContainer) { logContainer.innerHTML = '
Log cleared. Waiting for file changes...
'; } watcherChangesCount = 0; var changesCount = document.getElementById('watcherChangesCount'); if (changesCount) changesCount.textContent = '0'; } /** * Initialize watcher status on page load */ async function initWatcherStatus() { try { var response = await fetch('/api/codexlens/watch/status'); var result = await response.json(); if (result.success) { updateWatcherUI(result.running, { files_watched: result.files_watched, changes_detected: 0, uptime_seconds: result.uptime_seconds }); } } catch (err) { console.warn('[Watcher] Failed to get initial status:', err); } } // Make functions globally accessible window.runIncrementalUpdate = runIncrementalUpdate; window.toggleWatcher = toggleWatcher; window.updateWatcherUI = updateWatcherUI; window.addWatcherLogEntry = addWatcherLogEntry; window.clearWatcherLog = clearWatcherLog; window.initWatcherStatus = initWatcherStatus; /** * Initialize CodexLens Manager page event handlers */ function initCodexLensManagerPageEvents(currentConfig) { var saveBtn = document.getElementById('saveIndexPathBtn'); if (saveBtn) { saveBtn.onclick = async function() { var indexDirInput = document.getElementById('indexDirInput'); var newIndexDir = indexDirInput ? indexDirInput.value.trim() : ''; if (!newIndexDir) { showRefreshToast(t('codexlens.pathEmpty'), 'error'); return; } if (newIndexDir === currentConfig.index_dir) { showRefreshToast(t('codexlens.pathUnchanged'), 'info'); return; } saveBtn.disabled = true; saveBtn.innerHTML = '' + t('common.saving') + ''; try { var response = await fetch('/api/codexlens/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ index_dir: newIndexDir }) }); var result = await response.json(); if (result.success) { showRefreshToast(t('codexlens.configSaved'), 'success'); renderCodexLensManager(); } else { showRefreshToast(t('common.saveFailed') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } saveBtn.disabled = false; saveBtn.innerHTML = ' ' + t('codexlens.saveConfig'); if (window.lucide) lucide.createIcons(); }; } var runSearchBtn = document.getElementById('runSearchBtn'); if (runSearchBtn) { runSearchBtn.onclick = async function() { var searchType = document.getElementById('searchTypeSelect').value; var searchMode = document.getElementById('searchModeSelect').value; var query = document.getElementById('searchQueryInput').value.trim(); var resultsDiv = document.getElementById('searchResults'); var resultCount = document.getElementById('searchResultCount'); var resultContent = document.getElementById('searchResultContent'); if (!query) { showRefreshToast(t('codexlens.enterQuery'), 'warning'); return; } runSearchBtn.disabled = true; runSearchBtn.innerHTML = '' + t('codexlens.searching') + ''; resultsDiv.classList.add('hidden'); try { var endpoint = '/api/codexlens/' + searchType; var params = new URLSearchParams({ query: query, limit: '20' }); if (searchType === 'search' || searchType === 'search_files') { params.append('mode', searchMode); } var response = await fetch(endpoint + '?' + params.toString()); var result = await response.json(); if (result.success) { var results = result.results || result.files || []; resultCount.textContent = results.length + ' ' + t('codexlens.resultsCount'); resultContent.textContent = JSON.stringify(results, null, 2); resultsDiv.classList.remove('hidden'); } else { resultContent.textContent = t('common.error') + ': ' + (result.error || t('common.unknownError')); resultsDiv.classList.remove('hidden'); } } catch (err) { resultContent.textContent = t('common.exception') + ': ' + err.message; resultsDiv.classList.remove('hidden'); } runSearchBtn.disabled = false; runSearchBtn.innerHTML = ' ' + t('codexlens.runSearch'); if (window.lucide) lucide.createIcons(); }; } var searchInput = document.getElementById('searchQueryInput'); if (searchInput) { searchInput.onkeypress = function(e) { if (e.key === 'Enter' && runSearchBtn) { runSearchBtn.click(); } }; } } /** * Show index initialization modal */ function showIndexInitModal() { // Use initCodexLensIndex with default settings initCodexLensIndex('vector', 'code'); } /** * Load index stats for the CodexLens Manager page */ async function loadIndexStatsForPage() { try { var response = await fetch('/api/codexlens/indexes'); if (!response.ok) throw new Error('Failed to load index stats'); var data = await response.json(); renderIndexStatsForPage(data); } catch (err) { console.error('[CodexLens] Failed to load index stats:', err); var tbody = document.getElementById('indexTableBody'); if (tbody) { tbody.innerHTML = '' + escapeHtml(err.message) + ''; } } } /** * Render index stats in the CodexLens Manager page */ function renderIndexStatsForPage(data) { var summary = data.summary || {}; var indexes = data.indexes || []; var indexDir = data.indexDir || ''; // Update summary stats var totalSizeEl = document.getElementById('indexTotalSize'); var projectCountEl = document.getElementById('indexProjectCount'); var totalSizeValEl = document.getElementById('indexTotalSizeVal'); var vectorCountEl = document.getElementById('indexVectorCount'); var ftsCountEl = document.getElementById('indexFtsCount'); var indexDirEl = document.getElementById('indexDirDisplay'); if (totalSizeEl) totalSizeEl.textContent = summary.totalSizeFormatted || '0 B'; if (projectCountEl) projectCountEl.textContent = summary.totalProjects || 0; if (totalSizeValEl) totalSizeValEl.textContent = summary.totalSizeFormatted || '0 B'; if (vectorCountEl) vectorCountEl.textContent = summary.vectorIndexCount || 0; if (ftsCountEl) ftsCountEl.textContent = summary.normalIndexCount || 0; if (indexDirEl && indexDir) { indexDirEl.textContent = indexDir; indexDirEl.title = indexDir; } // Render table rows var tbody = document.getElementById('indexTableBody'); if (!tbody) return; if (indexes.length === 0) { tbody.innerHTML = '' + (t('index.noIndexes') || 'No indexes yet') + ''; return; } var rows = ''; indexes.forEach(function(idx) { var vectorBadge = idx.hasVectorIndex ? '' + (t('index.vector') || 'Vector') + '' : ''; var normalBadge = idx.hasNormalIndex ? '' + (t('index.fts') || 'FTS') + '' : ''; rows += '' + '' + '' + escapeHtml(idx.id) + '' + '' + '' + (idx.sizeFormatted || '-') + '' + '
' + vectorBadge + normalBadge + '
' + '' + formatTimeAgoSimple(idx.lastModified) + '' + '' + '' + '' + ''; }); tbody.innerHTML = rows; if (window.lucide) lucide.createIcons(); } /** * Simple time ago formatter */ function formatTimeAgoSimple(isoString) { if (!isoString) return t('common.never') || 'Never'; var date = new Date(isoString); var now = new Date(); var diffMs = now - date; var diffMins = Math.floor(diffMs / 60000); var diffHours = Math.floor(diffMins / 60); var diffDays = Math.floor(diffHours / 24); if (diffMins < 1) return t('common.justNow') || 'Just now'; if (diffMins < 60) return diffMins + 'm ' + (t('common.ago') || 'ago'); if (diffHours < 24) return diffHours + 'h ' + (t('common.ago') || 'ago'); if (diffDays < 30) return diffDays + 'd ' + (t('common.ago') || 'ago'); return date.toLocaleDateString(); } /** * Check and display index health for current workspace */ async function checkIndexHealth() { var healthBadge = document.getElementById('indexHealthBadge'); var healthDetails = document.getElementById('indexHealthDetails'); var lastUpdateEl = document.getElementById('indexLastUpdate'); var commitsSinceEl = document.getElementById('indexCommitsSince'); if (!healthBadge) return; try { // Get current workspace index info var indexResponse = await fetch('/api/codexlens/indexes'); var indexData = await indexResponse.json(); var indexes = indexData.indexes || []; // Find current workspace index (newest one or matching current path) var currentIndex = indexes.length > 0 ? indexes[0] : null; if (!currentIndex) { healthBadge.className = 'text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground'; healthBadge.textContent = 'No Index'; if (healthDetails) healthDetails.classList.add('hidden'); return; } var lastIndexTime = currentIndex.lastModified ? new Date(currentIndex.lastModified) : null; // Get git commits since last index var commitsSince = 0; try { var gitResponse = await fetch('/api/git/commits-since?since=' + encodeURIComponent(currentIndex.lastModified || '')); var gitData = await gitResponse.json(); commitsSince = gitData.count || 0; } catch (gitErr) { console.warn('[CodexLens] Could not get git history:', gitErr); // Fallback: estimate based on time if (lastIndexTime) { var hoursSince = (Date.now() - lastIndexTime.getTime()) / (1000 * 60 * 60); commitsSince = Math.floor(hoursSince / 2); // Rough estimate } } // Determine health status var healthStatus = 'good'; var healthText = 'Up to date'; var healthClass = 'bg-success/20 text-success'; if (commitsSince > 50 || (lastIndexTime && (Date.now() - lastIndexTime.getTime()) > 7 * 24 * 60 * 60 * 1000)) { // More than 50 commits or 7 days old healthStatus = 'outdated'; healthText = 'Outdated'; healthClass = 'bg-destructive/20 text-destructive'; } else if (commitsSince > 10 || (lastIndexTime && (Date.now() - lastIndexTime.getTime()) > 24 * 60 * 60 * 1000)) { // More than 10 commits or 1 day old healthStatus = 'stale'; healthText = 'Stale'; healthClass = 'bg-warning/20 text-warning'; } // Update badge healthBadge.className = 'text-xs px-2 py-0.5 rounded-full ' + healthClass; healthBadge.textContent = healthText; // Update details section if (healthDetails && healthStatus !== 'good') { healthDetails.classList.remove('hidden'); if (lastUpdateEl) lastUpdateEl.textContent = lastIndexTime ? formatTimeAgoSimple(currentIndex.lastModified) : 'Unknown'; if (commitsSinceEl) { commitsSinceEl.textContent = commitsSince; commitsSinceEl.className = 'font-medium ' + (commitsSince > 20 ? 'text-destructive' : commitsSince > 5 ? 'text-warning' : 'text-foreground'); } } else if (healthDetails) { healthDetails.classList.add('hidden'); } } catch (err) { console.error('[CodexLens] Failed to check index health:', err); healthBadge.className = 'text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground'; healthBadge.textContent = 'Unknown'; } } // Make function globally accessible window.checkIndexHealth = checkIndexHealth; /** * Clean a specific project's index from the page */ async function cleanIndexProjectFromPage(projectId) { if (!confirm((t('index.cleanProjectConfirm') || 'Clean index for') + ' ' + projectId + '?')) { return; } try { showRefreshToast(t('index.cleaning') || 'Cleaning index...', 'info'); var response = await fetch('/api/codexlens/clean', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId: projectId }) }); var result = await response.json(); if (result.success) { showRefreshToast(t('index.cleanSuccess') || 'Index cleaned successfully', 'success'); await loadIndexStatsForPage(); } else { showRefreshToast((t('index.cleanFailed') || 'Clean failed') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error'); } } /** * Clean all indexes from the page */ async function cleanAllIndexesFromPage() { if (!confirm(t('index.cleanAllConfirm') || 'Are you sure you want to clean ALL indexes? This cannot be undone.')) { return; } try { showRefreshToast(t('index.cleaning') || 'Cleaning indexes...', 'info'); var response = await fetch('/api/codexlens/clean', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ all: true }) }); var result = await response.json(); if (result.success) { showRefreshToast(t('index.cleanAllSuccess') || 'All indexes cleaned', 'success'); await loadIndexStatsForPage(); } else { showRefreshToast((t('index.cleanFailed') || 'Clean failed') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error'); } } // ============================================================ // MULTI-PROVIDER ROTATION CONFIGURATION // ============================================================ /** * Load and display rotation status in the page */ async function loadRotationStatus() { try { // Load from unified embedding-pool API (handles both new and legacy config) var response = await fetch('/api/litellm-api/embedding-pool'); if (!response.ok) { console.warn('[CodexLens] Failed to load embedding pool config:', response.status); return; } var data = await response.json(); window.embeddingPoolConfig = data.poolConfig; window.embeddingPoolAvailableModels = data.availableModels || []; // Also get endpoint count var endpointsResponse = await fetch('/api/litellm-api/codexlens/rotation/endpoints'); var endpointsData = endpointsResponse.ok ? await endpointsResponse.json() : { count: 0 }; updateRotationStatusDisplay(data.poolConfig, endpointsData.count); } catch (err) { console.error('[CodexLens] Error loading rotation status:', err); } } /** * Update the rotation status display in the page * @param {Object} poolConfig - The embedding pool configuration * @param {number} endpointCount - Number of active endpoints */ function updateRotationStatusDisplay(poolConfig, endpointCount) { var badge = document.getElementById('rotationStatusBadge'); var detailsEl = document.getElementById('rotationDetails'); var modelNameEl = document.getElementById('rotationModelName'); var countEl = document.getElementById('rotationEndpointCount'); if (!badge) return; if (poolConfig && poolConfig.enabled) { badge.textContent = t('common.enabled'); badge.className = 'text-xs px-2 py-0.5 rounded-full bg-success/10 text-success'; // Show details if (detailsEl) { detailsEl.classList.remove('hidden'); if (modelNameEl) modelNameEl.textContent = poolConfig.targetModel || ''; if (countEl) countEl.textContent = (endpointCount || 0) + ' ' + t('codexlens.totalEndpoints').toLowerCase(); } } else { badge.textContent = t('common.disabled'); badge.className = 'text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground'; if (detailsEl) detailsEl.classList.add('hidden'); } } /** * Navigate to API Settings Embedding Pool tab */ function navigateToApiSettingsEmbeddingPool() { // Navigate to API Settings page with embedding-pool tab if (typeof switchView === 'function') { switchView('api-settings'); // Give time for page to render, then switch to embedding-pool tab setTimeout(function() { if (typeof switchSidebarTab === 'function') { switchSidebarTab('embedding-pool'); } }, 100); } } /** * Show the rotation configuration modal */ async function showRotationConfigModal() { try { // Load current config if not already loaded if (!window.rotationConfig) { await loadRotationStatus(); } var rotationConfig = window.rotationConfig || { enabled: false, strategy: 'round_robin', defaultCooldown: 60, targetModel: 'qwen3-embedding', providers: [] }; var availableProviders = window.availableRotationProviders || []; var modalHtml = buildRotationConfigModal(rotationConfig, availableProviders); var tempContainer = document.createElement('div'); tempContainer.innerHTML = modalHtml; var modal = tempContainer.firstElementChild; document.body.appendChild(modal); if (window.lucide) lucide.createIcons(); initRotationConfigEvents(rotationConfig, availableProviders); } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Build the rotation configuration modal HTML */ function buildRotationConfigModal(rotationConfig, availableProviders) { var isEnabled = rotationConfig.enabled || false; var strategy = rotationConfig.strategy || 'round_robin'; var cooldown = rotationConfig.defaultCooldown || 60; var targetModel = rotationConfig.targetModel || 'qwen3-embedding'; var configuredProviders = rotationConfig.providers || []; // Build provider list HTML var providerListHtml = ''; if (availableProviders.length === 0) { providerListHtml = '
' + t('codexlens.noRotationProviders') + '
'; } else { availableProviders.forEach(function(provider, index) { // Find if this provider is already configured var configured = configuredProviders.find(function(p) { return p.providerId === provider.providerId; }); var isProviderEnabled = configured ? configured.enabled : false; var weight = configured ? configured.weight : 1; var maxConcurrent = configured ? configured.maxConcurrentPerKey : 4; var useAllKeys = configured ? configured.useAllKeys : true; // Get model options var modelOptions = provider.embeddingModels.map(function(m) { var selected = configured && configured.modelId === m.modelId ? 'selected' : ''; return ''; }).join(''); // Get key count var keyCount = provider.apiKeys.filter(function(k) { return k.enabled; }).length; providerListHtml += '
' + '
' + '
' + '' + '' + '' + keyCount + ' keys' + '
' + '
' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '
'; }); } return ''; } /** * Initialize rotation config modal events */ function initRotationConfigEvents(rotationConfig, availableProviders) { // Store in window for save function window._rotationAvailableProviders = availableProviders; } /** * Close the rotation config modal */ function closeRotationModal() { var modal = document.getElementById('rotationConfigModal'); if (modal) modal.remove(); } /** * Save the rotation configuration */ async function saveRotationConfig() { try { var enabledToggle = document.getElementById('rotationEnabledToggle'); var strategySelect = document.getElementById('rotationStrategy'); var cooldownInput = document.getElementById('rotationCooldown'); var targetModelInput = document.getElementById('rotationTargetModel'); var enabled = enabledToggle ? enabledToggle.checked : false; var strategy = strategySelect ? strategySelect.value : 'round_robin'; var cooldown = cooldownInput ? parseInt(cooldownInput.value, 10) : 60; var targetModel = targetModelInput ? targetModelInput.value.trim() : 'qwen3-embedding'; // Collect provider configurations var providers = []; var providerToggles = document.querySelectorAll('.rotation-provider-toggle'); providerToggles.forEach(function(toggle) { var providerId = toggle.getAttribute('data-provider-id'); var isEnabled = toggle.checked; var modelSelect = document.querySelector('.rotation-model-select[data-provider-id="' + providerId + '"]'); var weightInput = document.querySelector('.rotation-weight-input[data-provider-id="' + providerId + '"]'); var concurrentInput = document.querySelector('.rotation-concurrent-input[data-provider-id="' + providerId + '"]'); var useAllKeysToggle = document.querySelector('.rotation-use-all-keys[data-provider-id="' + providerId + '"]'); providers.push({ providerId: providerId, modelId: modelSelect ? modelSelect.value : '', weight: weightInput ? parseFloat(weightInput.value) || 1 : 1, maxConcurrentPerKey: concurrentInput ? parseInt(concurrentInput.value, 10) || 4 : 4, useAllKeys: useAllKeysToggle ? useAllKeysToggle.checked : true, enabled: isEnabled }); }); var rotationConfig = { enabled: enabled, strategy: strategy, defaultCooldown: cooldown, targetModel: targetModel, providers: providers }; var response = await fetch('/api/litellm-api/codexlens/rotation', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(rotationConfig) }); var result = await response.json(); if (result.success) { // Show sync result in toast var syncMsg = ''; if (result.syncResult) { if (result.syncResult.success) { syncMsg = ' (' + result.syncResult.endpointCount + ' ' + t('codexlens.endpointsSynced') + ')'; } else { syncMsg = ' (' + t('codexlens.syncFailed') + ': ' + result.syncResult.message + ')'; } } showRefreshToast(t('codexlens.rotationSaved') + syncMsg, 'success'); window.rotationConfig = rotationConfig; updateRotationStatusDisplay(rotationConfig); closeRotationModal(); } else { showRefreshToast(t('common.saveFailed') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } // ============================================================ // RERANKER CONFIGURATION MODAL // ============================================================ /** * Show Reranker configuration modal */ async function showRerankerConfigModal() { try { showRefreshToast(t('codexlens.loadingRerankerConfig') || 'Loading reranker configuration...', 'info'); // Fetch current reranker config const response = await fetch('/api/codexlens/reranker/config'); const config = await response.json(); if (!config.success) { showRefreshToast(t('common.error') + ': ' + (config.error || 'Failed to load config'), 'error'); return; } const modalHtml = buildRerankerConfigContent(config); // Create and show modal const tempContainer = document.createElement('div'); tempContainer.innerHTML = modalHtml; const modal = tempContainer.firstElementChild; document.body.appendChild(modal); // Initialize icons if (window.lucide) lucide.createIcons(); // Initialize event handlers initRerankerConfigEvents(config); } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Build Reranker configuration modal content */ function buildRerankerConfigContent(config) { const backend = config.backend || 'onnx'; const modelName = config.model_name || ''; const apiProvider = config.api_provider || 'siliconflow'; const apiKeySet = config.api_key_set || false; const availableBackends = config.available_backends || ['onnx', 'api', 'litellm', 'legacy']; const apiProviders = config.api_providers || ['siliconflow', 'cohere', 'jina']; const litellmEndpoints = config.litellm_endpoints || []; // ONNX models const onnxModels = [ 'cross-encoder/ms-marco-MiniLM-L-6-v2', 'cross-encoder/ms-marco-TinyBERT-L-2-v2', 'BAAI/bge-reranker-base', 'BAAI/bge-reranker-large' ]; // Build backend options const backendOptions = availableBackends.map(function(b) { const labels = { 'onnx': 'ONNX (Local, Optimum)', 'api': 'API (SiliconFlow/Cohere/Jina)', 'litellm': 'LiteLLM (Custom Endpoint)', 'legacy': 'Legacy (SentenceTransformers)' }; return ''; }).join(''); // Build API provider options const providerOptions = apiProviders.map(function(p) { return ''; }).join(''); // Build ONNX model options const onnxModelOptions = onnxModels.map(function(m) { return ''; }).join(''); // Build LiteLLM endpoint options const litellmOptions = litellmEndpoints.length > 0 ? litellmEndpoints.map(function(ep) { return ''; }).join('') : ''; return ''; } /** * Toggle reranker configuration sections based on selected backend */ function toggleRerankerSections() { var backend = document.getElementById('rerankerBackend').value; document.getElementById('rerankerOnnxSection').style.display = backend === 'onnx' ? 'block' : 'none'; document.getElementById('rerankerApiSection').style.display = backend === 'api' ? 'block' : 'none'; document.getElementById('rerankerLitellmSection').style.display = backend === 'litellm' ? 'block' : 'none'; document.getElementById('rerankerLegacySection').style.display = backend === 'legacy' ? 'block' : 'none'; } /** * Initialize reranker config modal events */ function initRerankerConfigEvents(config) { // Handle ONNX model custom input toggle var onnxModelSelect = document.getElementById('rerankerOnnxModel'); var customModelInput = document.getElementById('rerankerCustomModel'); if (onnxModelSelect && customModelInput) { onnxModelSelect.addEventListener('change', function() { customModelInput.style.display = this.value === 'custom' ? 'block' : 'none'; }); } // Store original config for reset window._rerankerOriginalConfig = config; } /** * Close the reranker config modal */ function closeRerankerModal() { var modal = document.getElementById('rerankerConfigModal'); if (modal) modal.remove(); } /** * Reset reranker config to original values */ function resetRerankerConfig() { var config = window._rerankerOriginalConfig; if (!config) return; document.getElementById('rerankerBackend').value = config.backend || 'onnx'; toggleRerankerSections(); // Reset ONNX section var onnxModels = [ 'cross-encoder/ms-marco-MiniLM-L-6-v2', 'cross-encoder/ms-marco-TinyBERT-L-2-v2', 'BAAI/bge-reranker-base', 'BAAI/bge-reranker-large' ]; if (onnxModels.includes(config.model_name)) { document.getElementById('rerankerOnnxModel').value = config.model_name; document.getElementById('rerankerCustomModel').style.display = 'none'; } else { document.getElementById('rerankerOnnxModel').value = 'custom'; document.getElementById('rerankerCustomModel').value = config.model_name || ''; document.getElementById('rerankerCustomModel').style.display = 'block'; } // Reset API section document.getElementById('rerankerApiProvider').value = config.api_provider || 'siliconflow'; document.getElementById('rerankerApiKey').value = ''; document.getElementById('rerankerApiModel').value = config.model_name || ''; showRefreshToast(t('common.reset') || 'Reset to original values', 'info'); } /** * Save reranker configuration */ async function saveRerankerConfig() { try { var backend = document.getElementById('rerankerBackend').value; var payload = { backend: backend }; // Collect model name based on backend if (backend === 'onnx') { var onnxModel = document.getElementById('rerankerOnnxModel').value; if (onnxModel === 'custom') { payload.model_name = document.getElementById('rerankerCustomModel').value.trim(); } else { payload.model_name = onnxModel; } } else if (backend === 'api') { payload.api_provider = document.getElementById('rerankerApiProvider').value; payload.model_name = document.getElementById('rerankerApiModel').value.trim(); var apiKey = document.getElementById('rerankerApiKey').value.trim(); if (apiKey) { payload.api_key = apiKey; } } else if (backend === 'litellm') { payload.litellm_endpoint = document.getElementById('rerankerLitellmEndpoint').value; } var response = await fetch('/api/codexlens/reranker/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); var result = await response.json(); if (result.success) { showRefreshToast((t('codexlens.rerankerConfigSaved') || 'Reranker configuration saved') + ': ' + result.message, 'success'); closeRerankerModal(); } else { showRefreshToast(t('common.saveFailed') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } // ============================================================ // FILE WATCHER CONTROL // ============================================================ /** * Show File Watcher control modal */ async function showWatcherControlModal() { try { showRefreshToast(t('codexlens.loadingWatcherStatus') || 'Loading watcher status...', 'info'); // Fetch current watcher status and indexed projects in parallel const [statusResponse, indexesResponse] = await Promise.all([ fetch('/api/codexlens/watch/status'), fetch('/api/codexlens/indexes') ]); const status = await statusResponse.json(); const indexes = await indexesResponse.json(); // Get first indexed project path as default let defaultPath = ''; if (indexes.success && indexes.projects && indexes.projects.length > 0) { // Sort by last_indexed desc and pick the most recent const sorted = indexes.projects.sort((a, b) => new Date(b.last_indexed || 0) - new Date(a.last_indexed || 0) ); defaultPath = sorted[0].source_root || ''; } const modalHtml = buildWatcherControlContent(status, defaultPath); // Create and show modal const tempContainer = document.createElement('div'); tempContainer.innerHTML = modalHtml; const modal = tempContainer.firstElementChild; document.body.appendChild(modal); // Initialize icons if (window.lucide) lucide.createIcons(); // Start polling if watcher is running if (status.running) { startWatcherStatusPolling(); } } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Build File Watcher control modal content * @param {Object} status - Watcher status * @param {string} defaultPath - Default path from indexed projects */ function buildWatcherControlContent(status, defaultPath) { const running = status.running || false; defaultPath = defaultPath || ''; const rootPath = status.root_path || ''; const eventsProcessed = status.events_processed || 0; const uptimeSeconds = status.uptime_seconds || 0; // Format uptime const formatUptime = function(seconds) { if (seconds < 60) return seconds + 's'; if (seconds < 3600) return Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's'; return Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm'; }; return ''; } /** * Toggle file watcher on/off */ async function toggleWatcher() { var toggle = document.getElementById('watcherToggle'); var shouldRun = toggle.checked; try { if (shouldRun) { // Start watcher var watchPath = document.getElementById('watcherPath').value.trim(); var debounceMs = parseInt(document.getElementById('watcherDebounce').value, 10) || 1000; var response = await fetch('/api/codexlens/watch/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: watchPath || undefined, debounce_ms: debounceMs }) }); var result = await response.json(); if (result.success) { showRefreshToast((t('codexlens.watcherStarted') || 'Watcher started') + ': ' + result.path, 'success'); document.getElementById('watcherStats').style.display = 'block'; document.getElementById('watcherStartConfig').style.display = 'none'; startWatcherStatusPolling(); } else { toggle.checked = false; showRefreshToast(t('common.error') + ': ' + result.error, 'error'); } } else { // Stop watcher var response = await fetch('/api/codexlens/watch/stop', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); var result = await response.json(); if (result.success) { showRefreshToast((t('codexlens.watcherStopped') || 'Watcher stopped') + ': ' + result.events_processed + ' events processed', 'success'); document.getElementById('watcherStats').style.display = 'none'; document.getElementById('watcherStartConfig').style.display = 'block'; stopWatcherStatusPolling(); } else { toggle.checked = true; showRefreshToast(t('common.error') + ': ' + result.error, 'error'); } } } catch (err) { toggle.checked = !shouldRun; showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } // Watcher status polling var watcherPollingInterval = null; function startWatcherStatusPolling() { if (watcherPollingInterval) return; watcherPollingInterval = setInterval(async function() { try { var response = await fetch('/api/codexlens/watch/status'); var status = await response.json(); if (status.running) { document.getElementById('watcherEventsCount').textContent = status.events_processed || 0; // Format uptime var seconds = status.uptime_seconds || 0; var formatted = seconds < 60 ? seconds + 's' : seconds < 3600 ? Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's' : Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm'; document.getElementById('watcherUptime').textContent = formatted; } else { // Watcher stopped externally stopWatcherStatusPolling(); document.getElementById('watcherToggle').checked = false; document.getElementById('watcherStats').style.display = 'none'; document.getElementById('watcherStartConfig').style.display = 'block'; } } catch (err) { console.error('Failed to poll watcher status:', err); } }, 2000); } function stopWatcherStatusPolling() { if (watcherPollingInterval) { clearInterval(watcherPollingInterval); watcherPollingInterval = null; } } /** * Close the watcher control modal */ function closeWatcherModal() { stopWatcherStatusPolling(); var modal = document.getElementById('watcherControlModal'); if (modal) modal.remove(); } /** * Handle watcher status update from WebSocket * @param {Object} payload - { running: boolean, path?: string, error?: string } */ function handleWatcherStatusUpdate(payload) { var toggle = document.getElementById('watcherToggle'); var statsDiv = document.getElementById('watcherStats'); var configDiv = document.getElementById('watcherStartConfig'); if (payload.error) { // Watcher failed - update UI to show stopped state if (toggle) toggle.checked = false; if (statsDiv) statsDiv.style.display = 'none'; if (configDiv) configDiv.style.display = 'block'; stopWatcherStatusPolling(); } else if (payload.running) { // Watcher started if (toggle) toggle.checked = true; if (statsDiv) statsDiv.style.display = 'block'; if (configDiv) configDiv.style.display = 'none'; startWatcherStatusPolling(); } else { // Watcher stopped normally if (toggle) toggle.checked = false; if (statsDiv) statsDiv.style.display = 'none'; if (configDiv) configDiv.style.display = 'block'; stopWatcherStatusPolling(); } }