// CodexLens Manager - Configuration, Model Management, and Semantic Dependencies
// Extracted from cli-manager.js for better maintainability
// ============================================================
// 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
*/
function buildCodexLensConfigContent(config) {
const indexDir = config.index_dir || '~/.codexlens/indexes';
const indexCount = config.index_count || 0;
const isInstalled = window.cliToolsStatus?.codexlens?.installed || false;
return '
' +
'
' +
'' +
'
' +
// Status Section
'
' +
// Index Storage Path Section
'
' +
// Actions Section
'
' +
// Semantic Dependencies Section
(isInstalled
? '
'
: '') +
// Model Management Section
(isInstalled
? '
'
: '') +
// Test Search Section
(isInstalled
? '
'
: '') +
'
' +
// Footer
'' +
'
';
}
/**
* Initialize CodexLens config modal event handlers
*/
function initCodexLensConfigEvents(currentConfig) {
// Save button
var saveBtn = document.getElementById('saveCodexLensConfigBtn');
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) {
closeModal();
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');
closeModal();
// Refresh CodexLens status
if (typeof loadCodexLensStatus === 'function') {
await loadCodexLensStatus();
renderToolsSection();
if (window.lucide) lucide.createIcons();
}
} else {
showRefreshToast(t('common.saveFailed') + ': ' + result.error, 'error');
saveBtn.disabled = false;
saveBtn.innerHTML = '
' + t('codexlens.saveConfig');
if (window.lucide) lucide.createIcons();
}
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
saveBtn.disabled = false;
saveBtn.innerHTML = '
' + t('codexlens.saveConfig');
if (window.lucide) lucide.createIcons();
}
};
}
// Test Search Button
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' });
// Add mode parameter for search and search_files (not for symbol search)
if (searchType === 'search' || searchType === 'search_files') {
params.append('mode', searchMode);
}
var response = await fetch(endpoint + '?' + params.toString());
var result = await response.json();
console.log('[CodexLens Test] Search result:', result);
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');
showRefreshToast(t('codexlens.searchCompleted') + ': ' + results.length + ' ' + t('codexlens.resultsCount'), 'success');
} else {
resultContent.textContent = t('common.error') + ': ' + (result.error || t('common.unknownError'));
resultsDiv.classList.remove('hidden');
showRefreshToast(t('codexlens.searchFailed') + ': ' + result.error, 'error');
}
runSearchBtn.disabled = false;
runSearchBtn.innerHTML = '
' + t('codexlens.runSearch');
if (window.lucide) lucide.createIcons();
} catch (err) {
console.error('[CodexLens Test] Error:', err);
resultContent.textContent = t('common.exception') + ': ' + err.message;
resultsDiv.classList.remove('hidden');
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
runSearchBtn.disabled = false;
runSearchBtn.innerHTML = '
' + t('codexlens.runSearch');
if (window.lucide) lucide.createIcons();
}
};
}
// Load semantic dependencies status
loadSemanticDepsStatus();
// Load model list
loadModelList();
}
// ============================================================
// SEMANTIC DEPENDENCIES MANAGEMENT
// ============================================================
// Store detected GPU info
var detectedGpuInfo = null;
// Store available GPU devices
var availableGpuDevices = null;
/**
* Detect GPU support
*/
async function detectGpuSupport() {
try {
var response = await fetch('/api/codexlens/gpu/detect');
var result = await response.json();
if (result.success) {
detectedGpuInfo = result;
return result;
}
} catch (err) {
console.error('GPU detection failed:', err);
}
return { mode: 'cpu', available: ['cpu'], info: 'CPU only' };
}
/**
* Load semantic dependencies status
*/
async function loadSemanticDepsStatus() {
var container = document.getElementById('semanticDepsStatus');
if (!container) return;
try {
// Detect GPU support and load GPU devices in parallel
var gpuPromise = detectGpuSupport();
var gpuDevicesPromise = loadGpuDevices();
var response = await fetch('/api/codexlens/semantic/status');
var result = await response.json();
var gpuInfo = await gpuPromise;
var gpuDevices = await gpuDevicesPromise;
if (result.available) {
// Build accelerator badge
var accelerator = result.accelerator || 'CPU';
var acceleratorIcon = 'cpu';
var acceleratorClass = 'bg-muted text-muted-foreground';
if (accelerator === 'CUDA') {
acceleratorIcon = 'zap';
acceleratorClass = 'bg-green-500/20 text-green-600';
} else if (accelerator === 'DirectML') {
acceleratorIcon = 'gpu-card';
acceleratorClass = 'bg-blue-500/20 text-blue-600';
} else if (accelerator === 'ROCm') {
acceleratorIcon = 'flame';
acceleratorClass = 'bg-red-500/20 text-red-600';
}
// Build GPU device selector if multiple GPUs available
var gpuDeviceSelector = buildGpuDeviceSelector(gpuDevices);
container.innerHTML =
'
' +
'
' +
' ' +
'' + t('codexlens.semanticInstalled') + ' ' +
'(' + (result.backend || 'fastembed') + ') ' +
'
' +
'
' +
'' +
' ' +
accelerator +
' ' +
(result.providers && result.providers.length > 0
? '' + result.providers.join(', ') + ' '
: '') +
'
' +
gpuDeviceSelector +
'
';
} else {
// Build GPU mode options
var gpuOptions = buildGpuModeSelector(gpuInfo);
container.innerHTML =
'
' +
'
' +
' ' +
'' + t('codexlens.semanticNotInstalled') + ' ' +
'
' +
gpuOptions +
'
' +
' ' + t('codexlens.installDeps') +
' ' +
'
';
}
if (window.lucide) lucide.createIcons();
} catch (err) {
container.innerHTML =
'
' + t('common.error') + ': ' + err.message + '
';
}
}
/**
* Build GPU mode selector HTML
*/
function buildGpuModeSelector(gpuInfo) {
var modes = [
{
id: 'cpu',
label: 'CPU',
desc: t('codexlens.cpuModeDesc') || 'Standard CPU processing',
icon: 'cpu',
available: true
},
{
id: 'directml',
label: 'DirectML',
desc: t('codexlens.directmlModeDesc') || 'Windows GPU (NVIDIA/AMD/Intel)',
icon: 'gpu-card',
available: gpuInfo.available.includes('directml'),
recommended: gpuInfo.mode === 'directml'
},
{
id: 'cuda',
label: 'CUDA',
desc: t('codexlens.cudaModeDesc') || 'NVIDIA GPU (requires CUDA Toolkit)',
icon: 'zap',
available: gpuInfo.available.includes('cuda'),
recommended: gpuInfo.mode === 'cuda'
}
];
var html =
'
' +
'
' +
' ' +
(t('codexlens.selectGpuMode') || 'Select acceleration mode') +
'
' +
'
' +
' ' + gpuInfo.info +
'
' +
'
';
modes.forEach(function(mode) {
var isDisabled = !mode.available;
var isRecommended = mode.recommended;
var isDefault = mode.id === gpuInfo.mode;
html +=
'
' +
' ' +
'' +
'
' +
' ' +
'' + mode.label + ' ' +
(isRecommended ? '' + (t('common.recommended') || 'Recommended') + ' ' : '') +
(isDisabled ? '(' + (t('common.unavailable') || 'Unavailable') + ') ' : '') +
'
' +
'
' + mode.desc + '
' +
'
' +
' ';
});
html +=
'
' +
'
';
return html;
}
/**
* Get selected GPU mode
*/
function getSelectedGpuMode() {
var selected = document.querySelector('input[name="gpuMode"]:checked');
return selected ? selected.value : 'cpu';
}
/**
* Load available GPU devices
*/
async function loadGpuDevices() {
try {
var response = await fetch('/api/codexlens/gpu/list');
var result = await response.json();
if (result.success && result.result) {
availableGpuDevices = result.result;
return result.result;
}
} catch (err) {
console.error('GPU devices load failed:', err);
}
return { devices: [], selected_device_id: null };
}
/**
* Build GPU device selector HTML
*/
function buildGpuDeviceSelector(gpuDevices) {
if (!gpuDevices || !gpuDevices.devices || gpuDevices.devices.length === 0) {
return '';
}
// Only show selector if there are multiple GPUs
if (gpuDevices.devices.length < 2) {
return '';
}
var html =
'
' +
'
' +
' ' +
(t('codexlens.selectGpuDevice') || 'Select GPU Device') +
'
' +
'
';
gpuDevices.devices.forEach(function(device) {
var isSelected = device.is_selected;
var vendorIcon = device.vendor === 'nvidia' ? 'zap' : (device.vendor === 'amd' ? 'flame' : 'cpu');
var vendorColor = device.vendor === 'nvidia' ? 'text-green-500' : (device.vendor === 'amd' ? 'text-red-500' : 'text-blue-500');
var typeLabel = device.is_discrete ? (t('codexlens.discrete') || 'Discrete') : (t('codexlens.integrated') || 'Integrated');
html +=
'
' +
' ' +
'' +
'
' +
' ' +
'' + device.name + ' ' +
'
' +
'
' +
'' + device.vendor.toUpperCase() + ' ' +
'' +
typeLabel +
' ' +
(device.is_preferred ? '' + (t('common.auto') || 'Auto') + ' ' : '') +
'
' +
'
' +
' ';
});
html +=
'
' +
'
' +
' ' + (t('codexlens.resetToAuto') || 'Reset to Auto') +
' ' +
'
';
return html;
}
/**
* Select a GPU device
*/
async function selectGpuDevice(deviceId) {
try {
showRefreshToast(t('codexlens.selectingGpu') || 'Selecting GPU...', 'info');
var response = await fetch('/api/codexlens/gpu/select', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device_id: deviceId })
});
var result = await response.json();
if (result.success) {
showRefreshToast(t('codexlens.gpuSelected') || 'GPU selected', 'success');
// Reload semantic status to reflect change
loadSemanticDepsStatus();
} else {
showRefreshToast(result.error || 'Failed to select GPU', 'error');
}
} catch (err) {
showRefreshToast(err.message, 'error');
}
}
/**
* Reset GPU device selection to auto
*/
async function resetGpuDevice() {
try {
showRefreshToast(t('codexlens.resettingGpu') || 'Resetting GPU selection...', 'info');
var response = await fetch('/api/codexlens/gpu/reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
var result = await response.json();
if (result.success) {
showRefreshToast(t('codexlens.gpuReset') || 'GPU selection reset to auto', 'success');
// Reload semantic status to reflect change
loadSemanticDepsStatus();
} else {
showRefreshToast(result.error || 'Failed to reset GPU', 'error');
}
} catch (err) {
showRefreshToast(err.message, 'error');
}
}
/**
* Install semantic dependencies with GPU mode
*/
async function installSemanticDepsWithGpu() {
var container = document.getElementById('semanticDepsStatus');
if (!container) return;
var gpuMode = getSelectedGpuMode();
var modeLabels = {
cpu: 'CPU',
cuda: 'NVIDIA CUDA',
directml: 'DirectML'
};
container.innerHTML =
'
' +
'
' +
'
' +
'
' + t('codexlens.installingDeps') + ' ' +
'
' +
'
' +
(t('codexlens.installingMode') || 'Installing with') + ': ' + modeLabels[gpuMode] +
'
' +
'
';
try {
var response = await fetch('/api/codexlens/semantic/install', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ gpuMode: gpuMode })
});
var result = await response.json();
if (result.success) {
showRefreshToast(t('codexlens.depsInstalled') + ' (' + modeLabels[gpuMode] + ')', 'success');
await loadSemanticDepsStatus();
await loadModelList();
} else {
showRefreshToast(t('codexlens.depsInstallFailed') + ': ' + result.error, 'error');
await loadSemanticDepsStatus();
}
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
await loadSemanticDepsStatus();
}
}
/**
* Install semantic dependencies (legacy, defaults to CPU)
*/
async function installSemanticDeps() {
await installSemanticDepsWithGpu();
}
// ============================================================
// MODEL MANAGEMENT
// ============================================================
/**
* Build manual download guide HTML
*/
function buildManualDownloadGuide() {
var modelData = [
{ profile: 'code', name: 'jinaai/jina-embeddings-v2-base-code', size: '~150 MB' },
{ profile: 'fast', name: 'BAAI/bge-small-en-v1.5', size: '~80 MB' }
];
var html =
'
' +
'
' +
' ' +
' ' +
'' + (t('codexlens.manualDownloadGuide') || 'Manual Download Guide') + ' ' +
' ' +
'
' +
// Method 1: CLI
'
' +
'
' +
'1 ' +
'' + (t('codexlens.cliMethod') || 'Command Line (Recommended)') + ' ' +
'
' +
'
' +
(t('codexlens.cliMethodDesc') || 'Run in terminal with progress display:') +
'
' +
'
';
modelData.forEach(function(m) {
html +=
'
' +
'codexlens model-download ' + m.profile + '' +
'' +
' ' +
' ' +
'
';
});
html +=
'
' +
'
' +
// Method 2: Python
'
' +
'
' +
'2 ' +
'' + (t('codexlens.pythonMethod') || 'Python Script') + ' ' +
'
' +
'
' +
(t('codexlens.pythonMethodDesc') || 'Pre-download model using Python:') +
'
' +
'
' +
'
' +
'# Install fastembed first\n' +
'pip install fastembed\n\n' +
'# Download model (choose one)\n' +
'from fastembed import TextEmbedding\n\n' +
'# Code model (recommended for code search)\n' +
'model = TextEmbedding("jinaai/jina-embeddings-v2-base-code")\n\n' +
'# Fast model (lightweight)\n' +
'# model = TextEmbedding("BAAI/bge-small-en-v1.5")' +
' ' +
'
' +
'
' +
// Method 3: Hugging Face Hub
'
' +
'
' +
'3 ' +
'' + (t('codexlens.hfHubMethod') || 'Hugging Face Hub CLI') + ' ' +
'
' +
'
' +
(t('codexlens.hfHubMethodDesc') || 'Download using huggingface-cli with resume support:') +
'
' +
'
' +
'
' +
'# Install huggingface_hub\n' +
'pip install huggingface_hub\n\n' +
'# Download model (supports resume on failure)\n' +
'huggingface-cli download jinaai/jina-embeddings-v2-base-code' +
' ' +
'
' +
'
' +
// Model Links
'
' +
'
' +
' ' +
'' + (t('codexlens.modelLinks') || 'Direct Model Links') + ' ' +
'
' +
'
' +
'
' +
// Cache location info
'
' +
'
' +
'
' +
'
' +
'' + (t('codexlens.cacheLocation') || 'Cache Location') + ': ' +
'Default: ~/.cache/huggingface ' +
'(Check HF_HOME env var if set)' +
'
' +
'
' +
'
' +
'
' +
'
';
return html;
}
/**
* Toggle manual download guide visibility
*/
function toggleManualDownloadGuide() {
var content = document.getElementById('manualDownloadContent');
var chevron = document.getElementById('manualDownloadChevron');
if (content && chevron) {
content.classList.toggle('hidden');
chevron.style.transform = content.classList.contains('hidden') ? '' : 'rotate(90deg)';
}
}
/**
* Copy text to clipboard
*/
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
showRefreshToast(t('common.copied') || 'Copied to clipboard', 'success');
}).catch(function(err) {
console.error('Failed to copy:', err);
});
}
/**
* Load model list
*/
async function loadModelList() {
var container = document.getElementById('modelListContainer');
if (!container) return;
try {
var response = await fetch('/api/codexlens/models');
var result = await response.json();
if (!result.success) {
// Check if the error is specifically about fastembed not being installed
var errorMsg = result.error || '';
if (errorMsg.includes('fastembed not installed') || errorMsg.includes('Semantic')) {
container.innerHTML =
'
' + t('codexlens.semanticNotInstalled') + '
';
} else {
// Show actual error message for other failures
container.innerHTML =
'
' + t('codexlens.modelListError') + ': ' + (errorMsg || t('common.unknownError')) + '
';
}
return;
}
if (!result.result || !result.result.models) {
container.innerHTML =
'
' + t('codexlens.noModelsAvailable') + '
';
return;
}
var models = result.result.models;
var html = '
';
models.forEach(function(model) {
var statusIcon = model.installed
? '
'
: '
';
var sizeText = model.installed
? model.actual_size_mb.toFixed(1) + ' MB'
: '~' + model.estimated_size_mb + ' MB';
var actionBtn = model.installed
? '
' +
' ' + t('codexlens.deleteModel') +
' '
: '
' +
' ' + t('codexlens.downloadModel') +
' ';
html +=
'
' +
'
' +
'
' +
'
' +
statusIcon +
'' + model.profile + ' ' +
'(' + model.dimensions + ' dims) ' +
'
' +
'
' + model.model_name + '
' +
'
' + model.use_case + '
' +
'
' +
'
' +
'
' + sizeText + '
' +
actionBtn +
'
' +
'
' +
'
';
});
html += '
';
// Add manual download guide section
html += buildManualDownloadGuide();
container.innerHTML = html;
if (window.lucide) lucide.createIcons();
} catch (err) {
container.innerHTML =
'
' + t('common.error') + ': ' + err.message + '
';
}
}
/**
* Download model with progress simulation and manual download info
*/
async function downloadModel(profile) {
var modelCard = document.getElementById('model-' + profile);
if (!modelCard) return;
var originalHTML = modelCard.innerHTML;
// Get model info for size estimation
var modelSizes = {
'fast': { size: 80, time: '1-2' },
'code': { size: 150, time: '2-5' }
};
var modelInfo = modelSizes[profile] || { size: 100, time: '2-5' };
// Show detailed download UI with progress simulation
modelCard.innerHTML =
'
' +
'
' +
'
' +
'
' + (t('codexlens.downloadingModel') || 'Downloading') + ' ' + profile + ' ' +
'
' +
'
' +
'
' +
'
' +
'' + (t('codexlens.connectingToHuggingFace') || 'Connecting to Hugging Face...') + ' ' +
'~' + modelInfo.size + ' MB ' +
'
' +
'
' +
'
' +
'
' +
' ' +
'' + (t('codexlens.downloadTimeEstimate') || 'Estimated time') + ': ' + modelInfo.time + ' ' + (t('common.minutes') || 'minutes') + ' ' +
'
' +
'
' +
' ' +
'' + (t('codexlens.manualDownloadHint') || 'Manual download') + ': codexlens model-download ' + profile + ' ' +
'
' +
'
' +
'
' +
(t('common.cancel') || 'Cancel') +
' ' +
'
';
if (window.lucide) lucide.createIcons();
// Start progress simulation
var progressBar = document.getElementById('model-progress-' + profile);
var statusText = document.getElementById('model-status-' + profile);
var simulatedProgress = 0;
var progressInterval = null;
var downloadAborted = false;
// Store abort controller for cancellation
window['modelDownloadAbort_' + profile] = function() {
downloadAborted = true;
if (progressInterval) clearInterval(progressInterval);
};
// Simulate progress based on model size
var progressStages = [
{ percent: 10, msg: t('codexlens.downloadingModelFiles') || 'Downloading model files...' },
{ percent: 30, msg: t('codexlens.downloadingWeights') || 'Downloading model weights...' },
{ percent: 60, msg: t('codexlens.downloadingTokenizer') || 'Downloading tokenizer...' },
{ percent: 80, msg: t('codexlens.verifyingModel') || 'Verifying model...' },
{ percent: 95, msg: t('codexlens.finalizingDownload') || 'Finalizing...' }
];
var stageIndex = 0;
var baseInterval = Math.max(2000, modelInfo.size * 30); // Slower for larger models
progressInterval = setInterval(function() {
if (downloadAborted) return;
if (stageIndex < progressStages.length) {
var stage = progressStages[stageIndex];
simulatedProgress = stage.percent;
if (progressBar) progressBar.style.width = simulatedProgress + '%';
if (statusText) statusText.textContent = stage.msg;
stageIndex++;
}
}, baseInterval);
try {
var response = await fetch('/api/codexlens/models/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profile: profile })
});
// Clear simulation
if (progressInterval) clearInterval(progressInterval);
if (downloadAborted) {
modelCard.innerHTML = originalHTML;
if (window.lucide) lucide.createIcons();
return;
}
var result = await response.json();
if (result.success) {
// Show completion
if (progressBar) progressBar.style.width = '100%';
if (statusText) statusText.textContent = t('codexlens.downloadComplete') || 'Download complete!';
showRefreshToast(t('codexlens.modelDownloaded') + ': ' + profile, 'success');
// Refresh model list after short delay
setTimeout(function() {
loadModelList();
}, 500);
} else {
showRefreshToast(t('codexlens.modelDownloadFailed') + ': ' + result.error, 'error');
showModelDownloadError(modelCard, profile, result.error, originalHTML);
}
} catch (err) {
if (progressInterval) clearInterval(progressInterval);
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
showModelDownloadError(modelCard, profile, err.message, originalHTML);
}
// Cleanup abort function
delete window['modelDownloadAbort_' + profile];
}
/**
* Show model download error with manual download instructions
*/
function showModelDownloadError(modelCard, profile, error, originalHTML) {
var modelNames = {
'fast': 'BAAI/bge-small-en-v1.5',
'code': 'jinaai/jina-embeddings-v2-base-code'
};
var modelName = modelNames[profile] || profile;
var hfUrl = 'https://huggingface.co/' + modelName;
modelCard.innerHTML =
'
' +
'
' +
'
' +
'
' +
'
' + (t('codexlens.downloadFailed') || 'Download failed') + '
' +
'
' + error + '
' +
'
' +
'
' +
'
' +
'
' + (t('codexlens.manualDownloadOptions') || 'Manual download options') + ':
' +
'
' +
'
' +
'1. ' +
'' + (t('codexlens.cliDownload') || 'CLI') + ': codexlens model-download ' + profile + ' ' +
'
' +
'
' +
'
2. ' +
'
' + (t('codexlens.huggingfaceDownload') || 'Hugging Face') + ': ' + modelName + ' ' +
'
' +
'
' +
'
' +
'
' +
'' +
' ' + (t('common.refresh') || 'Refresh') +
' ' +
'' +
' ' + (t('common.retry') || 'Retry') +
' ' +
'
' +
'
';
if (window.lucide) lucide.createIcons();
}
/**
* Cancel model download
*/
function cancelModelDownload(profile) {
if (window['modelDownloadAbort_' + profile]) {
window['modelDownloadAbort_' + profile]();
showRefreshToast(t('codexlens.downloadCanceled') || 'Download canceled', 'info');
loadModelList();
}
}
/**
* Delete model
*/
async function deleteModel(profile) {
if (!confirm(t('codexlens.deleteModelConfirm') + ' ' + profile + '?')) {
return;
}
var modelCard = document.getElementById('model-' + profile);
if (!modelCard) return;
var originalHTML = modelCard.innerHTML;
modelCard.innerHTML =
'
' +
'' + t('codexlens.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(t('codexlens.modelDeleted') + ': ' + profile, 'success');
await loadModelList();
} else {
showRefreshToast(t('codexlens.modelDeleteFailed') + ': ' + result.error, 'error');
modelCard.innerHTML = originalHTML;
if (window.lucide) lucide.createIcons();
}
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
modelCard.innerHTML = originalHTML;
if (window.lucide) lucide.createIcons();
}
}
// ============================================================
// 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)
*/
async function initCodexLensIndex(indexType, embeddingModel, embeddingBackend) {
indexType = indexType || 'vector';
embeddingModel = embeddingModel || 'code';
embeddingBackend = embeddingBackend || 'fastembed';
// For vector or full index, check if semantic dependencies are available
if (indexType === 'vector' || indexType === 'full') {
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') + '
' +
'
' +
'
' +
'
' +
'
' +
t('common.cancel') +
' ' +
'
' +
' ' +
' ' +
'
' +
'
';
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);
}
/**
* 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)
*/
async function startCodexLensIndexing(indexType, embeddingModel, embeddingBackend) {
indexType = indexType || 'vector';
embeddingModel = embeddingModel || 'code';
embeddingBackend = embeddingBackend || 'fastembed';
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);
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 })
});
var result = await response.json();
console.log('[CodexLens] Init result:', result);
// Check if completed successfully (WebSocket might have already reported)
if (result.success) {
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') + '
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' + t('codexlens.startingInstall') + ' ' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'' + t('common.cancel') + ' ' +
'' +
' ' +
t('codexlens.installNow') +
' ' +
'
' +
'
';
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') + ' ' +
' ' +
' ' +
'
' +
'
' +
'
' +
'
' +
'
' + t('codexlens.removing') + ' ' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'' + t('common.cancel') + ' ' +
'' +
' ' +
t('codexlens.uninstall') +
' ' +
'
' +
'
';
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 {
// Load CodexLens status first to populate window.cliToolsStatus.codexlens
if (typeof loadCodexLensStatus === 'function') {
await loadCodexLensStatus();
}
// Load LiteLLM API config for embedding backend options
try {
console.log('[CodexLens] Loading LiteLLM config...');
var litellmResponse = await fetch('/api/litellm-api/config');
console.log('[CodexLens] LiteLLM response status:', litellmResponse.status);
if (litellmResponse.ok) {
window.litellmApiConfig = await litellmResponse.json();
console.log('[CodexLens] LiteLLM config loaded:', window.litellmApiConfig);
console.log('[CodexLens] Providers:', window.litellmApiConfig?.providers?.length || 0);
} else {
console.warn('[CodexLens] LiteLLM config response not ok:', litellmResponse.status);
}
} catch (e) {
console.warn('[CodexLens] Could not load LiteLLM config:', e);
}
var response = await fetch('/api/codexlens/config');
var config = await response.json();
container.innerHTML = buildCodexLensManagerPage(config);
if (window.lucide) lucide.createIcons();
initCodexLensManagerPageEvents(config);
loadSemanticDepsStatus();
loadModelList();
// Load index stats for the Index Manager section
if (window.cliToolsStatus?.codexlens?.installed) {
loadIndexStatsForPage();
}
} catch (err) {
container.innerHTML = '
' + t('common.error') + ': ' + 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;
// Build model options for vector indexing
var modelOptions = buildModelSelectOptionsForPage();
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
'
' +
'
' + t('codexlens.createIndex') + '' +
'
' +
// Backend selector (fastembed local or litellm API)
'
' +
'
' + (t('codexlens.embeddingBackend') || 'Embedding Backend') + ' ' +
'
' +
'' + (t('codexlens.localFastembed') || 'Local (FastEmbed)') + ' ' +
'' + (t('codexlens.apiLitellm') || 'API (LiteLLM)') + ' ' +
' ' +
'
' + (t('codexlens.backendHint') || 'Select local model or remote API endpoint') + '
' +
'
' +
// Model selector
'
' +
'
' + t('codexlens.embeddingModel') + ' ' +
'
' +
modelOptions +
' ' +
'
' + t('codexlens.modelHint') + '
' +
'
' +
// Index buttons - two modes: full (FTS + Vector) or FTS only
'
' +
'' +
' ' +
'' + t('codexlens.fullIndex') + ' ' +
' ' +
'' +
' ' +
'' + t('codexlens.ftsIndex') + ' ' +
' ' +
'
' +
'
' + t('codexlens.indexTypeHint') + '
' +
'
' +
'
' +
// Storage Path Section
'
' +
'
' + t('codexlens.indexStoragePath') + '' +
'
' +
'
' +
'
' + t('codexlens.currentPath') + ' ' +
'
' + indexDir + '
' +
'
' +
'
' +
'
' + t('codexlens.newStoragePath') + ' ' +
'
' +
' ' +
' ' +
'
' +
'
' + t('codexlens.pathInfo') + '
' +
'
' +
'
' +
'
' +
// Maintenance Section
'
' +
'
' + t('codexlens.maintenance') + '' +
'
' +
' ' + t('codexlens.cleanCurrentWorkspace') + ' ' +
' ' + t('codexlens.cleanAllIndexes') + ' ' +
' ' + t('cli.uninstall') + ' ' +
'
' +
'
' +
'
' +
// Right Column
'
' +
// Semantic Dependencies
'
' +
'
' + t('codexlens.semanticDeps') + '' +
'
' +
'
' +
'
' + t('codexlens.checkingDeps') +
'
' +
'
' +
'
' +
// Model Management
'
' +
'
' + t('codexlens.modelManagement') + '' +
'
' +
'
' +
'
' + t('codexlens.loadingModels') +
'
' +
'
' +
'
' +
'
' +
'
' +
// Index Manager Section
'
' +
'
' +
'
' +
' ' +
'' + t('index.manager') + ' ' +
'- ' +
'
' +
'
' +
'' +
' ' +
' ' +
'
' +
'
' +
'
' +
'
' +
' ' +
'' + 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') + ' ' +
' ' +
'
' +
'
' +
'
' +
'' +
' ' +
t('index.cleanAll') +
' ' +
'
' +
'
' +
'
' +
// Test Search Section
'
' +
'
' + t('codexlens.testSearch') + '' +
'
' +
'
' +
'' +
'' + t('codexlens.textSearch') + ' ' +
'' + t('codexlens.fileSearch') + ' ' +
'' + t('codexlens.symbolSearch') + ' ' +
' ' +
'' +
'' + t('codexlens.exactMode') + ' ' +
'' + t('codexlens.fuzzyMode') + ' ' +
'' + t('codexlens.hybridMode') + ' ' +
'' + t('codexlens.vectorMode') + ' ' +
' ' +
'
' +
'
' +
' ' +
' ' + t('codexlens.runSearch') + ' ' +
'
' +
'
' +
'
' +
'' + t('codexlens.results') + ': ' +
' ' +
'
' +
'
' +
'
' +
'
' +
'
'
: // Not installed: Show install prompt
'
' +
'
' +
'
' +
' ' +
'
' +
'
' + t('codexlens.installCodexLens') + ' ' +
'
' + t('codexlens.installFirst') + '
' +
'
' +
' ' + t('codexlens.installCodexLens') +
' ' +
'
' +
'
'
) +
'
';
}
/**
* 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 '
code (default) ' +
'
fast ';
}
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 += '
' + label + ' ';
});
return options;
}
/**
* Handle embedding backend change
*/
function onEmbeddingBackendChange() {
var backendSelect = document.getElementById('pageBackendSelect');
var modelSelect = document.getElementById('pageModelSelect');
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;
} else {
// Load local fastembed models
modelSelect.innerHTML = buildModelSelectOptionsForPage();
}
}
/**
* 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 += '
' + label + ' (' + providerName + ') ';
console.log('[CodexLens] Added option:', label, 'from', providerName);
});
});
if (options === '') {
console.warn('[CodexLens] No embedding models found in LiteLLM config');
options = '
' + (t('codexlens.noApiModels') || 'No API embedding models configured') + ' ';
}
return options;
}
// Make functions globally accessible
window.onEmbeddingBackendChange = onEmbeddingBackendChange;
/**
* Initialize index from page with selected model
*/
function initCodexLensIndexFromPage(indexType) {
var backendSelect = document.getElementById('pageBackendSelect');
var modelSelect = document.getElementById('pageModelSelect');
var selectedBackend = backendSelect ? backendSelect.value : 'fastembed';
var selectedModel = modelSelect ? modelSelect.value : 'code';
// For FTS-only index, model is not needed
if (indexType === 'normal') {
initCodexLensIndex(indexType);
} else {
initCodexLensIndex(indexType, selectedModel, selectedBackend);
}
}
/**
* 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 = '
' + 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();
}
/**
* 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');
}
}