You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

437 lines
20 KiB

const API_BASE = '/api/v1/ai-config';
let currentConfig = null;
let selectedProvider = 'openai';
document.addEventListener('DOMContentLoaded', function() {
loadProviders();
loadConfig();
initEventListeners();
});
function initEventListeners() {
document.getElementById('api-provider').addEventListener('change', function() {
selectedProvider = this.value;
updateProviderModels();
});
document.getElementById('temperature').addEventListener('input', function() {
document.getElementById('temp-value').textContent = this.value;
});
}
async function loadProviders() {
try {
const response = await fetch(`${API_BASE}/providers`);
const data = await response.json();
if (data.success) {
renderProviders(data.data);
}
} catch (error) {
console.error('加载提供商失败:', error);
renderProviders(getDefaultProviders());
}
}
function getDefaultProviders() {
return [
{ id: 'openai', name: 'OpenAI', icon: 'brain' },
{ id: 'anthropic', name: 'Claude', icon: 'robot' },
{ id: 'google', name: 'Gemini', icon: 'gem' },
{ id: 'aliyun', name: '通义千问', icon: 'cloud' },
{ id: 'aliyun_coding', name: '通义灵码', icon: 'code' },
{ id: 'bailian', name: '阿里百炼', icon: 'flask' },
{ id: 'baidu', name: '文心一言', icon: 'comments' },
{ id: 'zhipu', name: '智谱清言', icon: 'lightbulb' }
];
}
function getProviderSVG(iconName) {
const svgMap = {
'brain': `<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 4C11.5 4 8 7.5 8 12C8 14.5 9.5 16.5 11 18V24C11 26 13 28 16 28C19 28 21 26 21 24V18C22.5 16.5 24 14.5 24 12C24 7.5 20.5 4 16 4Z" stroke="currentColor" stroke-width="2"/>
<path d="M13 28V30H19V28" stroke="currentColor" stroke-width="2"/>
<circle cx="13" cy="12" r="1.5" fill="currentColor"/>
<circle cx="19" cy="12" r="1.5" fill="currentColor"/>
</svg>`,
'robot': `<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6" y="10" width="20" height="14" rx="3" stroke="currentColor" stroke-width="2"/>
<circle cx="13" cy="17" r="2" fill="currentColor"/>
<circle cx="19" cy="17" r="2" fill="currentColor"/>
<path d="M10 6V4C10 2.89543 10.8954 2 12 2H20C21.1046 2 22 2.89543 22 4V6" stroke="currentColor" stroke-width="2"/>
<path d="M12 24H20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>`,
'gem': `<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 4L28 12L16 28L4 12L16 4Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<path d="M4 12H28" stroke="currentColor" stroke-width="2"/>
<path d="M16 4L12 12L16 28L20 12L16 4Z" stroke="currentColor" stroke-width="2"/>
</svg>`,
'cloud': `<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 22C5.79086 22 4 20.2091 4 18C4 15.9584 5.53246 14.2884 7.5 14.0284C8.16974 10.6833 11.1509 8.16667 14.6667 8.16667C18.8756 8.16667 22.25 11.591 22.25 15.75C22.25 15.9028 22.2457 16.0549 22.2373 16.2059C24.0427 16.5125 25.4167 18.1191 25.4167 20.0833C25.4167 22.4167 23.5 24.5 21.25 24.5H8Z" stroke="currentColor" stroke-width="2"/>
</svg>`,
'code': `<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 10L4 16L10 22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 10L28 16L22 22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 6L14 26" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>`,
'flask': `<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 4H20V12L26 24C26.5 25 26 26 25 27C24 28 23 28 22 28H10C9 28 8 27 7 26C6 25 5.5 24 6 23L12 12V4Z" stroke="currentColor" stroke-width="2"/>
<path d="M10 4H22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M8 22H24" stroke="currentColor" stroke-width="2"/>
</svg>`,
'comments': `<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 6C4 4.89543 4.89543 4 6 4H26C27.1046 4 28 4.89543 28 6V18C28 19.1046 27.1046 20 26 20H10L6 24V20C4.89543 20 4 19.1046 4 18V6Z" stroke="currentColor" stroke-width="2"/>
<path d="M10 10H22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M10 14H18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>`,
'lightbulb': `<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 4C10.4772 4 6 8.47715 6 14C6 17.5 8 20.5 11 22V25C11 26.1046 11.8954 27 13 27H19C20.1046 27 21 26.1046 21 25V22C24 20.5 26 17.5 26 14C26 8.47715 21.5228 4 16 4Z" stroke="currentColor" stroke-width="2"/>
<path d="M13 27V29H19V27" stroke="currentColor" stroke-width="2"/>
</svg>`,
'cog': `<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="4" stroke="currentColor" stroke-width="2"/>
<path d="M16 4V8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M16 24V28" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M4 16H8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M24 16H28" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M7.05029 7.05029L9.87873 9.87873" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M22.1213 22.1213L24.9497 24.9497" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M7.05029 24.9497L9.87873 22.1213" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M22.1213 9.87873L24.9497 7.05029" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>`
};
return svgMap[iconName] || svgMap['cog'];
}
function renderProviders(providers) {
const grid = document.getElementById('provider-grid');
const iconMap = {
'openai': 'brain',
'anthropic': 'robot',
'google': 'gem',
'aliyun': 'cloud',
'aliyun_coding': 'code',
'bailian': 'flask',
'baidu': 'comments',
'zhipu': 'lightbulb',
'custom': 'cog'
};
grid.innerHTML = providers.map(p => `
<div class="provider-card ${p.id === selectedProvider ? 'active' : ''}" data-provider="${p.id}">
${getProviderSVG(iconMap[p.id] || 'cog')}
<div class="provider-name">${p.name}</div>
</div>
`).join('');
grid.querySelectorAll('.provider-card').forEach(card => {
card.addEventListener('click', function() {
grid.querySelectorAll('.provider-card').forEach(c => c.classList.remove('active'));
this.classList.add('active');
selectedProvider = this.dataset.provider;
document.getElementById('api-provider').value = selectedProvider;
updateProviderModels();
});
});
}
function updateProviderModels() {
const modelSelect = document.getElementById('model-id');
const modelMap = {
'openai': ['gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo'],
'anthropic': ['claude-3-opus', 'claude-3-sonnet', 'claude-3-haiku'],
'google': ['gemini-pro', 'gemini-pro-vision'],
'aliyun': ['qwen-max', 'qwen-plus', 'qwen-turbo'],
'aliyun_coding': ['qwen-coder-plus', 'qwen-coder-turbo'],
'bailian': ['qwen3.6-plus', 'qwen3.5-plus', 'qwen3-max', 'qwen3-coder-plus', 'MiniMax-M2.5', 'glm-4.7', 'kimi-k2.5', 'custom'],
'baidu': ['ernie-4.0', 'ernie-3.5', 'ernie-speed'],
'zhipu': ['glm-4', 'glm-3-turbo'],
'custom': ['custom-model']
};
const apiBaseMap = {
'openai': 'https://api.openai.com/v1',
'anthropic': 'https://api.anthropic.com/v1',
'google': 'https://generativelanguage.googleapis.com/v1beta',
'aliyun': 'https://dashscope.aliyuncs.com/compatible-mode/v1',
'aliyun_coding': 'https://dashscope.aliyuncs.com/compatible-mode/v1',
'bailian': 'https://coding.dashscope.aliyuncs.com/v1',
'baidu': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop',
'zhipu': 'https://open.bigmodel.cn/api/paas/v4',
'custom': ''
};
const models = modelMap[selectedProvider] || ['custom-model'];
modelSelect.innerHTML = models.map(m => `<option value="${m}">${m}</option>`).join('');
document.getElementById('api-base').value = apiBaseMap[selectedProvider] || '';
}
async function loadConfig() {
try {
const response = await fetch(API_BASE);
const result = await response.json();
if (result.success && result.data) {
currentConfig = result.data;
populateForm(currentConfig);
renderModelsList(currentConfig.models || []);
}
} catch (error) {
console.error('加载配置失败:', error);
}
}
function populateForm(config) {
if (config.models && config.models.length > 0) {
const activeModel = config.models.find(m => m.enabled) || config.models[0];
document.getElementById('api-provider').value = activeModel.provider || 'openai';
document.getElementById('api-key').value = activeModel.api_key || '';
document.getElementById('api-base').value = activeModel.api_base || '';
document.getElementById('model-id').value = activeModel.model_id || 'gpt-4o';
document.getElementById('custom-model').value = '';
document.getElementById('temperature').value = activeModel.temperature || 0.7;
document.getElementById('temp-value').textContent = activeModel.temperature || 0.7;
document.getElementById('max-tokens').value = activeModel.max_tokens || 2000;
const providerCard = document.querySelector(`.provider-card[data-provider="${activeModel.provider}"]`);
if (providerCard) {
document.querySelectorAll('.provider-card').forEach(c => c.classList.remove('active'));
providerCard.classList.add('active');
}
updateProviderModels();
}
if (config.analysis_settings) {
document.getElementById('enable-technical').checked = config.analysis_settings.enable_technical_analysis !== false;
document.getElementById('enable-fundamental').checked = config.analysis_settings.enable_fundamental_analysis === true;
document.getElementById('enable-sentiment').checked = config.analysis_settings.enable_sentiment_analysis === true;
document.getElementById('risk-tolerance').value = config.analysis_settings.risk_tolerance || 'medium';
document.getElementById('max-position').value = config.analysis_settings.max_position_pct || 10;
}
}
function renderModelsList(models) {
const list = document.getElementById('models-list');
const activeCard = document.getElementById('active-ai-card');
if (!models || models.length === 0) {
list.innerHTML = '<div class="empty-state">暂无已保存的模型</div>';
activeCard.style.display = 'none';
return;
}
const activeModel = models.find(m => m.enabled);
if (activeModel) {
activeCard.style.display = 'block';
document.getElementById('active-ai-name').textContent = getProviderName(activeModel.provider || activeModel.api_base);
document.getElementById('active-ai-model').textContent = activeModel.model_name || activeModel.model_id || '--';
const iconMap = {
'openai': 'brain',
'anthropic': 'robot',
'google': 'gem',
'aliyun': 'cloud',
'aliyun_coding': 'code',
'bailian': 'flask',
'baidu': 'comments',
'zhipu': 'lightbulb'
};
document.getElementById('active-ai-icon').innerHTML = getProviderSVG(iconMap[activeModel.provider] || 'robot');
} else {
activeCard.style.display = 'none';
}
list.innerHTML = models.map((model, index) => `
<div class="model-item">
<div class="model-info">
<div class="model-status ${model.enabled ? 'active' : 'inactive'}"></div>
<div>
<div class="model-name">${model.model_name || model.model_id}</div>
<div class="model-provider">${getProviderName(model.provider || model.api_base)}</div>
</div>
</div>
<div class="model-actions">
${!model.enabled ? `<button class="btn-set-active" onclick="setActiveModel(${index})">设为默认</button>` : '<span class="active-badge">当前启用</span>'}
<button class="btn-delete" onclick="deleteModel(${index})">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3H12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M5 3V2C5 1.44772 5.44772 1 6 1H8C8.55228 1 9 1.44772 9 2V3" stroke="currentColor" stroke-width="1.5"/>
<path d="M3 3L3.5 12C3.5 12.5523 3.94772 13 4.5 13H9.5C10.0523 13 10.5 12.5523 10.5 12L11 3" stroke="currentColor" stroke-width="1.5"/>
</svg>
</button>
</div>
</div>
`).join('');
}
function getProviderName(apiBase) {
const map = {
'openai': 'OpenAI',
'anthropic': 'Anthropic',
'google': 'Google',
'aliyun': '阿里云',
'aliyun_coding': '阿里云通义灵码',
'bailian': '阿里百炼',
'baidu': '百度',
'zhipu': '智谱'
};
return map[apiBase] || apiBase;
}
function toggleApiKeyVisibility() {
const input = document.getElementById('api-key');
const svgIcon = document.querySelector('.toggle-visibility .eye-icon');
if (input.type === 'password') {
input.type = 'text';
svgIcon.innerHTML = `
<path d="M3 3L15 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M11.4141 6.41406C12.8406 7.51393 13.9453 8.93657 14.6605 10.5658C14.6957 10.6462 14.7012 10.7373 14.6761 10.8214C14.2061 12.3998 13.2893 13.8157 12.0285 14.921C10.7677 16.0263 9.20885 16.7814 7.53077 17.0963C5.85269 17.4113 4.11977 17.2734 2.50625 16.7004C1.69972 16.4166 0.95692 16.0016 0.3125 15.475" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M1 8.5C1 8.5 4 2.5 8.5 2.5C10.5 2.5 12.2667 3.5 13.6667 4.83333" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
`;
} else {
input.type = 'password';
svgIcon.innerHTML = `
<path d="M1 9C1 9 4 3 9 3C14 3 17 9 17 9C17 9 14 15 9 15C4 15 1 9 1 9Z" stroke="currentColor" stroke-width="1.5"/>
<circle cx="9" cy="9" r="2.5" stroke="currentColor" stroke-width="1.5"/>
`;
}
}
async function testConnection() {
const resultEl = document.getElementById('test-result');
resultEl.textContent = '测试中...';
resultEl.className = 'test-result';
const customModel = document.getElementById('custom-model').value.trim();
const selectedModel = document.getElementById('model-id').value;
const modelId = customModel || (selectedModel === 'custom' ? '' : selectedModel);
if (!modelId) {
resultEl.textContent = '✗ 请输入模型ID';
resultEl.className = 'test-result error';
return;
}
const config = {
model_name: modelId,
api_key: document.getElementById('api-key').value,
api_base: document.getElementById('api-base').value,
model_id: modelId,
temperature: parseFloat(document.getElementById('temperature').value),
max_tokens: parseInt(document.getElementById('max-tokens').value),
enabled: true
};
try {
const response = await fetch(`${API_BASE}/test`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
if (!response.ok) {
resultEl.textContent = `✗ 请求失败: ${response.status}`;
resultEl.className = 'test-result error';
return;
}
const data = await response.json();
if (data.success) {
resultEl.textContent = '✓ 连接成功';
resultEl.className = 'test-result success';
} else {
resultEl.textContent = '✗ ' + (data.message || '未知错误');
resultEl.className = 'test-result error';
}
} catch (error) {
resultEl.textContent = '✗ 连接失败: ' + (error.message || '未知错误');
resultEl.className = 'test-result error';
}
}
async function saveConfig() {
const models = currentConfig?.models || [];
const existingIndex = models.findIndex(m => m.provider === selectedProvider);
const customModel = document.getElementById('custom-model').value.trim();
const selectedModel = document.getElementById('model-id').value;
const modelId = customModel || (selectedModel === 'custom' ? '' : selectedModel);
const newModel = {
model_name: modelId,
provider: selectedProvider,
api_key: document.getElementById('api-key').value,
api_base: document.getElementById('api-base').value,
model_id: modelId,
temperature: parseFloat(document.getElementById('temperature').value),
max_tokens: parseInt(document.getElementById('max-tokens').value),
enabled: true
};
if (existingIndex >= 0) {
models[existingIndex] = { ...models[existingIndex], ...newModel };
} else {
models.push(newModel);
}
models.forEach(m => {
m.enabled = (m.provider === selectedProvider && m.model_id === modelId);
});
const config = {
models: models,
active_model: selectedProvider,
analysis_settings: {
enable_technical_analysis: document.getElementById('enable-technical').checked,
enable_fundamental_analysis: document.getElementById('enable-fundamental').checked,
enable_sentiment_analysis: document.getElementById('enable-sentiment').checked,
risk_tolerance: document.getElementById('risk-tolerance').value,
max_position_pct: parseInt(document.getElementById('max-position').value)
}
};
try {
const response = await fetch(API_BASE, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
const data = await response.json();
if (data.success) {
alert('配置保存成功!');
currentConfig = config;
renderModelsList(models);
} else {
alert('保存失败: ' + data.message);
}
} catch (error) {
alert('保存失败: ' + error.message);
}
}
function setActiveModel(index) {
if (!currentConfig || !currentConfig.models) return;
currentConfig.models.forEach((m, i) => {
m.enabled = i === index;
});
saveConfig();
}
function deleteModel(index) {
if (!confirm('确定要删除这个模型吗?')) return;
if (!currentConfig || !currentConfig.models) return;
currentConfig.models.splice(index, 1);
saveConfig();
}
function addNewModel() {
document.getElementById('api-key').value = '';
document.getElementById('api-key').focus();
}