' +
// SPLADE status hidden - not currently used
// '
' +
// '
' + t('common.loading') + '
' +
// '
' +
'
' +
// Model Management - Simplified with Embedding and Reranker sections
'
' +
'
' +
' ' + t('codexlens.models') +
'
' +
// Embedding Models
'
' +
'
' +
' Embedding Models' +
'
' +
'
' +
'
' + t('codexlens.loadingModels') + '
' +
'
' +
'
' +
// Reranker Models
'
' +
'
' +
' Reranker Models' +
'
' +
'
' +
'
' + t('common.loading') + '
' +
'
' +
'
' +
'
' +
// Danger Zone
'
' +
'
' +
' Danger Zone' +
'
' +
'
' +
'' +
'' +
'
' +
'
' +
'
' +
'
'
: '') +
'
' + // End Tab Content Container
'
' + // End modal-body
// Footer
'' +
'
';
}
/**
* Initialize CodexLens config modal event handlers
*/
function initCodexLensConfigEvents(currentConfig) {
// Tab switching
document.querySelectorAll('.codexlens-tab').forEach(function(tab) {
tab.onclick = function() {
// Remove active from all tabs
document.querySelectorAll('.codexlens-tab').forEach(function(t) {
t.classList.remove('active', 'border-primary', 'text-primary');
t.classList.add('border-transparent', 'text-muted-foreground');
});
// Hide all content
document.querySelectorAll('.codexlens-tab-content').forEach(function(c) {
c.classList.add('hidden');
c.classList.remove('active');
});
// Activate clicked tab
this.classList.add('active', 'border-primary', 'text-primary');
this.classList.remove('border-transparent', 'text-muted-foreground');
// Show corresponding content
var tabName = this.dataset.tab;
var content = document.querySelector('.codexlens-tab-content[data-tab="' + tabName + '"]');
if (content) {
content.classList.remove('hidden');
content.classList.add('active');
}
};
});
// Save button
var saveBtn = document.getElementById('saveCodexLensConfigBtn');
if (saveBtn) {
saveBtn.onclick = async function() {
var indexDirInput = document.getElementById('indexDirInput');
var apiMaxWorkersInput = document.getElementById('apiMaxWorkersInput');
var apiBatchSizeInput = document.getElementById('apiBatchSizeInput');
var newIndexDir = indexDirInput ? indexDirInput.value.trim() : '';
var newMaxWorkers = apiMaxWorkersInput ? parseInt(apiMaxWorkersInput.value) || 4 : 4;
var newBatchSize = apiBatchSizeInput ? parseInt(apiBatchSizeInput.value) || 8 : 8;
if (!newIndexDir) {
showRefreshToast(t('codexlens.pathEmpty'), 'error');
return;
}
// Check if anything changed
var hasChanges = newIndexDir !== currentConfig.index_dir ||
newMaxWorkers !== (currentConfig.api_max_workers || 4) ||
newBatchSize !== (currentConfig.api_batch_size || 8);
if (!hasChanges) {
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,
api_max_workers: newMaxWorkers,
api_batch_size: newBatchSize
})
});
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 searchLimit = document.getElementById('searchLimitInput')?.value || '5';
var contentLength = document.getElementById('contentLengthInput')?.value || '200';
var extraFiles = document.getElementById('extraFilesInput')?.value || '10';
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: searchLimit,
max_content_length: contentLength,
extra_files_count: extraFiles
});
// 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 FastEmbed installation status (show/hide install card)
loadFastEmbedInstallStatus();
// Load semantic dependencies status
loadSemanticDepsStatus();
// SPLADE status hidden - not currently used
// loadSpladeStatus();
// Load model lists (embedding and reranker)
loadModelList();
loadRerankerModelList();
}
// ============================================================
// MODEL LOCK/UNLOCK MANAGEMENT
// ============================================================
var MODEL_LOCK_KEY = 'codexlens_model_lock';
/**
* Get model lock state from localStorage
* @returns {Object} { locked: boolean, backend: string, model: string }
*/
function getModelLockState() {
try {
var stored = localStorage.getItem(MODEL_LOCK_KEY);
if (stored) {
return JSON.parse(stored);
}
} catch (e) {
console.warn('[CodexLens] Failed to get model lock state:', e);
}
return { locked: false, backend: 'fastembed', model: 'code' };
}
/**
* Set model lock state in localStorage
* @param {boolean} locked - Whether model is locked
* @param {string} backend - Selected backend
* @param {string} model - Selected model
*/
function setModelLockState(locked, backend, model) {
try {
localStorage.setItem(MODEL_LOCK_KEY, JSON.stringify({
locked: locked,
backend: backend || 'fastembed',
model: model || 'code'
}));
} catch (e) {
console.warn('[CodexLens] Failed to save model lock state:', e);
}
}
/**
* Toggle model lock state
*/
function toggleModelLock() {
var backendSelect = document.getElementById('pageBackendSelect');
var modelSelect = document.getElementById('pageModelSelect');
var lockBtn = document.getElementById('modelLockBtn');
var lockIcon = document.getElementById('modelLockIcon');
var currentState = getModelLockState();
var newLocked = !currentState.locked;
// Get current values if locking
var backend = newLocked ? (backendSelect ? backendSelect.value : 'fastembed') : currentState.backend;
var model = newLocked ? (modelSelect ? modelSelect.value : 'code') : currentState.model;
// Save state
setModelLockState(newLocked, backend, model);
// Update UI
applyModelLockUI(newLocked, backend, model);
// Show feedback
if (newLocked) {
showRefreshToast('Model locked: ' + backend + ' / ' + model, 'success');
} else {
showRefreshToast('Model unlocked', 'info');
}
}
/**
* Apply model lock UI state
*/
function applyModelLockUI(locked, backend, model) {
var backendSelect = document.getElementById('pageBackendSelect');
var modelSelect = document.getElementById('pageModelSelect');
var lockBtn = document.getElementById('modelLockBtn');
var lockIcon = document.getElementById('modelLockIcon');
var lockText = document.getElementById('modelLockText');
if (backendSelect) {
backendSelect.disabled = locked;
if (locked && backend) {
backendSelect.value = backend;
}
}
if (modelSelect) {
modelSelect.disabled = locked;
if (locked && model) {
modelSelect.value = model;
}
}
if (lockBtn) {
if (locked) {
lockBtn.classList.remove('btn-outline');
lockBtn.classList.add('btn-primary');
} else {
lockBtn.classList.remove('btn-primary');
lockBtn.classList.add('btn-outline');
}
}
if (lockIcon) {
lockIcon.setAttribute('data-lucide', locked ? 'lock' : 'unlock');
if (window.lucide) lucide.createIcons();
}
if (lockText) {
lockText.textContent = locked ? 'Locked' : 'Lock Model';
}
}
/**
* Initialize model lock state on page load
*/
function initModelLockState() {
var state = getModelLockState();
if (state.locked) {
applyModelLockUI(true, state.backend, state.model);
}
}
// Make functions globally accessible
window.toggleModelLock = toggleModelLock;
window.initModelLockState = initModelLockState;
window.getModelLockState = getModelLockState;
// ============================================================
// ENVIRONMENT VARIABLES MANAGEMENT
// ============================================================
// Environment variable groups for organized display
// Maps to settings.json structure in ~/.codexlens/settings.json
// Embedding and Reranker are configured separately
var ENV_VAR_GROUPS = {
embedding: {
labelKey: 'codexlens.envGroup.embedding',
icon: 'box',
vars: {
'CODEXLENS_EMBEDDING_BACKEND': { label: 'Backend', type: 'select', options: ['local', 'api'], default: 'local', settingsPath: 'embedding.backend' },
'CODEXLENS_EMBEDDING_MODEL': {
label: 'Model',
type: 'model-select',
placeholder: 'Select or enter model...',
default: 'fast',
settingsPath: 'embedding.model',
localModels: [
{ group: 'FastEmbed Profiles', items: ['fast', 'code', 'base', 'minilm', 'multilingual', 'balanced'] }
],
apiModels: [
{ group: 'OpenAI', items: ['text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002'] },
{ group: 'Cohere', items: ['embed-english-v3.0', 'embed-multilingual-v3.0', 'embed-english-light-v3.0'] },
{ group: 'Voyage', items: ['voyage-3', 'voyage-3-lite', 'voyage-code-3', 'voyage-multilingual-2'] },
{ group: 'SiliconFlow', items: ['BAAI/bge-m3', 'BAAI/bge-large-zh-v1.5', 'BAAI/bge-large-en-v1.5'] },
{ group: 'Jina', items: ['jina-embeddings-v3', 'jina-embeddings-v2-base-en', 'jina-embeddings-v2-base-zh'] }
]
},
'CODEXLENS_USE_GPU': { label: 'Use GPU', type: 'select', options: ['true', 'false'], default: 'true', settingsPath: 'embedding.use_gpu', showWhen: function(env) { return env['CODEXLENS_EMBEDDING_BACKEND'] !== 'litellm'; } },
'CODEXLENS_EMBEDDING_STRATEGY': { label: 'Load Balance', type: 'select', options: ['round_robin', 'latency_aware', 'weighted_random'], default: 'latency_aware', settingsPath: 'embedding.strategy', showWhen: function(env) { return env['CODEXLENS_EMBEDDING_BACKEND'] === 'litellm'; } },
'CODEXLENS_EMBEDDING_COOLDOWN': { label: 'Rate Limit Cooldown (s)', type: 'number', placeholder: '60', default: '60', settingsPath: 'embedding.cooldown', min: 0, max: 300, showWhen: function(env) { return env['CODEXLENS_EMBEDDING_BACKEND'] === 'litellm'; } }
}
},
reranker: {
labelKey: 'codexlens.envGroup.reranker',
icon: 'arrow-up-down',
vars: {
'CODEXLENS_RERANKER_ENABLED': { label: 'Enabled', type: 'select', options: ['true', 'false'], default: 'true', settingsPath: 'reranker.enabled' },
'CODEXLENS_RERANKER_BACKEND': { label: 'Backend', type: 'select', options: ['local', 'api'], default: 'local', settingsPath: 'reranker.backend' },
'CODEXLENS_RERANKER_MODEL': {
label: 'Model',
type: 'model-select',
placeholder: 'Select or enter model...',
default: 'Xenova/ms-marco-MiniLM-L-6-v2',
settingsPath: 'reranker.model',
localModels: [
{ group: 'FastEmbed/ONNX', items: ['Xenova/ms-marco-MiniLM-L-6-v2', 'cross-encoder/ms-marco-MiniLM-L-6-v2', 'BAAI/bge-reranker-base'] }
],
apiModels: [
{ group: 'Cohere', items: ['rerank-english-v3.0', 'rerank-multilingual-v3.0', 'rerank-english-v2.0'] },
{ group: 'Voyage', items: ['rerank-2', 'rerank-2-lite', 'rerank-1'] },
{ group: 'SiliconFlow', items: ['BAAI/bge-reranker-v2-m3', 'BAAI/bge-reranker-large', 'BAAI/bge-reranker-base'] },
{ group: 'Jina', items: ['jina-reranker-v2-base-multilingual', 'jina-reranker-v1-base-en'] }
]
},
'CODEXLENS_RERANKER_TOP_K': { label: 'Top K Results', type: 'number', placeholder: '50', default: '50', settingsPath: 'reranker.top_k', min: 5, max: 200 }
}
},
concurrency: {
labelKey: 'codexlens.envGroup.concurrency',
icon: 'cpu',
vars: {
'CODEXLENS_API_MAX_WORKERS': { label: 'Max Workers', type: 'number', placeholder: '4', default: '4', settingsPath: 'api.max_workers', min: 1, max: 32 },
'CODEXLENS_API_BATCH_SIZE': { label: 'Batch Size', type: 'number', placeholder: '8', default: '8', settingsPath: 'api.batch_size', min: 1, max: 64 }
}
},
cascade: {
labelKey: 'codexlens.envGroup.cascade',
icon: 'git-branch',
vars: {
'CODEXLENS_CASCADE_STRATEGY': { label: 'Search Strategy', type: 'select', options: ['binary', 'hybrid', 'binary_rerank', 'dense_rerank'], default: 'dense_rerank', settingsPath: 'cascade.strategy' },
'CODEXLENS_CASCADE_COARSE_K': { label: 'Coarse K (1st stage)', type: 'number', placeholder: '100', default: '100', settingsPath: 'cascade.coarse_k', min: 10, max: 500 },
'CODEXLENS_CASCADE_FINE_K': { label: 'Fine K (final)', type: 'number', placeholder: '10', default: '10', settingsPath: 'cascade.fine_k', min: 1, max: 100 }
}
},
llm: {
labelKey: 'codexlens.envGroup.llm',
icon: 'sparkles',
collapsed: true,
vars: {
'CODEXLENS_LLM_ENABLED': { label: 'Enable LLM', type: 'select', options: ['true', 'false'], default: 'false', settingsPath: 'llm.enabled' },
'CODEXLENS_LLM_BATCH_SIZE': { label: 'Batch Size', type: 'number', placeholder: '5', default: '5', settingsPath: 'llm.batch_size', min: 1, max: 20 }
}
}
};
/**
* Load environment variables from ~/.codexlens/.env
*/
async function loadEnvVariables() {
var container = document.getElementById('envVarsContainer');
if (!container) return;
container.innerHTML = '
Loading...
';
try {
// Fetch env vars, configured models, and local models in parallel
var [envResponse, embeddingPoolResponse, rerankerPoolResponse, localModelsResponse, localRerankerModelsResponse] = await Promise.all([
fetch('/api/codexlens/env'),
fetch('/api/litellm-api/embedding-pool').catch(function() { return null; }),
fetch('/api/litellm-api/reranker-pool').catch(function() { return null; }),
fetch('/api/codexlens/models').catch(function() { return null; }),
fetch('/api/codexlens/reranker/models').catch(function() { return null; })
]);
var result = await envResponse.json();
if (!result.success) {
container.innerHTML = '
' + escapeHtml(result.error || 'Failed to load') + '
';
return;
}
// Get configured embedding models from API settings
var configuredEmbeddingModels = [];
if (embeddingPoolResponse && embeddingPoolResponse.ok) {
var poolData = await embeddingPoolResponse.json();
configuredEmbeddingModels = poolData.availableModels || [];
}
// Get configured reranker models from API settings
var configuredRerankerModels = [];
if (rerankerPoolResponse && rerankerPoolResponse.ok) {
var rerankerData = await rerankerPoolResponse.json();
configuredRerankerModels = rerankerData.availableModels || [];
}
// Get local downloaded embedding models
var localEmbeddingModels = [];
if (localModelsResponse && localModelsResponse.ok) {
var localData = await localModelsResponse.json();
// CLI returns { success: true, result: { models: [...] } }
if (localData.success) {
var models = localData.models || (localData.result && localData.result.models) || [];
// Filter to only installed models (CLI uses 'installed' not 'downloaded')
localEmbeddingModels = models.filter(function(m) { return m.installed; });
}
}
// Get local downloaded reranker models
var localRerankerModels = [];
if (localRerankerModelsResponse && localRerankerModelsResponse.ok) {
var localRerankerData = await localRerankerModelsResponse.json();
// CLI returns { success: true, result: { models: [...] } }
if (localRerankerData.success) {
var models = localRerankerData.models || (localRerankerData.result && localRerankerData.result.models) || [];
// Filter to only installed models
localRerankerModels = models.filter(function(m) { return m.installed; });
}
}
// Cache model data for dynamic backend switching
var embeddingVars = ENV_VAR_GROUPS.embedding.vars;
var rerankerVars = ENV_VAR_GROUPS.reranker.vars;
cachedEmbeddingModels = {
local: localEmbeddingModels,
api: configuredEmbeddingModels,
apiModels: embeddingVars['CODEXLENS_EMBEDDING_MODEL'] ? embeddingVars['CODEXLENS_EMBEDDING_MODEL'].apiModels || [] : []
};
cachedRerankerModels = {
local: localRerankerModels,
api: configuredRerankerModels,
apiModels: rerankerVars['CODEXLENS_RERANKER_MODEL'] ? rerankerVars['CODEXLENS_RERANKER_MODEL'].apiModels || [] : []
};
var env = result.env || {};
var settings = result.settings || {}; // Current settings from settings.json
var html = '
';
// Get available LiteLLM providers
var litellmProviders = window.litellmApiConfig?.providers || [];
// Render each group
for (var groupKey in ENV_VAR_GROUPS) {
var group = ENV_VAR_GROUPS[groupKey];
// Check if this group should be shown
if (group.showWhen && !group.showWhen(env)) {
continue;
}
var groupLabel = group.labelKey ? t(group.labelKey) : group.label;
html += '
' +
'
' +
'' +
groupLabel +
'
' +
'
';
// Add provider selector for API group
if (groupKey === 'api' && litellmProviders.length > 0) {
html += '
' +
'' +
'
';
}
for (var key in group.vars) {
var config = group.vars[key];
// Check variable-level showWhen condition
if (config.showWhen && !config.showWhen(env)) {
continue;
}
// Priority: env file > settings.json > hardcoded default
var value = env[key] || settings[key] || config.default || '';
if (config.type === 'select') {
// Add onchange handler for backend selects to update model options dynamically
var onchangeHandler = '';
if (key === 'CODEXLENS_EMBEDDING_BACKEND' || key === 'CODEXLENS_RERANKER_BACKEND') {
onchangeHandler = ' onchange="updateModelOptionsOnBackendChange(\'' + key + '\', this.value)"';
}
html += '
' +
'' +
'
';
} else if (config.type === 'model-select') {
// Model selector with grouped options and custom input support
// Supports localModels/apiModels based on backend type
var datalistId = 'models-' + key.replace(/_/g, '-').toLowerCase();
var isEmbedding = key.indexOf('EMBEDDING') !== -1;
var isReranker = key.indexOf('RERANKER') !== -1;
var backendKey = isEmbedding ? 'CODEXLENS_EMBEDDING_BACKEND' : 'CODEXLENS_RERANKER_BACKEND';
var isApiBackend = env[backendKey] === 'litellm' || env[backendKey] === 'api';
// Get actual downloaded local models
var actualLocalModels = isEmbedding ? localEmbeddingModels : localRerankerModels;
// Get configured API models
var configuredModels = isEmbedding ? configuredEmbeddingModels : configuredRerankerModels;
// Fallback preset list for API models
var apiModelList = config.apiModels || [];
html += '
' +
'' +
'
' +
'' +
'' +
'
' +
'
';
} else {
var inputType = config.type || 'text';
var extraAttrs = '';
if (config.type === 'number') {
if (config.min !== undefined) extraAttrs += ' min="' + config.min + '"';
if (config.max !== undefined) extraAttrs += ' max="' + config.max + '"';
extraAttrs += ' step="1"';
}
html += '
';
container.innerHTML = html;
if (window.lucide) lucide.createIcons();
// Add change handler for backend selects to dynamically update model options
// Note: Does NOT auto-save - user must click Save button
var backendSelects = container.querySelectorAll('select[data-env-key*="BACKEND"]');
backendSelects.forEach(function(select) {
select.addEventListener('change', function() {
var backendKey = select.getAttribute('data-env-key');
var newBackend = select.value;
// 'api' is the API backend, 'local' is the local backend
var isApiBackend = newBackend === 'api';
// Determine which model input to update
var isEmbedding = backendKey.indexOf('EMBEDDING') !== -1;
var modelKey = isEmbedding ? 'CODEXLENS_EMBEDDING_MODEL' : 'CODEXLENS_RERANKER_MODEL';
var modelInput = document.querySelector('[data-env-key="' + modelKey + '"]');
if (modelInput) {
var datalistId = modelInput.getAttribute('list');
var datalist = document.getElementById(datalistId);
if (datalist) {
// Get model config from ENV_VAR_GROUPS
var groupKey = isEmbedding ? 'embedding' : 'reranker';
var modelConfig = ENV_VAR_GROUPS[groupKey]?.vars[modelKey];
if (modelConfig) {
// Use the loaded models from closure
var apiModelList = modelConfig.apiModels || [];
var apiConfiguredModels = isEmbedding ? configuredEmbeddingModels : configuredRerankerModels;
var actualLocalModels = isEmbedding ? localEmbeddingModels : localRerankerModels;
// Rebuild datalist
var optionsHtml = '';
if (isApiBackend) {
// For API backend: show configured models from API settings first
if (apiConfiguredModels.length > 0) {
optionsHtml += '';
apiConfiguredModels.forEach(function(model) {
var providers = model.providers ? model.providers.join(', ') : '';
optionsHtml += '';
});
}
// Then show common API models as suggestions
if (apiModelList.length > 0) {
optionsHtml += '';
apiModelList.forEach(function(group) {
group.items.forEach(function(model) {
var exists = apiConfiguredModels.some(function(m) { return m.modelId === model; });
if (!exists) {
optionsHtml += '';
}
});
});
}
} else {
// For local backend: show actually downloaded models
if (actualLocalModels.length > 0) {
optionsHtml += '';
actualLocalModels.forEach(function(model) {
var modelId = model.profile || model.model_id || model.id || model.name;
var displayName = model.display_name || model.name || model.profile || modelId;
var displayText = model.profile && model.name ?
model.profile + ' (' + model.name + ')' : displayName;
optionsHtml += '';
});
} else {
optionsHtml += '';
}
}
datalist.innerHTML = optionsHtml;
// Clear current model value when switching backend type
modelInput.value = '';
modelInput.placeholder = isApiBackend ?
(t('codexlens.selectApiModel') || 'Select API model...') :
(t('codexlens.selectLocalModel') || 'Select local model...');
}
}
}
// Note: No auto-save here - user must click Save button
});
});
} catch (err) {
container.innerHTML = '
' + escapeHtml(err.message) + '
';
}
}
/**
* Apply LiteLLM provider settings to environment variables
* Note: API credentials are now managed via API Settings page
*/
function applyLiteLLMProvider(providerId) {
if (!providerId) return;
var providers = window.litellmApiConfig?.providers || [];
var provider = providers.find(function(p) {
return (p.id || p.name) === providerId;
});
if (!provider) {
console.warn('[CodexLens] Provider not found:', providerId);
return;
}
// Auto-fill model fields based on provider
var embeddingModelInput = document.querySelector('[data-env-key="CODEXLENS_EMBEDDING_MODEL"]');
var rerankerModelInput = document.querySelector('[data-env-key="CODEXLENS_RERANKER_MODEL"]');
// Set default models based on provider type
var providerName = (provider.name || provider.id || '').toLowerCase();
if (embeddingModelInput) {
if (providerName.includes('openai')) {
embeddingModelInput.value = embeddingModelInput.value || 'text-embedding-3-small';
} else if (providerName.includes('cohere')) {
embeddingModelInput.value = embeddingModelInput.value || 'embed-english-v3.0';
} else if (providerName.includes('voyage')) {
embeddingModelInput.value = embeddingModelInput.value || 'voyage-2';
} else if (provider.embedding_model) {
embeddingModelInput.value = provider.embedding_model;
}
}
if (rerankerModelInput) {
if (providerName.includes('cohere')) {
rerankerModelInput.value = rerankerModelInput.value || 'rerank-english-v3.0';
} else if (providerName.includes('voyage')) {
rerankerModelInput.value = rerankerModelInput.value || 'rerank-1';
} else if (provider.reranker_model) {
rerankerModelInput.value = provider.reranker_model;
}
}
showRefreshToast('Applied settings from: ' + (provider.name || providerId), 'success');
}
// Make function globally accessible
window.applyLiteLLMProvider = applyLiteLLMProvider;
/**
* Update model datalist options when backend changes
* @param {string} backendKey - The backend key that changed (CODEXLENS_EMBEDDING_BACKEND or CODEXLENS_RERANKER_BACKEND)
* @param {string} newBackend - The new backend value ('local' or 'api')
*/
function updateModelOptionsOnBackendChange(backendKey, newBackend) {
var isEmbedding = backendKey === 'CODEXLENS_EMBEDDING_BACKEND';
var modelKey = isEmbedding ? 'CODEXLENS_EMBEDDING_MODEL' : 'CODEXLENS_RERANKER_MODEL';
var datalistId = 'models-' + modelKey.replace(/_/g, '-').toLowerCase();
var datalist = document.getElementById(datalistId);
if (!datalist) return;
var isApiBackend = newBackend === 'api' || newBackend === 'litellm';
var cachedModels = isEmbedding ? cachedEmbeddingModels : cachedRerankerModels;
var html = '';
if (isApiBackend) {
// For API backend: show configured models from API settings first
var configuredModels = cachedModels.api || [];
if (configuredModels.length > 0) {
html += '';
configuredModels.forEach(function(model) {
var providers = model.providers ? model.providers.join(', ') : '';
html += '';
});
}
// Then show common API models as suggestions
var apiModelList = cachedModels.apiModels || [];
if (apiModelList.length > 0) {
html += '';
apiModelList.forEach(function(group) {
group.items.forEach(function(model) {
// Skip if already in configured list
var exists = configuredModels.some(function(m) { return m.modelId === model; });
if (!exists) {
html += '';
}
});
});
}
} else {
// For local backend: show actually downloaded models
var localModels = cachedModels.local || [];
if (localModels.length > 0) {
html += '';
localModels.forEach(function(model) {
var modelId = model.profile || model.model_id || model.id || model.name;
var displayName = model.display_name || model.name || model.profile || modelId;
var displayText = model.profile && model.name ?
model.profile + ' (' + model.name + ')' : displayName;
html += '';
});
} else {
html += '';
}
}
datalist.innerHTML = html;
}
// Make function globally accessible
window.updateModelOptionsOnBackendChange = updateModelOptionsOnBackendChange;
/**
* Save environment variables to ~/.codexlens/.env
*/
async function saveEnvVariables() {
var inputs = document.querySelectorAll('[data-env-key]');
var env = {};
inputs.forEach(function(input) {
var key = input.dataset.envKey;
var value = input.value.trim();
if (value) {
env[key] = value;
}
});
try {
var response = await fetch('/api/codexlens/env', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ env: env })
});
var result = await response.json();
if (result.success) {
showRefreshToast('Environment configuration saved', 'success');
} else {
showRefreshToast('Failed to save: ' + result.error, 'error');
}
} catch (err) {
showRefreshToast('Error: ' + err.message, 'error');
}
}
// ============================================================
// SEMANTIC DEPENDENCIES MANAGEMENT
// ============================================================
// Store detected GPU info
var detectedGpuInfo = null;
// Store available GPU devices
var availableGpuDevices = null;
// Store model data for dynamic backend switching
var cachedEmbeddingModels = { local: [], api: [], apiModels: [] };
var cachedRerankerModels = { local: [], api: [], apiModels: [] };
/**
* 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 = 'cpu';
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 =
'
';
modes.forEach(function(mode) {
var isDisabled = !mode.available;
var isRecommended = mode.recommended;
var isDefault = mode.id === gpuInfo.mode;
var hasWarning = mode.warning;
html +=
'';
});
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.installMayTakeTime') || 'This may take several minutes. Please do not close this page.') +
'
' +
'
' +
'' +
'
' +
'
' +
'
';
try {
var response = await fetch('/api/codexlens/semantic/install', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ gpuMode: selectedMode })
});
var result = await response.json();
if (result.success) {
showRefreshToast((t('codexlens.fastembedInstalled') || 'FastEmbed installed') + ' (' + modeLabels[selectedMode] + ')', 'success');
// Hide card and reload status
await loadFastEmbedInstallStatus();
await loadSemanticDepsStatus();
await loadModelList();
} else {
showRefreshToast((t('codexlens.fastembedInstallFailed') || 'FastEmbed installation failed') + ': ' + result.error, 'error');
await loadFastEmbedInstallStatus();
}
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
await loadFastEmbedInstallStatus();
}
}
/**
* 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 (simplified version)
*/
async function loadModelList() {
var container = document.getElementById('modelListContainer');
if (!container) return;
try {
// Get config for backend info
var configResponse = await fetch('/api/codexlens/config');
var config = await configResponse.json();
var embeddingBackend = config.embedding_backend || 'fastembed';
var response = await fetch('/api/codexlens/models');
var result = await response.json();
var html = '
';
// Show current backend status
var backendLabel = embeddingBackend === 'litellm' ? 'API (LiteLLM)' : 'Local (FastEmbed)';
var backendIcon = embeddingBackend === 'litellm' ? 'cloud' : 'hard-drive';
html +=
'
' +
'
' +
'' +
'' + backendLabel + '' +
'
' +
'via Environment Variables' +
'
';
if (!result.success) {
var errorMsg = result.error || '';
if (errorMsg.includes('fastembed not installed') || errorMsg.includes('Semantic')) {
// Just show a simple message - installation UI is in the separate card above
html += '
' +
'' +
'' + (t('codexlens.installFastembedFirst') || 'Install FastEmbed above to manage local embedding models') + '' +
'
';
});
} 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) - fallback if API unavailable
var RERANKER_MODELS = [
{ id: 'ms-marco-mini', name: 'Xenova/ms-marco-MiniLM-L-6-v2', size: 90, desc: 'Fast, lightweight', recommended: true },
{ id: 'ms-marco-12', name: 'Xenova/ms-marco-MiniLM-L-12-v2', size: 130, desc: 'Better accuracy', recommended: true },
{ id: 'bge-base', name: 'BAAI/bge-reranker-base', size: 280, desc: 'High quality', recommended: true },
{ id: 'bge-large', name: 'BAAI/bge-reranker-large', size: 560, desc: 'Maximum quality', recommended: false },
{ id: 'jina-tiny', name: 'jinaai/jina-reranker-v1-tiny-en', size: 70, desc: 'Tiny, fast', recommended: true },
{ id: 'jina-turbo', name: 'jinaai/jina-reranker-v1-turbo-en', size: 150, desc: 'Balanced', recommended: true }
];
/**
* Load reranker model list with download/delete support
*/
async function loadRerankerModelList() {
// Update both containers (advanced tab and page model management)
var containers = [
document.getElementById('rerankerModelListContainer'),
document.getElementById('pageRerankerModelListContainer')
].filter(Boolean);
console.log('[CodexLens] loadRerankerModelList - containers found:', containers.length);
if (containers.length === 0) {
console.warn('[CodexLens] No reranker model list containers found');
return;
}
try {
// Fetch both config and models list in parallel
var [configResponse, modelsResponse] = await Promise.all([
fetch('/api/codexlens/reranker/config'),
fetch('/api/codexlens/reranker/models')
]);
if (!configResponse.ok) {
throw new Error('Failed to load reranker config: ' + configResponse.status);
}
var config = await configResponse.json();
console.log('[CodexLens] Reranker config loaded:', { backend: config.backend, model: config.model_name });
// Handle API response format
var currentModel = config.model_name || config.result?.reranker_model || 'Xenova/ms-marco-MiniLM-L-6-v2';
var currentBackend = config.backend || config.result?.reranker_backend || 'fastembed';
// Try to use API models, fall back to static list
var models = RERANKER_MODELS;
var modelsFromApi = false;
if (modelsResponse.ok) {
var modelsData = await modelsResponse.json();
if (modelsData.success && modelsData.result && modelsData.result.models) {
models = modelsData.result.models.map(function(m) {
return {
id: m.profile,
name: m.model_name,
size: m.installed && m.actual_size_mb ? m.actual_size_mb : m.estimated_size_mb,
desc: m.description,
installed: m.installed,
recommended: m.recommended
};
});
modelsFromApi = true;
console.log('[CodexLens] Loaded ' + models.length + ' reranker models from API');
}
}
var html = '
';
// Show current backend status
var isApiBackend = currentBackend === 'litellm' || currentBackend === 'api';
var backendLabel = isApiBackend ? 'API (' + (currentBackend === 'litellm' ? 'LiteLLM' : 'Remote') + ')' : 'Local (FastEmbed)';
var backendIcon = isApiBackend ? 'cloud' : 'hard-drive';
html +=
'
' +
'
' +
'' +
'' + backendLabel + '' +
'
' +
'via Environment Variables' +
'
';
// Helper to match model names (handles different prefixes like Xenova/ vs cross-encoder/)
function modelMatches(current, target) {
if (!current || !target) return false;
// Exact match
if (current === target) return true;
// Match by base name (after last /)
var currentBase = current.split('/').pop();
var targetBase = target.split('/').pop();
return currentBase === targetBase;
}
// Show API info when using API backend
if (isApiBackend) {
html +=
'
';
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;
// Load FastEmbed installation status (show/hide install card)
loadFastEmbedInstallStatus();
// 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();
// 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 = '