feat: Unified Embedding Pool with auto-discovery

Architecture refactoring for multi-provider rotation:

Backend:
- Add EmbeddingPoolConfig type with autoDiscover support
- Implement discoverProvidersForModel() for auto-aggregation
- Add GET/PUT /api/litellm-api/embedding-pool endpoints
- Add GET /api/litellm-api/embedding-pool/discover/:model preview
- Convert ccw-litellm status check to async with 5-min cache
- Maintain backward compatibility with legacy rotation config

Frontend:
- Add "Embedding Pool" tab in API Settings
- Auto-discover providers when target model selected
- Show provider/key count with include/exclude controls
- Increase sidebar width (280px → 320px)
- Add sync result feedback on save

Other:
- Remove worker count limits (was max=32)
- Add i18n translations (EN/CN)
- Update .gitignore for .mcp.json

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
catlog22
2025-12-25 16:06:49 +08:00
parent 4e6ee2db25
commit a1413dd1b3
10 changed files with 882 additions and 43 deletions

View File

@@ -958,8 +958,8 @@ select.cli-input {
/* Left Sidebar */
.api-settings-sidebar {
width: 280px;
min-width: 240px;
width: 320px;
min-width: 280px;
border-right: 1px solid hsl(var(--border));
display: flex;
flex-direction: column;

View File

@@ -19,6 +19,7 @@ const i18n = {
'common.delete': 'Delete',
'common.cancel': 'Cancel',
'common.save': 'Save',
'common.include': 'Include',
'common.close': 'Close',
'common.loading': 'Loading...',
'common.error': 'Error',
@@ -28,6 +29,8 @@ const i18n = {
'common.retry': 'Retry',
'common.refresh': 'Refresh',
'common.minutes': 'minutes',
'common.enabled': 'Enabled',
'common.disabled': 'Disabled',
// Header
'header.project': 'Project:',
@@ -267,7 +270,7 @@ const i18n = {
'codexlens.embeddingModel': 'Embedding Model',
'codexlens.modelHint': 'Select embedding model for vector search (models with ✓ are installed)',
'codexlens.concurrency': 'API Concurrency',
'codexlens.concurrencyHint': 'Number of parallel API calls (1-32). Higher values speed up indexing but may hit rate limits.',
'codexlens.concurrencyHint': 'Number of parallel API calls. Higher values speed up indexing but may hit rate limits.',
'codexlens.concurrencyCustom': 'Custom',
'codexlens.rotation': 'Multi-Provider Rotation',
'codexlens.rotationDesc': 'Aggregate multiple API providers and keys for parallel embedding generation',
@@ -289,6 +292,8 @@ const i18n = {
'codexlens.selectKeys': 'Select Keys',
'codexlens.configureRotation': 'Configure Rotation',
'codexlens.rotationSaved': 'Rotation config saved successfully',
'codexlens.endpointsSynced': 'endpoints synced to CodexLens',
'codexlens.syncFailed': 'Sync failed',
'codexlens.rotationDeleted': 'Rotation config deleted',
'codexlens.totalEndpoints': 'Total Endpoints',
'codexlens.fullIndex': 'Full',
@@ -312,6 +317,9 @@ const i18n = {
'codexlens.runSearch': 'Run Search',
'codexlens.results': 'Results',
'codexlens.resultsCount': 'results',
'codexlens.resultLimit': 'Limit',
'codexlens.contentLength': 'Content Length',
'codexlens.extraFiles': 'Extra Files',
'codexlens.saveConfig': 'Save Configuration',
'codexlens.searching': 'Searching...',
'codexlens.searchCompleted': 'Search completed',
@@ -1470,6 +1478,20 @@ const i18n = {
'apiSettings.endpointDeleted': 'Endpoint deleted successfully',
'apiSettings.cacheCleared': 'Cache cleared successfully',
'apiSettings.cacheSettingsUpdated': 'Cache settings updated',
'apiSettings.embeddingPool': 'Embedding Pool',
'apiSettings.embeddingPoolDesc': 'Auto-rotate between providers with same model',
'apiSettings.targetModel': 'Target Model',
'apiSettings.discoveredProviders': 'Discovered Providers',
'apiSettings.autoDiscover': 'Auto-discover providers',
'apiSettings.excludeProvider': 'Exclude',
'apiSettings.defaultCooldown': 'Cooldown (seconds)',
'apiSettings.defaultConcurrent': 'Concurrent per key',
'apiSettings.poolEnabled': 'Enable Embedding Pool',
'apiSettings.noProvidersFound': 'No providers found for this model',
'apiSettings.poolSaved': 'Embedding pool config saved',
'apiSettings.strategy': 'Strategy',
'apiSettings.providerKeys': 'keys',
'apiSettings.selectTargetModel': 'Select target model',
'apiSettings.confirmDeleteProvider': 'Are you sure you want to delete this provider?',
'apiSettings.confirmDeleteEndpoint': 'Are you sure you want to delete this endpoint?',
'apiSettings.confirmClearCache': 'Are you sure you want to clear the cache?',
@@ -1703,6 +1725,7 @@ const i18n = {
'common.delete': '删除',
'common.cancel': '取消',
'common.save': '保存',
'common.include': '包含',
'common.close': '关闭',
'common.loading': '加载中...',
'common.error': '错误',
@@ -1712,6 +1735,8 @@ const i18n = {
'common.retry': '重试',
'common.refresh': '刷新',
'common.minutes': '分钟',
'common.enabled': '已启用',
'common.disabled': '已禁用',
// Header
'header.project': '项目:',
@@ -1951,7 +1976,7 @@ const i18n = {
'codexlens.embeddingModel': '嵌入模型',
'codexlens.modelHint': '选择向量搜索的嵌入模型(带 ✓ 的已安装)',
'codexlens.concurrency': 'API 并发数',
'codexlens.concurrencyHint': '并行 API 调用数量1-32。较高的值可加速索引但可能触发速率限制。',
'codexlens.concurrencyHint': '并行 API 调用数量。较高的值可加速索引但可能触发速率限制。',
'codexlens.concurrencyCustom': '自定义',
'codexlens.rotation': '多供应商轮训',
'codexlens.rotationDesc': '聚合多个 API 供应商和密钥进行并行嵌入生成',
@@ -1973,6 +1998,8 @@ const i18n = {
'codexlens.selectKeys': '选择密钥',
'codexlens.configureRotation': '配置轮训',
'codexlens.rotationSaved': '轮训配置保存成功',
'codexlens.endpointsSynced': '个端点已同步到 CodexLens',
'codexlens.syncFailed': '同步失败',
'codexlens.rotationDeleted': '轮训配置已删除',
'codexlens.totalEndpoints': '总端点数',
'codexlens.fullIndex': '全部',
@@ -1996,6 +2023,9 @@ const i18n = {
'codexlens.runSearch': '运行搜索',
'codexlens.results': '结果',
'codexlens.resultsCount': '个结果',
'codexlens.resultLimit': '数量限制',
'codexlens.contentLength': '内容长度',
'codexlens.extraFiles': '额外文件',
'codexlens.saveConfig': '保存配置',
'codexlens.searching': '搜索中...',
'codexlens.searchCompleted': '搜索完成',
@@ -3163,6 +3193,20 @@ const i18n = {
'apiSettings.endpointDeleted': '端点删除成功',
'apiSettings.cacheCleared': '缓存清除成功',
'apiSettings.cacheSettingsUpdated': '缓存设置已更新',
'apiSettings.embeddingPool': '高可用嵌入',
'apiSettings.embeddingPoolDesc': '自动轮训相同模型的供应商',
'apiSettings.targetModel': '目标模型',
'apiSettings.discoveredProviders': '发现的供应商',
'apiSettings.autoDiscover': '自动发现供应商',
'apiSettings.excludeProvider': '排除',
'apiSettings.defaultCooldown': '冷却时间(秒)',
'apiSettings.defaultConcurrent': '每密钥并发数',
'apiSettings.poolEnabled': '启用嵌入池',
'apiSettings.noProvidersFound': '未找到提供此模型的供应商',
'apiSettings.poolSaved': '嵌入池配置已保存',
'apiSettings.strategy': '策略',
'apiSettings.providerKeys': '密钥',
'apiSettings.selectTargetModel': '选择目标模型',
'apiSettings.confirmDeleteProvider': '确定要删除此提供商吗?',
'apiSettings.confirmDeleteEndpoint': '确定要删除此端点吗?',
'apiSettings.confirmClearCache': '确定要清除缓存吗?',

View File

@@ -11,7 +11,12 @@ let selectedProviderId = null;
let providerSearchQuery = '';
let activeModelTab = 'llm';
let expandedModelGroups = new Set();
let activeSidebarTab = 'providers'; // 'providers' | 'endpoints' | 'cache'
let activeSidebarTab = 'providers'; // 'providers' | 'endpoints' | 'cache' | 'embedding-pool'
// Embedding Pool state
let embeddingPoolConfig = null;
let embeddingPoolAvailableModels = [];
let embeddingPoolDiscoveredProviders = [];
// ========== Data Loading ==========
@@ -61,6 +66,112 @@ async function loadCacheStats() {
}
}
/**
* Load embedding pool configuration and available models
*/
async function loadEmbeddingPoolConfig() {
try {
const response = await fetch('/api/litellm-api/embedding-pool');
if (!response.ok) throw new Error('Failed to load embedding pool config');
const data = await response.json();
embeddingPoolConfig = data.poolConfig;
embeddingPoolAvailableModels = data.availableModels || [];
// If pool is enabled and has a target model, discover providers
if (embeddingPoolConfig && embeddingPoolConfig.enabled && embeddingPoolConfig.targetModel) {
await discoverProvidersForTargetModel(embeddingPoolConfig.targetModel);
}
return data;
} catch (err) {
console.error('Failed to load embedding pool config:', err);
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
return null;
}
}
/**
* Discover providers for a specific target model
*/
async function discoverProvidersForTargetModel(targetModel) {
try {
const response = await fetch('/api/litellm-api/embedding-pool/discover/' + encodeURIComponent(targetModel));
if (!response.ok) throw new Error('Failed to discover providers');
const data = await response.json();
embeddingPoolDiscoveredProviders = data.discovered || [];
return data;
} catch (err) {
console.error('Failed to discover providers:', err);
embeddingPoolDiscoveredProviders = [];
return null;
}
}
/**
* Save embedding pool configuration
*/
async function saveEmbeddingPoolConfig() {
try {
const enabled = document.getElementById('embedding-pool-enabled')?.checked || false;
const targetModel = document.getElementById('embedding-pool-target-model')?.value || '';
const strategy = document.getElementById('embedding-pool-strategy')?.value || 'round_robin';
const defaultCooldown = parseInt(document.getElementById('embedding-pool-cooldown')?.value || '60');
const defaultMaxConcurrentPerKey = parseInt(document.getElementById('embedding-pool-concurrent')?.value || '4');
const poolConfig = enabled ? {
enabled: true,
targetModel: targetModel,
strategy: strategy,
autoDiscover: true,
excludedProviderIds: embeddingPoolConfig?.excludedProviderIds || [],
defaultCooldown: defaultCooldown,
defaultMaxConcurrentPerKey: defaultMaxConcurrentPerKey
} : null;
const response = await fetch('/api/litellm-api/embedding-pool', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(poolConfig)
});
if (!response.ok) throw new Error('Failed to save embedding pool config');
const result = await response.json();
embeddingPoolConfig = result.poolConfig;
const syncCount = result.syncResult?.syncedEndpoints?.length || 0;
showRefreshToast(t('apiSettings.poolSaved') + (syncCount > 0 ? ' (' + syncCount + ' endpoints synced)' : ''), 'success');
// Reload the embedding pool section
await renderEmbeddingPoolMainPanel();
} catch (err) {
console.error('Failed to save embedding pool config:', err);
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
}
}
/**
* Toggle provider exclusion in embedding pool
*/
async function toggleProviderExclusion(providerId) {
if (!embeddingPoolConfig) return;
const excludedIds = embeddingPoolConfig.excludedProviderIds || [];
const index = excludedIds.indexOf(providerId);
if (index > -1) {
excludedIds.splice(index, 1);
} else {
excludedIds.push(providerId);
}
embeddingPoolConfig.excludedProviderIds = excludedIds;
// Re-render the discovered providers section
renderDiscoveredProviders();
}
// ========== Provider Management ==========
/**
@@ -825,6 +936,9 @@ async function renderApiSettings() {
'<button class="sidebar-tab' + (activeSidebarTab === 'endpoints' ? ' active' : '') + '" onclick="switchSidebarTab(\'endpoints\')">' +
'<i data-lucide="link"></i> ' + t('apiSettings.endpoints') +
'</button>' +
'<button class="sidebar-tab' + (activeSidebarTab === 'embedding-pool' ? ' active' : '') + '" onclick="switchSidebarTab(\'embedding-pool\')">' +
'<i data-lucide="repeat"></i> ' + t('apiSettings.embeddingPool') +
'</button>' +
'<button class="sidebar-tab' + (activeSidebarTab === 'cache' ? ' active' : '') + '" onclick="switchSidebarTab(\'cache\')">' +
'<i data-lucide="database"></i> ' + t('apiSettings.cache') +
'</button>' +
@@ -833,7 +947,7 @@ async function renderApiSettings() {
// Build sidebar content based on active tab
var sidebarContentHtml = '';
var addButtonHtml = '';
if (activeSidebarTab === 'providers') {
sidebarContentHtml = '<div class="provider-search">' +
'<i data-lucide="search" class="search-icon"></i>' +
@@ -848,6 +962,10 @@ async function renderApiSettings() {
addButtonHtml = '<button class="btn btn-primary btn-full" onclick="showAddEndpointModal()">' +
'<i data-lucide="plus"></i> ' + t('apiSettings.addEndpoint') +
'</button>';
} else if (activeSidebarTab === 'embedding-pool') {
sidebarContentHtml = '<div class="embedding-pool-sidebar-info" style="padding: 1rem; color: var(--text-secondary); font-size: 0.875rem;">' +
'<p>' + t('apiSettings.embeddingPoolDesc') + '</p>' +
'</div>';
} else if (activeSidebarTab === 'cache') {
sidebarContentHtml = '<div class="cache-sidebar-info" style="padding: 1rem; color: var(--text-secondary); font-size: 0.875rem;">' +
'<p>' + t('apiSettings.cacheTabHint') + '</p>' +
@@ -887,6 +1005,8 @@ async function renderApiSettings() {
} else if (activeSidebarTab === 'endpoints') {
renderEndpointsList();
renderEndpointsMainPanel();
} else if (activeSidebarTab === 'embedding-pool') {
renderEmbeddingPoolMainPanel();
} else if (activeSidebarTab === 'cache') {
renderCacheMainPanel();
}
@@ -2367,6 +2487,174 @@ function generateKeyId() {
return 'key-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
}
// ========== Embedding Pool Management ==========
/**
* Render embedding pool main panel
*/
async function renderEmbeddingPoolMainPanel() {
var container = document.getElementById('provider-detail-panel');
if (!container) return;
// Load embedding pool config if not already loaded
if (!embeddingPoolConfig) {
await loadEmbeddingPoolConfig();
}
const enabled = embeddingPoolConfig?.enabled || false;
const targetModel = embeddingPoolConfig?.targetModel || '';
const strategy = embeddingPoolConfig?.strategy || 'round_robin';
const defaultCooldown = embeddingPoolConfig?.defaultCooldown || 60;
const defaultMaxConcurrentPerKey = embeddingPoolConfig?.defaultMaxConcurrentPerKey || 4;
// Build model dropdown options
let modelOptionsHtml = '<option value="">' + t('apiSettings.selectTargetModel') + '</option>';
embeddingPoolAvailableModels.forEach(function(model) {
const providerCount = model.providers.length;
const selected = model.modelId === targetModel ? ' selected' : '';
modelOptionsHtml += '<option value="' + model.modelId + '"' + selected + '>' +
model.modelName + ' (' + providerCount + ' providers)' +
'</option>';
});
var html = '<div class="embedding-pool-main-panel">' +
'<div class="panel-header">' +
'<h2><i data-lucide="repeat"></i> ' + t('apiSettings.embeddingPool') + '</h2>' +
'<p class="panel-subtitle">' + t('apiSettings.embeddingPoolDesc') + '</p>' +
'</div>' +
// Enable/Disable Toggle
'<div class="settings-section">' +
'<div class="section-header">' +
'<h3>' + t('apiSettings.poolEnabled') + '</h3>' +
'<label class="toggle-switch">' +
'<input type="checkbox" id="embedding-pool-enabled" ' + (enabled ? 'checked' : '') + ' onchange="onEmbeddingPoolEnabledChange(this.checked)" />' +
'<span class="toggle-track"><span class="toggle-thumb"></span></span>' +
'</label>' +
'</div>' +
'</div>' +
// Configuration Form
'<div class="settings-section" id="embedding-pool-config" style="' + (enabled ? '' : 'display: none;') + '">' +
'<div class="form-group">' +
'<label for="embedding-pool-target-model">' + t('apiSettings.targetModel') + '</label>' +
'<select id="embedding-pool-target-model" class="cli-input" onchange="onTargetModelChange(this.value)">' +
modelOptionsHtml +
'</select>' +
'</div>' +
'<div class="form-group">' +
'<label for="embedding-pool-strategy">' + t('apiSettings.strategy') + '</label>' +
'<select id="embedding-pool-strategy" class="cli-input">' +
'<option value="round_robin"' + (strategy === 'round_robin' ? ' selected' : '') + '>Round Robin</option>' +
'<option value="latency_aware"' + (strategy === 'latency_aware' ? ' selected' : '') + '>Latency Aware</option>' +
'<option value="weighted_random"' + (strategy === 'weighted_random' ? ' selected' : '') + '>Weighted Random</option>' +
'</select>' +
'</div>' +
'<div class="form-group">' +
'<label for="embedding-pool-cooldown">' + t('apiSettings.defaultCooldown') + '</label>' +
'<input type="number" id="embedding-pool-cooldown" class="cli-input" value="' + defaultCooldown + '" min="1" />' +
'</div>' +
'<div class="form-group">' +
'<label for="embedding-pool-concurrent">' + t('apiSettings.defaultConcurrent') + '</label>' +
'<input type="number" id="embedding-pool-concurrent" class="cli-input" value="' + defaultMaxConcurrentPerKey + '" min="1" />' +
'</div>' +
// Discovered Providers Section
'<div id="discovered-providers-section"></div>' +
'<div class="form-actions">' +
'<button class="btn btn-primary" onclick="saveEmbeddingPoolConfig()">' +
'<i data-lucide="save"></i> ' + t('common.save') +
'</button>' +
'</div>' +
'</div>' +
'</div>';
container.innerHTML = html;
if (window.lucide) lucide.createIcons();
// Render discovered providers if we have a target model
if (enabled && targetModel) {
renderDiscoveredProviders();
}
}
/**
* Handle embedding pool enabled/disabled toggle
*/
function onEmbeddingPoolEnabledChange(enabled) {
const configSection = document.getElementById('embedding-pool-config');
if (configSection) {
configSection.style.display = enabled ? '' : 'none';
}
}
/**
* Handle target model selection change
*/
async function onTargetModelChange(modelId) {
if (!modelId) {
embeddingPoolDiscoveredProviders = [];
renderDiscoveredProviders();
return;
}
// Discover providers for this model
await discoverProvidersForTargetModel(modelId);
renderDiscoveredProviders();
}
/**
* Render discovered providers list
*/
function renderDiscoveredProviders() {
const container = document.getElementById('discovered-providers-section');
if (!container) return;
if (embeddingPoolDiscoveredProviders.length === 0) {
container.innerHTML = '<div class="info-message" style="margin-top: 1rem;">' +
'<i data-lucide="info"></i> ' + t('apiSettings.noProvidersFound') +
'</div>';
if (window.lucide) lucide.createIcons();
return;
}
const excludedIds = embeddingPoolConfig?.excludedProviderIds || [];
let totalProviders = 0;
let totalKeys = 0;
embeddingPoolDiscoveredProviders.forEach(function(p) {
totalProviders++;
totalKeys += p.keyCount || 1;
});
let providersHtml = '<div class="discovered-providers-box" style="margin-top: 1rem; padding: 1rem; background: var(--bg-secondary); border-radius: 8px;">' +
'<h4>' + t('apiSettings.discoveredProviders') + ' (' + totalProviders + ' providers, ' + totalKeys + ' ' + t('apiSettings.providerKeys') + ')</h4>' +
'<div class="providers-list" style="margin-top: 0.75rem;">';
embeddingPoolDiscoveredProviders.forEach(function(provider) {
const isExcluded = excludedIds.indexOf(provider.providerId) > -1;
const icon = isExcluded ? 'x-circle' : 'check-circle';
const statusClass = isExcluded ? 'text-error' : 'text-success';
const keyInfo = provider.keyCount > 1 ? ' (' + provider.keyCount + ' ' + t('apiSettings.providerKeys') + ')' : '';
providersHtml += '<div class="provider-item" style="display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem; border-bottom: 1px solid var(--border-color);">' +
'<i data-lucide="' + icon + '" class="' + statusClass + '"></i>' +
'<span style="flex: 1;">' + provider.providerName + keyInfo + '</span>' +
'<button class="btn btn-sm ' + (isExcluded ? 'btn-secondary' : 'btn-outline') + '" onclick="toggleProviderExclusion(\'' + provider.providerId + '\')">' +
(isExcluded ? t('common.include') : t('apiSettings.excludeProvider')) +
'</button>' +
'</div>';
});
providersHtml += '</div></div>';
container.innerHTML = providersHtml;
if (window.lucide) lucide.createIcons();
}
/**
* Render API keys section
*/

View File

@@ -271,6 +271,9 @@ function initCodexLensConfigEvents(currentConfig) {
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');
@@ -286,7 +289,12 @@ function initCodexLensConfigEvents(currentConfig) {
try {
var endpoint = '/api/codexlens/' + searchType;
var params = new URLSearchParams({ query: query, limit: '20' });
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);
@@ -2001,7 +2009,7 @@ function buildCodexLensManagerPage(config) {
'<div id="concurrencySelector" class="hidden">' +
'<label class="block text-sm font-medium mb-1.5">' + t('codexlens.concurrency') + '</label>' +
'<div class="flex items-center gap-2">' +
'<input type="number" id="pageConcurrencyInput" min="1" max="32" value="4" ' +
'<input type="number" id="pageConcurrencyInput" min="1" value="4" ' +
'class="w-24 px-3 py-2 border border-border rounded-lg bg-background text-sm" ' +
'onchange="validateConcurrencyInput(this)" />' +
'<span class="text-sm text-muted-foreground">workers</span>' +
@@ -2173,6 +2181,20 @@ function buildCodexLensManagerPage(config) {
'<option value="vector">' + t('codexlens.vectorMode') + '</option>' +
'</select>' +
'</div>' +
'<div class="flex gap-3 items-center">' +
'<div class="flex items-center gap-2">' +
'<label class="text-xs text-muted-foreground whitespace-nowrap">' + t('codexlens.resultLimit') + '</label>' +
'<input type="number" id="searchLimitInput" class="w-16 px-2 py-1.5 border border-border rounded-lg bg-background text-sm text-center" value="5" min="1" max="50" />' +
'</div>' +
'<div class="flex items-center gap-2">' +
'<label class="text-xs text-muted-foreground whitespace-nowrap">' + t('codexlens.contentLength') + '</label>' +
'<input type="number" id="contentLengthInput" class="w-20 px-2 py-1.5 border border-border rounded-lg bg-background text-sm text-center" value="200" min="50" max="2000" />' +
'</div>' +
'<div class="flex items-center gap-2">' +
'<label class="text-xs text-muted-foreground whitespace-nowrap">' + t('codexlens.extraFiles') + '</label>' +
'<input type="number" id="extraFilesInput" class="w-16 px-2 py-1.5 border border-border rounded-lg bg-background text-sm text-center" value="10" min="0" max="50" />' +
'</div>' +
'</div>' +
'<div class="flex gap-3">' +
'<input type="text" id="searchQueryInput" class="flex-1 px-3 py-2 border border-border rounded-lg bg-background text-sm" placeholder="' + t('codexlens.searchPlaceholder') + '" />' +
'<button class="btn-sm btn-primary" id="runSearchBtn"><i data-lucide="search" class="w-3.5 h-3.5"></i> ' + t('codexlens.runSearch') + '</button>' +
@@ -2228,14 +2250,12 @@ function buildModelSelectOptionsForPage() {
}
/**
* Validate concurrency input value (1-32)
* Validate concurrency input value (min 1, no max limit)
*/
function validateConcurrencyInput(input) {
var value = parseInt(input.value, 10);
if (isNaN(value) || value < 1) {
input.value = 1;
} else if (value > 32) {
input.value = 32;
}
}
@@ -2338,7 +2358,7 @@ function initCodexLensIndexFromPage(indexType) {
var concurrencyInput = document.getElementById('pageConcurrencyInput');
var selectedBackend = backendSelect ? backendSelect.value : 'fastembed';
var selectedModel = modelSelect ? modelSelect.value : 'code';
var selectedConcurrency = concurrencyInput ? Math.min(32, Math.max(1, parseInt(concurrencyInput.value, 10) || 4)) : 4;
var selectedConcurrency = concurrencyInput ? Math.max(1, parseInt(concurrencyInput.value, 10) || 4) : 4;
// For FTS-only index, model is not needed
if (indexType === 'normal') {
@@ -2879,7 +2899,16 @@ async function saveRotationConfig() {
var result = await response.json();
if (result.success) {
showRefreshToast(t('codexlens.rotationSaved'), 'success');
// Show sync result in toast
var syncMsg = '';
if (result.syncResult) {
if (result.syncResult.success) {
syncMsg = ' (' + result.syncResult.endpointCount + ' ' + t('codexlens.endpointsSynced') + ')';
} else {
syncMsg = ' (' + t('codexlens.syncFailed') + ': ' + result.syncResult.message + ')';
}
}
showRefreshToast(t('codexlens.rotationSaved') + syncMsg, 'success');
window.rotationConfig = rotationConfig;
updateRotationStatusDisplay(rotationConfig);
closeRotationModal();