997 lines
46 KiB
JavaScript
997 lines
46 KiB
JavaScript
/**
|
||
* UI 嵌入与重排配置渲染模块
|
||
* 提取嵌入模型(Embedding)和重排(Rerank)的配置界面渲染代码
|
||
*/
|
||
|
||
(function(window) {
|
||
'use strict';
|
||
|
||
// 确保 EmbeddingClient 已加载(必要时动态注入脚本)
|
||
async function ensureEmbeddingClientLoaded() {
|
||
if (window.EmbeddingClient && typeof window.EmbeddingClient.saveConfig === 'function') return true;
|
||
|
||
// 从已加载脚本推断候选路径
|
||
const candidates = [];
|
||
try {
|
||
const scripts = Array.from(document.getElementsByTagName('script'));
|
||
const sem = scripts.find(s => (s.src || '').includes('semantic-vector-search.js'));
|
||
if (sem && sem.src) candidates.push(sem.src.replace('semantic-vector-search.js', 'embedding-client.js'));
|
||
const rer = scripts.find(s => (s.src || '').includes('rerank-client.js'));
|
||
if (rer && rer.src) candidates.push(rer.src.replace('rerank-client.js', 'embedding-client.js'));
|
||
} catch(_) {}
|
||
|
||
// 兜底:相对当前页面常见路径
|
||
candidates.push('js/chatbot/agents/embedding-client.js');
|
||
|
||
// 动态加载(无论是否已存在旧标签,均追加一个带缓存破坏参数的标签)
|
||
for (const base of Array.from(new Set(candidates))) {
|
||
const url = base + (base.includes('?') ? '&' : '?') + 'v=' + Date.now();
|
||
try {
|
||
await new Promise((resolve, reject) => {
|
||
const s = document.createElement('script');
|
||
s.src = url;
|
||
s.async = true;
|
||
s.onload = () => resolve();
|
||
s.onerror = () => reject(new Error('load failed: ' + url));
|
||
document.head.appendChild(s);
|
||
});
|
||
if (window.EmbeddingClient && typeof window.EmbeddingClient.saveConfig === 'function') return true;
|
||
} catch(_) {
|
||
// try next
|
||
}
|
||
}
|
||
return !!(window.EmbeddingClient && typeof window.EmbeddingClient.saveConfig === 'function');
|
||
}
|
||
|
||
/**
|
||
* 显示嵌入模型选择器对话框
|
||
* @param {Array} models - 模型列表
|
||
* @param {HTMLInputElement} targetInput - 目标输入框
|
||
*/
|
||
function showEmbeddingModelSelector(models, targetInput) {
|
||
// 创建一个简单的选择对话框
|
||
const container = document.createElement('div');
|
||
container.style.cssText = `
|
||
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||
width: 400px; max-width: 90vw; max-height: 60vh;
|
||
background: #fff; border-radius: 12px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||
z-index: 100002; padding: 0; overflow: hidden;
|
||
`;
|
||
|
||
const header = document.createElement('div');
|
||
header.style.cssText = 'padding: 16px 20px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center;';
|
||
header.innerHTML = `
|
||
<h4 style="margin: 0; font-size: 16px; font-weight: 600; color: #111827;">选择嵌入模型</h4>
|
||
<button class="model-selector-close" style="border: none; background: none; font-size: 24px; color: #6b7280; cursor: pointer; line-height: 1;">×</button>
|
||
`;
|
||
|
||
const list = document.createElement('div');
|
||
list.style.cssText = 'max-height: 400px; overflow-y: auto; padding: 8px;';
|
||
|
||
models.forEach(model => {
|
||
const item = document.createElement('div');
|
||
item.style.cssText = `
|
||
padding: 12px 16px; margin: 4px 0; border-radius: 8px;
|
||
cursor: pointer; transition: all 0.2s;
|
||
border: 1px solid #e5e7eb;
|
||
`;
|
||
item.innerHTML = `
|
||
<div style="font-weight: 500; color: #111827;">${model.id}</div>
|
||
${model.owned_by ? `<div style="font-size: 12px; color: #6b7280; margin-top: 2px;">by ${model.owned_by}</div>` : ''}
|
||
`;
|
||
|
||
item.onmouseover = () => {
|
||
item.style.background = '#f3f4f6';
|
||
item.style.borderColor = '#3b82f6';
|
||
};
|
||
item.onmouseout = () => {
|
||
item.style.background = '#fff';
|
||
item.style.borderColor = '#e5e7eb';
|
||
};
|
||
item.onclick = () => {
|
||
targetInput.value = model.id;
|
||
document.body.removeChild(overlay);
|
||
document.body.removeChild(container);
|
||
};
|
||
|
||
list.appendChild(item);
|
||
});
|
||
|
||
container.appendChild(header);
|
||
container.appendChild(list);
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.style.cssText = `
|
||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5); z-index: 100001;
|
||
`;
|
||
|
||
const closeHandler = () => {
|
||
document.body.removeChild(overlay);
|
||
document.body.removeChild(container);
|
||
};
|
||
|
||
overlay.onclick = closeHandler;
|
||
header.querySelector('.model-selector-close').onclick = closeHandler;
|
||
|
||
document.body.appendChild(overlay);
|
||
document.body.appendChild(container);
|
||
}
|
||
|
||
/**
|
||
* 显示重排模型选择器对话框
|
||
* @param {Array} models - 模型列表
|
||
* @param {HTMLInputElement} targetInput - 目标输入框
|
||
*/
|
||
function showRerankModelSelector(models, targetInput) {
|
||
const container = document.createElement('div');
|
||
container.style.cssText = `
|
||
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||
width: 420px; max-width: 92vw; max-height: 60vh;
|
||
background: #fff; border-radius: 12px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||
z-index: 100002; padding: 0; overflow: hidden;
|
||
`;
|
||
|
||
const header = document.createElement('div');
|
||
header.style.cssText = 'padding: 16px 20px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center;';
|
||
header.innerHTML = `
|
||
<h4 style="margin: 0; font-size: 16px; font-weight: 600; color: #111827;">选择重排模型</h4>
|
||
<button class="model-selector-close" style="border: none; background: none; font-size: 24px; color: #6b7280; cursor: pointer; line-height: 1;">×</button>
|
||
`;
|
||
|
||
const list = document.createElement('div');
|
||
list.style.cssText = 'max-height: 400px; overflow-y: auto; padding: 8px;';
|
||
|
||
(models || []).forEach(model => {
|
||
const item = document.createElement('div');
|
||
item.style.cssText = `
|
||
padding: 12px 16px; margin: 4px 0; border-radius: 8px;
|
||
cursor: pointer; transition: all 0.2s;
|
||
border: 1px solid #e5e7eb;
|
||
`;
|
||
const id = model.id || model.name || '';
|
||
item.innerHTML = `
|
||
<div style="font-weight: 500; color: #111827;">${id}</div>
|
||
${model.owned_by ? `<div style="font-size: 12px; color: #6b7280; margin-top: 2px;">by ${model.owned_by}</div>` : ''}
|
||
`;
|
||
item.onmouseover = () => { item.style.background = '#f3f4f6'; item.style.borderColor = '#737373'; };
|
||
item.onmouseout = () => { item.style.background = '#fff'; item.style.borderColor = '#e5e7eb'; };
|
||
item.onclick = () => {
|
||
if (id) targetInput.value = id;
|
||
document.body.removeChild(overlay);
|
||
document.body.removeChild(container);
|
||
};
|
||
list.appendChild(item);
|
||
});
|
||
|
||
container.appendChild(header);
|
||
container.appendChild(list);
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.style.cssText = `
|
||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5); z-index: 100001;
|
||
`;
|
||
const closeHandler = () => { document.body.removeChild(overlay); document.body.removeChild(container); };
|
||
overlay.onclick = closeHandler;
|
||
header.querySelector('.model-selector-close').onclick = closeHandler;
|
||
document.body.appendChild(overlay);
|
||
document.body.appendChild(container);
|
||
}
|
||
|
||
/**
|
||
* 渲染嵌入模型配置界面(包含向量搜索和重排两个tab)
|
||
* @param {HTMLElement} container - 配置容器元素(modelConfigColumn)
|
||
*/
|
||
function renderEmbeddingConfig(container) {
|
||
// 从localStorage加载配置
|
||
const config = window.EmbeddingClient?.config || {};
|
||
const rerankConfig = window.RerankClient?.config || {};
|
||
const PRESETS = {
|
||
openai: { name: 'OpenAI格式', endpoint: 'https://api.openai.com/v1/embeddings' },
|
||
jina: { name: 'Jina AI', endpoint: 'https://api.jina.ai/v1/embeddings' },
|
||
zhipu: { name: '智谱AI', endpoint: 'https://open.bigmodel.cn/api/paas/v4/embeddings' },
|
||
alibaba: { name: '阿里云百炼', endpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings' }
|
||
};
|
||
|
||
// 阿里云百炼支持的模型和维度
|
||
const ALIBABA_MODELS = {
|
||
'text-embedding-v1': { name: 'text-embedding-v1 (中文)', dims: 1536 },
|
||
'text-embedding-v2': { name: 'text-embedding-v2 (多语言)', dims: 1536 },
|
||
'text-embedding-v3': { name: 'text-embedding-v3 (高性能)', dims: 1024 },
|
||
'text-embedding-v4': { name: 'text-embedding-v4 (多语言,支持2048维)', dims: 2048 }
|
||
};
|
||
|
||
const mainContainer = document.createElement('div');
|
||
|
||
// Tabs(样式更内敛)
|
||
const tabsDiv = document.createElement('div');
|
||
tabsDiv.className = 'flex border-b border-gray-200 mb-4';
|
||
tabsDiv.innerHTML = `
|
||
<button id="emb-km-tab-vector" class="emb-km-tab flex-1 px-4 py-2 text-sm font-medium text-gray-800 border-b-2 border-gray-300 transition-colors">
|
||
向量搜索
|
||
</button>
|
||
<button id="emb-km-tab-rerank" class="emb-km-tab flex-1 px-4 py-2 text-sm font-medium text-gray-500 border-b-2 border-transparent hover:text-gray-700 transition-colors">
|
||
重排 (Rerank)
|
||
</button>
|
||
`;
|
||
mainContainer.appendChild(tabsDiv);
|
||
|
||
// 向量搜索Tab内容
|
||
const vectorContainer = document.createElement('div');
|
||
vectorContainer.id = 'emb-km-vector-content';
|
||
vectorContainer.className = 'emb-km-tab-content space-y-4';
|
||
|
||
// 启用开关
|
||
const enabledDiv = document.createElement('div');
|
||
enabledDiv.className = 'flex items-center gap-2';
|
||
enabledDiv.innerHTML = `
|
||
<input type="checkbox" id="emb-enabled-km" ${config.enabled ? 'checked' : ''} class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
||
<label for="emb-enabled-km" class="text-sm font-medium text-gray-700">启用向量搜索</label>
|
||
`;
|
||
vectorContainer.appendChild(enabledDiv);
|
||
|
||
// 服务商选择
|
||
const providerDiv = document.createElement('div');
|
||
providerDiv.innerHTML = `
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">服务商</label>
|
||
<select id="emb-provider-km" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
<option value="openai" ${config.provider === 'openai' ? 'selected' : ''}>OpenAI格式</option>
|
||
<option value="jina" ${config.provider === 'jina' ? 'selected' : ''}>Jina AI (多语言优化)</option>
|
||
<option value="zhipu" ${config.provider === 'zhipu' ? 'selected' : ''}>智谱AI (GLM)</option>
|
||
<option value="alibaba" ${config.provider === 'alibaba' ? 'selected' : ''}>阿里云百炼</option>
|
||
</select>
|
||
`;
|
||
vectorContainer.appendChild(providerDiv);
|
||
|
||
// API Key(带显示/隐藏按钮)
|
||
const keyDiv = document.createElement('div');
|
||
keyDiv.innerHTML = `
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">API Key</label>
|
||
<div class="flex items-center gap-2">
|
||
<input type="password" id="emb-api-key-km" value="${config.apiKey || ''}" placeholder="sk-..." class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
<button type="button" id="emb-api-key-toggle-km" class="px-2.5 py-2 border border-gray-300 rounded-md text-xs text-gray-700 hover:bg-gray-50 flex items-center gap-1">
|
||
<iconify-icon icon="carbon:view" width="16"></iconify-icon>显示
|
||
</button>
|
||
</div>
|
||
`;
|
||
vectorContainer.appendChild(keyDiv);
|
||
|
||
// Base URL
|
||
const urlDiv = document.createElement('div');
|
||
// 显示时去掉 /embeddings 后缀
|
||
const displayUrl = (config.endpoint || '').replace(/\/embeddings\/?$/, '');
|
||
urlDiv.innerHTML = `
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||
Base URL
|
||
<span class="text-xs text-gray-500">(如 https://api.openai.com/v1)</span>
|
||
</label>
|
||
<input type="text" id="emb-endpoint-km" value="${displayUrl}" placeholder="https://api.openai.com/v1" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
`;
|
||
vectorContainer.appendChild(urlDiv);
|
||
|
||
// 模型选择
|
||
const modelDiv = document.createElement('div');
|
||
modelDiv.innerHTML = `
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">模型ID</label>
|
||
<div class="flex gap-2">
|
||
<input type="text" id="emb-model-km" value="${config.model || ''}" placeholder="请输入模型ID,如: text-embedding-3-small" class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
<button type="button" id="emb-fetch-models-km" class="px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md transition-colors whitespace-nowrap" style="display: none;">
|
||
获取列表
|
||
</button>
|
||
</div>
|
||
<p id="emb-model-hint-km" class="mt-1 text-xs text-gray-500">请输入服务商支持的嵌入模型ID</p>
|
||
`;
|
||
vectorContainer.appendChild(modelDiv);
|
||
|
||
// 向量维度 (OpenAI可选)
|
||
const dimsDiv = document.createElement('div');
|
||
dimsDiv.id = 'emb-dims-wrap-km';
|
||
dimsDiv.innerHTML = `
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||
向量维度
|
||
<span class="text-xs text-gray-500">(可选,留空使用默认)</span>
|
||
</label>
|
||
<input type="number" id="emb-dimensions-km" value="${config.dimensions || ''}" placeholder="1536" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
<p class="mt-1 text-xs text-gray-500">降低维度可减少存储和计算,但可能影响精度</p>
|
||
`;
|
||
vectorContainer.appendChild(dimsDiv);
|
||
|
||
// 并发数配置
|
||
const concurrencyDiv = document.createElement('div');
|
||
concurrencyDiv.innerHTML = `
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||
并发请求数
|
||
<span class="text-xs text-gray-500">(建议 5-20,最大50)</span>
|
||
</label>
|
||
<input type="number" id="emb-concurrency-km" value="${config.concurrency || 5}" min="1" max="50" placeholder="5" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
<p class="mt-1 text-xs text-gray-500">提高并发数可加快索引构建速度,但注意API速率限制</p>
|
||
`;
|
||
vectorContainer.appendChild(concurrencyDiv);
|
||
|
||
// 测试和保存按钮
|
||
const buttonsDiv = document.createElement('div');
|
||
buttonsDiv.className = 'flex gap-3 pt-2';
|
||
buttonsDiv.innerHTML = `
|
||
<button id="emb-test-km" class="flex-1 px-4 py-2 border border-gray-300 bg-white text-gray-700 rounded-md hover:bg-gray-50">测试连接</button>
|
||
<button id="emb-save-km" class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">保存配置</button>
|
||
`;
|
||
vectorContainer.appendChild(buttonsDiv);
|
||
|
||
// 测试结果
|
||
const resultDiv = document.createElement('div');
|
||
resultDiv.id = 'emb-test-result-km';
|
||
resultDiv.className = 'text-sm mt-2';
|
||
resultDiv.style.display = 'none';
|
||
vectorContainer.appendChild(resultDiv);
|
||
|
||
mainContainer.appendChild(vectorContainer);
|
||
|
||
// 重排Tab内容
|
||
const rerankContainer = document.createElement('div');
|
||
rerankContainer.id = 'emb-km-rerank-content';
|
||
rerankContainer.className = 'emb-km-tab-content space-y-4 hidden';
|
||
|
||
// 重排启用开关
|
||
const rerankEnabledDiv = document.createElement('div');
|
||
rerankEnabledDiv.className = 'flex items-center gap-2';
|
||
rerankEnabledDiv.innerHTML = `
|
||
<input type="checkbox" id="rerank-enabled-km" ${rerankConfig.enabled ? 'checked' : ''} class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
||
<label for="rerank-enabled-km" class="text-sm font-medium text-gray-700">启用重排</label>
|
||
`;
|
||
rerankContainer.appendChild(rerankEnabledDiv);
|
||
|
||
// 应用范围
|
||
const rerankScopeDiv = document.createElement('div');
|
||
const scope = rerankConfig.scope || 'vector-only';
|
||
rerankScopeDiv.innerHTML = `
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">应用范围</label>
|
||
<div class="space-y-2">
|
||
<label class="flex items-center cursor-pointer">
|
||
<input type="radio" name="rerank-scope-km" value="vector-only" ${scope === 'vector-only' ? 'checked' : ''} class="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500">
|
||
<span class="ml-2 text-sm text-gray-700">仅向量搜索使用重排</span>
|
||
</label>
|
||
<label class="flex items-center cursor-pointer">
|
||
<input type="radio" name="rerank-scope-km" value="all" ${scope === 'all' ? 'checked' : ''} class="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500">
|
||
<span class="ml-2 text-sm text-gray-700">所有搜索都使用重排(包括BM25等)</span>
|
||
</label>
|
||
</div>
|
||
<p class="mt-1 text-xs text-gray-500">选择重排功能的应用范围,失败时自动降级为原始排序</p>
|
||
`;
|
||
rerankContainer.appendChild(rerankScopeDiv);
|
||
|
||
// 服务商选择
|
||
const rerankProviderDiv = document.createElement('div');
|
||
rerankProviderDiv.innerHTML = `
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">服务商</label>
|
||
<select id="rerank-provider-km" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
<option value="jina" ${rerankConfig.provider === 'jina' ? 'selected' : ''}>Jina AI Reranker</option>
|
||
<option value="cohere" ${rerankConfig.provider === 'cohere' ? 'selected' : ''}>Cohere Rerank</option>
|
||
<option value="openai" ${rerankConfig.provider === 'openai' ? 'selected' : ''}>OpenAI格式</option>
|
||
</select>
|
||
`;
|
||
rerankContainer.appendChild(rerankProviderDiv);
|
||
|
||
// API Key(带显示/隐藏按钮)
|
||
const rerankKeyDiv = document.createElement('div');
|
||
rerankKeyDiv.innerHTML = `
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">API Key</label>
|
||
<div class="flex items-center gap-2">
|
||
<input type="password" id="rerank-api-key-km" value="${rerankConfig.apiKey || ''}" placeholder="jina_..." class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
<button type="button" id="rerank-api-key-toggle-km" class="px-2.5 py-2 border border-gray-300 rounded-md text-xs text-gray-700 hover:bg-gray-50 flex items-center gap-1">
|
||
<iconify-icon icon="carbon:view" width="16"></iconify-icon>显示
|
||
</button>
|
||
</div>
|
||
`;
|
||
rerankContainer.appendChild(rerankKeyDiv);
|
||
|
||
// Base URL(显示时去掉 /rerank 后缀)
|
||
const rerankUrlDiv = document.createElement('div');
|
||
const displayRerankBaseUrl = (rerankConfig.endpoint || '').replace(/\/rerank\/?$/, '');
|
||
rerankUrlDiv.innerHTML = `
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||
Base URL
|
||
<span class="text-xs text-gray-500">(如 https://api.jina.ai/v1 或 https://api.openai.com/v1)</span>
|
||
</label>
|
||
<input type="text" id="rerank-endpoint-km" value="${displayRerankBaseUrl}" placeholder="https://api.jina.ai/v1" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
`;
|
||
rerankContainer.appendChild(rerankUrlDiv);
|
||
|
||
// 模型ID(支持 OpenAI 格式获取列表与模型检测)
|
||
const rerankModelDiv = document.createElement('div');
|
||
rerankModelDiv.innerHTML = `
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">模型ID</label>
|
||
<div class="flex gap-2">
|
||
<input type="text" id="rerank-model-km" value="${rerankConfig.model || 'jina-reranker-v2-base-multilingual'}" placeholder="例如: jina-reranker-v2-base-multilingual 或 cohere/rerank-multilingual-v3.0" class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
<button type="button" id="rerank-fetch-models-km" class="px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md transition-colors whitespace-nowrap" style="display: none;">获取列表</button>
|
||
<button type="button" id="rerank-check-model-km" class="px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md transition-colors whitespace-nowrap">检测模型</button>
|
||
</div>
|
||
<p id="rerank-model-hint-km" class="mt-1 text-xs text-gray-500">请输入服务商支持的重排模型ID;OpenAI格式可点击“获取列表”</p>
|
||
`;
|
||
rerankContainer.appendChild(rerankModelDiv);
|
||
|
||
// Top N
|
||
const rerankTopNDiv = document.createElement('div');
|
||
rerankTopNDiv.innerHTML = `
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||
Top N
|
||
<span class="text-xs text-gray-500">(返回前N个结果)</span>
|
||
</label>
|
||
<input type="number" id="rerank-top-n-km" value="${rerankConfig.topN || 10}" min="1" max="50" placeholder="10" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
<p class="mt-1 text-xs text-gray-500">建议 5-20,根据实际需求调整</p>
|
||
`;
|
||
rerankContainer.appendChild(rerankTopNDiv);
|
||
|
||
// 重排测试和保存按钮
|
||
const rerankButtonsDiv = document.createElement('div');
|
||
rerankButtonsDiv.className = 'flex gap-3 pt-2';
|
||
rerankButtonsDiv.innerHTML = `
|
||
<button id="rerank-test-km" class="flex-1 px-4 py-2 border border-gray-300 bg-white text-gray-700 rounded-md hover:bg-gray-50">测试连接</button>
|
||
<button id="rerank-save-km" class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">保存配置</button>
|
||
`;
|
||
rerankContainer.appendChild(rerankButtonsDiv);
|
||
|
||
// 重排测试结果
|
||
const rerankResultDiv = document.createElement('div');
|
||
rerankResultDiv.id = 'rerank-test-result-km';
|
||
rerankResultDiv.className = 'text-sm mt-2';
|
||
rerankResultDiv.style.display = 'none';
|
||
rerankContainer.appendChild(rerankResultDiv);
|
||
|
||
// 说明
|
||
const rerankNoticeDiv = document.createElement('div');
|
||
rerankNoticeDiv.className = 'mt-4 p-3 bg-blue-50 border border-blue-200 rounded-md';
|
||
rerankNoticeDiv.innerHTML = `
|
||
<p class="text-xs text-blue-900">💡 <strong>重排工作原理</strong>:对搜索结果进行二次排序,使用更精确的模型计算相关性分数,提升最终结果的准确度。</p>
|
||
`;
|
||
rerankContainer.appendChild(rerankNoticeDiv);
|
||
|
||
mainContainer.appendChild(rerankContainer);
|
||
|
||
container.appendChild(mainContainer);
|
||
|
||
// 事件绑定
|
||
const $= (id) => document.getElementById(id);
|
||
|
||
// API Key 显示/隐藏切换(Embedding)
|
||
(function() {
|
||
const toggleBtn = $('emb-api-key-toggle-km');
|
||
const input = $('emb-api-key-km');
|
||
if (toggleBtn && input) {
|
||
toggleBtn.addEventListener('click', () => {
|
||
const isPassword = input.type === 'password';
|
||
input.type = isPassword ? 'text' : 'password';
|
||
toggleBtn.innerHTML = isPassword
|
||
? '<iconify-icon icon="carbon:view-off" width="16"></iconify-icon>隐藏'
|
||
: '<iconify-icon icon="carbon:view" width="16"></iconify-icon>显示';
|
||
});
|
||
}
|
||
})();
|
||
|
||
// API Key 显示/隐藏切换(Rerank)
|
||
(function() {
|
||
const toggleBtn = $('rerank-api-key-toggle-km');
|
||
const input = $('rerank-api-key-km');
|
||
if (toggleBtn && input) {
|
||
toggleBtn.addEventListener('click', () => {
|
||
const isPassword = input.type === 'password';
|
||
input.type = isPassword ? 'text' : 'password';
|
||
toggleBtn.innerHTML = isPassword
|
||
? '<iconify-icon icon="carbon:view-off" width="16"></iconify-icon>隐藏'
|
||
: '<iconify-icon icon="carbon:view" width="16"></iconify-icon>显示';
|
||
});
|
||
}
|
||
})();
|
||
|
||
// Tabs切换事件(中性灰)
|
||
const kmTabs = document.querySelectorAll('.emb-km-tab');
|
||
const kmTabContents = document.querySelectorAll('.emb-km-tab-content');
|
||
kmTabs.forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
// 更新tab样式
|
||
kmTabs.forEach(t => {
|
||
t.classList.remove('text-gray-800', 'border-gray-300');
|
||
t.classList.add('text-gray-500', 'border-transparent');
|
||
});
|
||
tab.classList.remove('text-gray-500', 'border-transparent');
|
||
tab.classList.add('text-gray-800', 'border-gray-300');
|
||
|
||
// 切换内容
|
||
const targetId = tab.id.replace('-tab-', '-') + '-content';
|
||
kmTabContents.forEach(content => {
|
||
content.classList.add('hidden');
|
||
});
|
||
const targetContent = document.getElementById(targetId);
|
||
if (targetContent) {
|
||
targetContent.classList.remove('hidden');
|
||
}
|
||
});
|
||
});
|
||
|
||
// 服务商切换
|
||
$('emb-provider-km').onchange = function() {
|
||
const provider = this.value;
|
||
const fetchBtn = $('emb-fetch-models-km');
|
||
const modelHint = $('emb-model-hint-km');
|
||
|
||
// 显示/隐藏获取模型列表按钮(仅 OpenAI格式支持)
|
||
if (provider === 'openai') {
|
||
fetchBtn.style.display = 'block';
|
||
modelHint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取';
|
||
} else {
|
||
fetchBtn.style.display = 'none';
|
||
modelHint.textContent = '请输入服务商支持的嵌入模型ID';
|
||
}
|
||
|
||
// 当选择阿里云百炼时,更新维度提示
|
||
if (provider === 'alibaba') {
|
||
const dimsInput = $('emb-dimensions-km');
|
||
const dimsHint = dimsInput.nextElementSibling;
|
||
const modelInput = $('emb-model-km');
|
||
|
||
// 根据当前模型更新默认维度
|
||
const updateDimensionsForModel = () => {
|
||
const modelId = modelInput.value.trim();
|
||
const modelInfo = ALIBABA_MODELS[modelId];
|
||
if (modelInfo) {
|
||
dimsInput.placeholder = `默认: ${modelInfo.dims}`;
|
||
dimsHint.textContent = `默认维度: ${modelInfo.dims}。可输入1-${modelInfo.dims}之间的整数,留空使用默认。`;
|
||
}
|
||
};
|
||
|
||
// 初始化时更新一次
|
||
updateDimensionsForModel();
|
||
|
||
// 模型改变时更新
|
||
modelInput.addEventListener('change', updateDimensionsForModel);
|
||
}
|
||
};
|
||
|
||
// 初始化时更新 UI
|
||
(function() {
|
||
const provider = config.provider || 'openai';
|
||
const fetchBtn = $('emb-fetch-models-km');
|
||
const modelHint = $('emb-model-hint-km');
|
||
if (provider === 'openai') {
|
||
fetchBtn.style.display = 'block';
|
||
modelHint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取';
|
||
}
|
||
})();
|
||
|
||
// 获取模型列表(仅 OpenAI格式)
|
||
$('emb-fetch-models-km').onclick = async () => {
|
||
const btn = $('emb-fetch-models-km');
|
||
const modelInput = $('emb-model-km');
|
||
const modelHint = $('emb-model-hint-km');
|
||
const provider = $('emb-provider-km').value;
|
||
const apiKey = $('emb-api-key-km').value;
|
||
let endpoint = $('emb-endpoint-km').value;
|
||
|
||
if (!apiKey) {
|
||
modelHint.style.color = '#dc2626';
|
||
modelHint.textContent = '❌ 请先输入 API Key';
|
||
setTimeout(() => {
|
||
modelHint.style.color = '#6b7280';
|
||
modelHint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取';
|
||
}, 3000);
|
||
return;
|
||
}
|
||
|
||
if (!endpoint) {
|
||
endpoint = PRESETS[provider]?.endpoint || '';
|
||
}
|
||
|
||
// 自动补全路径
|
||
if (endpoint && !endpoint.endsWith('/embeddings')) {
|
||
endpoint = endpoint.replace(/\/+$/, '') + '/embeddings';
|
||
}
|
||
|
||
// 构建 models 端点
|
||
let modelsEndpoint = endpoint.replace('/embeddings', '/models');
|
||
|
||
btn.textContent = '获取中...';
|
||
btn.disabled = true;
|
||
modelHint.style.color = '#6b7280';
|
||
modelHint.textContent = '正在获取模型列表...';
|
||
|
||
try {
|
||
const response = await fetch(modelsEndpoint, {
|
||
headers: {
|
||
'Authorization': `Bearer ${apiKey}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
const models = data.data || [];
|
||
|
||
// 过滤出嵌入模型(支持多种命名模式)
|
||
const embeddingModels = models.filter(m => {
|
||
const id = (m.id || '').toLowerCase();
|
||
return id.includes('embedding') ||
|
||
id.includes('embed') ||
|
||
id.includes('bge') ||
|
||
id.includes('text-similarity') ||
|
||
id.includes('sentence') ||
|
||
id.includes('vector');
|
||
});
|
||
|
||
if (embeddingModels.length === 0) {
|
||
modelHint.style.color = '#f59e0b';
|
||
modelHint.textContent = `⚠️ 未找到嵌入模型(共 ${models.length} 个模型)`;
|
||
setTimeout(() => {
|
||
modelHint.style.color = '#6b7280';
|
||
modelHint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取';
|
||
}, 3000);
|
||
return;
|
||
}
|
||
|
||
// 显示模型选择器
|
||
showEmbeddingModelSelector(embeddingModels, modelInput);
|
||
modelHint.style.color = '#059669';
|
||
modelHint.textContent = `✅ 找到 ${embeddingModels.length} 个嵌入模型`;
|
||
setTimeout(() => {
|
||
modelHint.style.color = '#6b7280';
|
||
modelHint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取';
|
||
}, 3000);
|
||
|
||
} catch (error) {
|
||
modelHint.style.color = '#dc2626';
|
||
modelHint.textContent = `❌ 获取失败: ${error.message}`;
|
||
setTimeout(() => {
|
||
modelHint.style.color = '#6b7280';
|
||
modelHint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取';
|
||
}, 3000);
|
||
} finally {
|
||
btn.textContent = '获取列表';
|
||
btn.disabled = false;
|
||
}
|
||
};
|
||
|
||
// 测试连接
|
||
$('emb-test-km').onclick = async () => {
|
||
const btn = $('emb-test-km');
|
||
const result = $('emb-test-result-km');
|
||
|
||
let baseUrl = $('emb-endpoint-km').value.trim();
|
||
// 自动补全 /embeddings 路径
|
||
if (baseUrl && !baseUrl.endsWith('/embeddings')) {
|
||
baseUrl = baseUrl.replace(/\/+$/, '') + '/embeddings';
|
||
}
|
||
|
||
const testConfig = {
|
||
provider: $('emb-provider-km').value,
|
||
apiKey: $('emb-api-key-km').value,
|
||
endpoint: baseUrl,
|
||
model: $('emb-model-km').value,
|
||
dimensions: parseInt($('emb-dimensions-km').value) || null
|
||
};
|
||
|
||
if (!testConfig.apiKey || !testConfig.endpoint || !testConfig.model) {
|
||
result.style.display = 'block';
|
||
result.style.color = '#dc2626';
|
||
result.textContent = '❌ 请填写完整配置';
|
||
return;
|
||
}
|
||
|
||
btn.disabled = true;
|
||
btn.textContent = '测试中...';
|
||
result.style.display = 'none';
|
||
|
||
try {
|
||
if (!window.EmbeddingClient || typeof window.EmbeddingClient.saveConfig !== 'function') {
|
||
const ok = await ensureEmbeddingClientLoaded();
|
||
if (!ok) throw new Error('EmbeddingClient 未加载');
|
||
}
|
||
window.EmbeddingClient.saveConfig({ ...testConfig, enabled: true });
|
||
const vector = await window.EmbeddingClient.embed('测试文本');
|
||
|
||
result.style.display = 'block';
|
||
result.style.color = '#059669';
|
||
result.textContent = `✅ 连接成功!向量维度: ${vector.length}`;
|
||
} catch (error) {
|
||
result.style.display = 'block';
|
||
result.style.color = '#dc2626';
|
||
result.textContent = `❌ 连接失败: ${error.message}`;
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = '测试连接';
|
||
}
|
||
};
|
||
|
||
// 保存配置
|
||
$('emb-save-km').onclick = async () => {
|
||
let baseUrl = $('emb-endpoint-km').value.trim();
|
||
// 自动补全 /embeddings 路径
|
||
if (baseUrl && !baseUrl.endsWith('/embeddings')) {
|
||
baseUrl = baseUrl.replace(/\/+$/, '') + '/embeddings';
|
||
}
|
||
|
||
const newConfig = {
|
||
enabled: $('emb-enabled-km').checked,
|
||
provider: $('emb-provider-km').value,
|
||
apiKey: $('emb-api-key-km').value,
|
||
endpoint: baseUrl,
|
||
model: $('emb-model-km').value,
|
||
dimensions: parseInt($('emb-dimensions-km').value) || null,
|
||
concurrency: Math.max(1, Math.min(parseInt($('emb-concurrency-km').value) || 5, 50))
|
||
};
|
||
|
||
if (!window.EmbeddingClient || typeof window.EmbeddingClient.saveConfig !== 'function') {
|
||
const ok = await ensureEmbeddingClientLoaded();
|
||
if (!ok) { alert('❌ 保存失败:EmbeddingClient 未加载'); return; }
|
||
}
|
||
window.EmbeddingClient.saveConfig(newConfig);
|
||
if (typeof showNotification === 'function') {
|
||
showNotification('向量搜索配置已保存', 'success');
|
||
} else {
|
||
alert('配置已保存');
|
||
}
|
||
};
|
||
|
||
// Rerank: 服务商切换(仅 OpenAI 格式显示“获取列表”)
|
||
$('rerank-provider-km').onchange = function() {
|
||
const provider = this.value;
|
||
const fetchBtn = $('rerank-fetch-models-km');
|
||
const hint = $('rerank-model-hint-km');
|
||
const endpointInput = $('rerank-endpoint-km');
|
||
|
||
if (provider === 'openai') {
|
||
fetchBtn.style.display = 'block';
|
||
hint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取';
|
||
if (!endpointInput.value.trim()) endpointInput.placeholder = 'https://api.openai.com/v1';
|
||
} else {
|
||
fetchBtn.style.display = 'none';
|
||
hint.textContent = '请输入服务商支持的重排模型ID';
|
||
if (!endpointInput.value.trim()) {
|
||
endpointInput.placeholder = provider === 'jina' ? 'https://api.jina.ai/v1' : 'https://api.cohere.ai/v1';
|
||
}
|
||
}
|
||
};
|
||
|
||
// 初始化 Rerank 提示与按钮显示
|
||
(function() {
|
||
const provider = ($('rerank-provider-km')?.value) || 'jina';
|
||
const fetchBtn = $('rerank-fetch-models-km');
|
||
const hint = $('rerank-model-hint-km');
|
||
const endpointInput = $('rerank-endpoint-km');
|
||
if (provider === 'openai') {
|
||
fetchBtn.style.display = 'block';
|
||
hint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取';
|
||
if (!endpointInput.value.trim()) endpointInput.placeholder = 'https://api.openai.com/v1';
|
||
} else {
|
||
fetchBtn.style.display = 'none';
|
||
hint.textContent = '请输入服务商支持的重排模型ID';
|
||
}
|
||
})();
|
||
|
||
// Rerank: 获取模型列表(OpenAI 格式)
|
||
$('rerank-fetch-models-km').onclick = async () => {
|
||
const btn = $('rerank-fetch-models-km');
|
||
const modelInput = $('rerank-model-km');
|
||
const hint = $('rerank-model-hint-km');
|
||
const apiKey = $('rerank-api-key-km').value;
|
||
let baseUrl = $('rerank-endpoint-km').value.trim();
|
||
|
||
if (!apiKey) {
|
||
hint.style.color = '#dc2626';
|
||
hint.textContent = '❌ 请先输入 API Key';
|
||
setTimeout(() => { hint.style.color = '#6b7280'; hint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取'; }, 3000);
|
||
return;
|
||
}
|
||
|
||
if (!baseUrl) {
|
||
baseUrl = 'https://api.openai.com/v1';
|
||
}
|
||
|
||
const modelsEndpoint = baseUrl.replace(/\/+$/, '') + '/models';
|
||
|
||
btn.textContent = '获取中...';
|
||
btn.disabled = true;
|
||
hint.style.color = '#6b7280';
|
||
hint.textContent = '正在获取模型列表...';
|
||
|
||
try {
|
||
const response = await fetch(modelsEndpoint, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
|
||
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
const data = await response.json();
|
||
const models = data.data || [];
|
||
const rerankModels = models.filter(m => {
|
||
const id = (m.id || '').toLowerCase();
|
||
return id.includes('rerank') || id.includes('rank') || id.includes('relevance') || id.includes('search');
|
||
});
|
||
const list = rerankModels.length > 0 ? rerankModels : models;
|
||
if (list.length === 0) {
|
||
hint.style.color = '#f59e0b';
|
||
hint.textContent = '⚠️ 未从服务端获取到模型列表';
|
||
setTimeout(() => { hint.style.color = '#6b7280'; hint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取'; }, 3000);
|
||
return;
|
||
}
|
||
showRerankModelSelector(list, modelInput);
|
||
hint.style.color = '#059669';
|
||
hint.textContent = `✅ 找到 ${list.length} 个模型`;
|
||
setTimeout(() => { hint.style.color = '#6b7280'; hint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取'; }, 3000);
|
||
} catch (error) {
|
||
hint.style.color = '#dc2626';
|
||
hint.textContent = `❌ 获取失败: ${error.message}`;
|
||
setTimeout(() => { hint.style.color = '#6b7280'; hint.textContent = '可手动输入模型ID,或点击"获取列表"从服务器获取'; }, 3000);
|
||
} finally {
|
||
btn.textContent = '获取列表';
|
||
btn.disabled = false;
|
||
}
|
||
};
|
||
|
||
// Rerank: 模型检测
|
||
$('rerank-check-model-km').onclick = async () => {
|
||
const btn = $('rerank-check-model-km');
|
||
const modelId = $('rerank-model-km').value.trim();
|
||
const provider = $('rerank-provider-km').value;
|
||
const apiKey = $('rerank-api-key-km').value;
|
||
let baseUrl = $('rerank-endpoint-km').value.trim();
|
||
const hint = $('rerank-model-hint-km');
|
||
|
||
if (!modelId) {
|
||
hint.style.color = '#dc2626';
|
||
hint.textContent = '❌ 请输入模型ID';
|
||
setTimeout(() => { hint.style.color = '#6b7280'; hint.textContent = '请输入服务商支持的重排模型ID;OpenAI格式可点击“获取列表”'; }, 2500);
|
||
return;
|
||
}
|
||
if (!apiKey) {
|
||
hint.style.color = '#dc2626';
|
||
hint.textContent = '❌ 请输入 API Key';
|
||
setTimeout(() => { hint.style.color = '#6b7280'; hint.textContent = '请输入服务商支持的重排模型ID;OpenAI格式可点击“获取列表”'; }, 2500);
|
||
return;
|
||
}
|
||
|
||
btn.disabled = true;
|
||
btn.textContent = '检测中...';
|
||
hint.style.color = '#6b7280';
|
||
hint.textContent = '正在检测模型...';
|
||
|
||
try {
|
||
if (provider === 'openai') {
|
||
if (!baseUrl) baseUrl = 'https://api.openai.com/v1';
|
||
const modelsEndpoint = baseUrl.replace(/\/+$/, '') + '/models';
|
||
const resp = await fetch(modelsEndpoint, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
|
||
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
|
||
const data = await resp.json();
|
||
const models = (data.data || []).map(m => m.id);
|
||
if (models.includes(modelId)) {
|
||
hint.style.color = '#059669';
|
||
hint.textContent = '✅ 模型可用';
|
||
} else {
|
||
hint.style.color = '#f59e0b';
|
||
hint.textContent = '⚠️ 未在列表中找到该模型(可能仍可用)';
|
||
}
|
||
} else {
|
||
const endpoint = (baseUrl || (provider === 'jina' ? 'https://api.jina.ai/v1' : 'https://api.cohere.ai/v1')).replace(/\/+$/, '') + '/rerank';
|
||
const resp = await fetch(endpoint, {
|
||
method: 'POST',
|
||
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ model: modelId, query: 'ping', documents: ['pong'], top_n: 1 })
|
||
});
|
||
if (resp.ok) {
|
||
hint.style.color = '#059669';
|
||
hint.textContent = '✅ 模型可用';
|
||
} else {
|
||
const text = await resp.text();
|
||
throw new Error(`${resp.status} ${text}`);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
hint.style.color = '#dc2626';
|
||
hint.textContent = `❌ 检测失败: ${error.message}`;
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = '检测模型';
|
||
}
|
||
};
|
||
|
||
// 重排测试连接
|
||
$('rerank-test-km').onclick = async () => {
|
||
const btn = $('rerank-test-km');
|
||
const result = $('rerank-test-result-km');
|
||
|
||
// 自动补全 /rerank 路径
|
||
let rerankBase = $('rerank-endpoint-km').value.trim();
|
||
if (rerankBase && !/\/rerank\/?$/.test(rerankBase)) {
|
||
rerankBase = rerankBase.replace(/\/+$/, '') + '/rerank';
|
||
}
|
||
|
||
const testConfig = {
|
||
provider: $('rerank-provider-km').value,
|
||
apiKey: $('rerank-api-key-km').value,
|
||
endpoint: rerankBase,
|
||
model: $('rerank-model-km').value,
|
||
topN: parseInt($('rerank-top-n-km').value) || 10
|
||
};
|
||
|
||
if (!testConfig.apiKey || !testConfig.model) {
|
||
result.style.display = 'block';
|
||
result.style.color = '#dc2626';
|
||
result.textContent = '❌ 请填写完整配置';
|
||
return;
|
||
}
|
||
|
||
btn.disabled = true;
|
||
btn.textContent = '测试中...';
|
||
result.style.display = 'none';
|
||
|
||
try {
|
||
if (!window.RerankClient) {
|
||
throw new Error('RerankClient 未加载');
|
||
}
|
||
|
||
window.RerankClient.saveConfig({ ...testConfig, enabled: true });
|
||
const testQuery = '测试查询';
|
||
const testDocs = ['文档1内容', '文档2内容', '文档3内容'];
|
||
const results = await window.RerankClient.rerank(testQuery, testDocs);
|
||
|
||
result.style.display = 'block';
|
||
result.style.color = '#059669';
|
||
result.textContent = `✅ 连接成功!返回 ${results.length} 个结果`;
|
||
} catch (error) {
|
||
result.style.display = 'block';
|
||
result.style.color = '#dc2626';
|
||
result.textContent = `❌ 连接失败: ${error.message}`;
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = '测试连接';
|
||
}
|
||
};
|
||
|
||
// 重排保存配置(补全 /rerank)
|
||
$('rerank-save-km').onclick = () => {
|
||
// 获取选中的scope
|
||
const scopeRadios = document.getElementsByName('rerank-scope-km');
|
||
let scope = 'vector-only';
|
||
for (const radio of scopeRadios) {
|
||
if (radio.checked) {
|
||
scope = radio.value;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 自动补全 /rerank 路径
|
||
let rerankBase = $('rerank-endpoint-km').value.trim();
|
||
if (rerankBase && !/\/rerank\/?$/.test(rerankBase)) {
|
||
rerankBase = rerankBase.replace(/\/+$/, '') + '/rerank';
|
||
}
|
||
|
||
const newConfig = {
|
||
enabled: $('rerank-enabled-km').checked,
|
||
scope: scope,
|
||
provider: $('rerank-provider-km').value,
|
||
apiKey: $('rerank-api-key-km').value,
|
||
endpoint: rerankBase,
|
||
model: $('rerank-model-km').value,
|
||
topN: parseInt($('rerank-top-n-km').value) || 10
|
||
};
|
||
|
||
if (!window.RerankClient) {
|
||
alert('RerankClient 未加载');
|
||
return;
|
||
}
|
||
|
||
window.RerankClient.saveConfig(newConfig);
|
||
if (typeof showNotification === 'function') {
|
||
showNotification('重排配置已保存', 'success');
|
||
} else {
|
||
alert('配置已保存');
|
||
}
|
||
};
|
||
}
|
||
|
||
// 导出到全局作用域
|
||
window.UIEmbeddingConfigRenderer = {
|
||
renderEmbeddingConfig
|
||
};
|
||
|
||
})(window);
|