paper-burner/js/ui/ui_embedding_config.js

997 lines
46 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;">&times;</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;">&times;</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">请输入服务商支持的重排模型IDOpenAI格式可点击“获取列表”</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 = '请输入服务商支持的重排模型IDOpenAI格式可点击“获取列表”'; }, 2500);
return;
}
if (!apiKey) {
hint.style.color = '#dc2626';
hint.textContent = '❌ 请输入 API Key';
setTimeout(() => { hint.style.color = '#6b7280'; hint.textContent = '请输入服务商支持的重排模型IDOpenAI格式可点击“获取列表”'; }, 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);