680 lines
30 KiB
HTML
680 lines
30 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Code Review 配置</title>
|
||
<meta charset="UTF-8">
|
||
<meta name="color-scheme" content="light dark">
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<style>
|
||
.container { max-width: 800px; margin-top: 2rem; }
|
||
.form-group { margin-bottom: 1rem; }
|
||
.password-field {
|
||
position: relative;
|
||
}
|
||
.toggle-password {
|
||
position: absolute;
|
||
right: 10px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
border: none;
|
||
background: none;
|
||
cursor: pointer;
|
||
}
|
||
/* 主题切换按钮样式 */
|
||
.theme-switcher {
|
||
position: fixed;
|
||
top: 1rem;
|
||
right: 1rem;
|
||
z-index: 1000;
|
||
}
|
||
.theme-switcher .dropdown-menu {
|
||
min-width: 8rem;
|
||
}
|
||
.theme-switcher .dropdown-item i {
|
||
width: 1rem;
|
||
margin-right: 0.5rem;
|
||
}
|
||
.theme-switcher .dropdown-item.active {
|
||
background-color: var(--bs-primary);
|
||
color: white;
|
||
}
|
||
|
||
/* 暗色主题下的一些自定义样式 */
|
||
[data-bs-theme="dark"] {
|
||
color-scheme: dark;
|
||
background-color: #1a1d20;
|
||
color: #e9ecef;
|
||
}
|
||
[data-bs-theme="dark"] .card {
|
||
background-color: #2b3035;
|
||
border-color: #495057;
|
||
}
|
||
[data-bs-theme="dark"] .form-control {
|
||
background-color: #1a1d20;
|
||
border-color: #495057;
|
||
color: #e9ecef;
|
||
}
|
||
[data-bs-theme="dark"] .form-control:focus {
|
||
background-color: #1a1d20;
|
||
border-color: #86b7fe;
|
||
color: #e9ecef;
|
||
}
|
||
|
||
/* 暗色主题下的其他元素样式 */
|
||
[data-bs-theme="dark"] .card-header {
|
||
background-color: #343a40;
|
||
border-bottom-color: #495057;
|
||
}
|
||
|
||
[data-bs-theme="dark"] .btn-outline-secondary {
|
||
color: #e9ecef;
|
||
border-color: #495057;
|
||
}
|
||
|
||
[data-bs-theme="dark"] .btn-outline-secondary:hover {
|
||
background-color: #495057;
|
||
color: #fff;
|
||
}
|
||
|
||
[data-bs-theme="dark"] .dropdown-menu {
|
||
background-color: #2b3035;
|
||
border-color: #495057;
|
||
}
|
||
|
||
[data-bs-theme="dark"] .dropdown-item {
|
||
color: #e9ecef;
|
||
}
|
||
|
||
[data-bs-theme="dark"] .dropdown-item:hover {
|
||
background-color: #343a40;
|
||
}
|
||
|
||
/* 亮色主题样式 */
|
||
[data-bs-theme="light"] {
|
||
background-color: #ffffff;
|
||
color: #212529;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body data-bs-theme="light">
|
||
<div class="theme-switcher dropdown">
|
||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||
<i class="bi bi-sun-fill"></i>
|
||
<span class="d-none d-md-inline ms-1">主题</span>
|
||
</button>
|
||
<ul class="dropdown-menu dropdown-menu-end">
|
||
<li><button class="dropdown-item" data-theme="light"><i class="bi bi-sun-fill"></i>亮色</button></li>
|
||
<li><button class="dropdown-item" data-theme="dark"><i class="bi bi-moon-fill"></i>暗色</button></li>
|
||
<li><button class="dropdown-item" data-theme="auto"><i class="bi bi-circle-half"></i>自动</button></li>
|
||
</ul>
|
||
</div>
|
||
<div class="container">
|
||
<div id="loginForm" class="card mb-3">
|
||
<div class="card-body">
|
||
<div class="form-group">
|
||
<label>管理令牌</label>
|
||
<input type="password" class="form-control" id="adminToken">
|
||
</div>
|
||
<button type="button" class="btn btn-primary" onclick="verifyToken()">验证</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="configSection" style="display: none;">
|
||
<h2>Code Review 配置</h2>
|
||
<form id="configForm">
|
||
<div class="card mb-3">
|
||
<div class="card-header">
|
||
AI 配置
|
||
<button type="button" class="btn btn-sm btn-primary float-end" onclick="addAIService()">添加 AI 服务</button>
|
||
</div>
|
||
<div class="card-body" id="aiServices">
|
||
<!-- AI 服务配置将动态添加到这里 -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-3">
|
||
<div class="card-header">自动禁用配置</div>
|
||
<div class="card-body">
|
||
<div class="form-check mb-3">
|
||
<input type="checkbox" class="form-check-input" name="auto_disable.enabled" id="autoDisableEnabled">
|
||
<label class="form-check-label" for="autoDisableEnabled">启用自动禁用</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>最大失败次数</label>
|
||
<input type="number" class="form-control" name="auto_disable.max_failures" min="1" value="3">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>重置时间(分钟)</label>
|
||
<input type="number" class="form-control" name="auto_disable.reset_after" min="1" value="30">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-3">
|
||
<div class="card-header">
|
||
Git 平台配置
|
||
<button type="button" class="btn btn-sm btn-primary float-end" onclick="addGitPlatform()">添加平台</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="gitPlatforms">
|
||
<!-- Git 平台配置将动态添加到这里 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-primary">保存配置</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 添加 Bootstrap JS
|
||
document.write('<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"><\/script>');
|
||
|
||
let adminToken = '';
|
||
|
||
// 验证令牌
|
||
async function verifyToken() {
|
||
const token = document.getElementById('adminToken').value;
|
||
try {
|
||
const response = await fetch('/api/config', {
|
||
headers: {
|
||
'X-Admin-Token': token
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('令牌验证失败');
|
||
}
|
||
|
||
// 验证成功,保存令牌并显示配置界面
|
||
adminToken = token;
|
||
document.getElementById('loginForm').style.display = 'none';
|
||
document.getElementById('configSection').style.display = 'block';
|
||
|
||
// 加载配置
|
||
loadConfig();
|
||
} catch (error) {
|
||
alert(error.message);
|
||
}
|
||
}
|
||
|
||
// 加载配置
|
||
async function loadConfig() {
|
||
try {
|
||
const response = await fetch('/api/config', {
|
||
headers: {
|
||
'X-Admin-Token': adminToken
|
||
}
|
||
});
|
||
const config = await response.json();
|
||
|
||
// 填充自动禁用配置
|
||
document.querySelector('[name="auto_disable.enabled"]').checked = config.auto_disable.enabled;
|
||
document.querySelector('[name="auto_disable.max_failures"]').value = config.auto_disable.max_failures;
|
||
document.querySelector('[name="auto_disable.reset_after"]').value = config.auto_disable.reset_after;
|
||
|
||
// 填充 AI 服务配置
|
||
if (config.ais && config.ais.length > 0) {
|
||
const aiServices = document.getElementById('aiServices');
|
||
aiServices.innerHTML = ''; // 清空现有内容
|
||
config.ais.forEach((ai, index) => {
|
||
aiServices.insertAdjacentHTML('beforeend', createAIServiceHTML(index, ai));
|
||
});
|
||
} else {
|
||
addAIService(); // 添加一个默认的 AI 服务配置
|
||
}
|
||
|
||
// 填充 Git 配置
|
||
const gitPlatforms = document.getElementById('gitPlatforms');
|
||
gitPlatforms.innerHTML = ''; // 清空现有内容
|
||
|
||
console.log('Git config:', config.git); // 调试日志
|
||
|
||
// 检查并处理 Git 平台配置
|
||
if (config.git && config.git.length > 0) {
|
||
config.git.forEach((platform, index) => {
|
||
gitPlatforms.insertAdjacentHTML('beforeend', createGitPlatformHTML(index, platform));
|
||
});
|
||
} else {
|
||
addGitPlatform(); // 添加一个默认的平台配置
|
||
}
|
||
|
||
// 添加调试日志
|
||
console.log('Loaded config:', config);
|
||
} catch (error) {
|
||
console.error('加载配置失败:', error);
|
||
alert('加载配置失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 默认系统提示词
|
||
const defaultSystemMsg = `你是一个代码审查员,你的职责是识别提交代码中的错误、性能问题和需要优化的地方。
|
||
你还负责提供建设性的反馈,并建议最佳实践来提高代码的整体质量。
|
||
|
||
在审查代码时:
|
||
- 审查代码变更(差异)并提供反馈
|
||
- 仔细检查是否真的存在错误或需要优化的空间,突出显示它们
|
||
- 不要突出显示小问题和细节
|
||
- 如果有多个评论,请使用要点符号
|
||
- 你不需要解释代码的功能
|
||
- 请使用中文给出反馈
|
||
- 如果你认为不需要优化或修改,请只回复 666`;
|
||
|
||
function createAIServiceHTML(index, ai = {}) {
|
||
return `
|
||
<div class="ai-service mb-4" data-index="${index}">
|
||
<h5 class="d-flex justify-content-between">
|
||
AI 服务 #${index + 1}
|
||
<button type="button" class="btn btn-sm btn-danger" onclick="removeAIService(${index})">删除</button>
|
||
</h5>
|
||
<div class="form-check mb-3">
|
||
<input type="checkbox" class="form-check-input" name="ais[${index}].enabled" ${ai.enabled ? 'checked' : ''}>
|
||
<label class="form-check-label">启用服务</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>名称</label>
|
||
<input type="text" class="form-control" name="ais[${index}].name"
|
||
value="${ai.name || ''}" required
|
||
title="请输入 AI 服务名称">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>类型</label>
|
||
<select class="form-control" name="ais[${index}].type"
|
||
onchange="toggleAPIKeyField(${index})" required
|
||
title="请选择 AI 服务类型">
|
||
<option value="openai" ${ai.type === 'openai' ? 'selected' : ''}>OpenAI</option>
|
||
<option value="ollama" ${ai.type === 'ollama' ? 'selected' : ''}>Ollama</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>API URL</label>
|
||
<input type="url" class="form-control" name="ais[${index}].url"
|
||
value="${ai.url || ''}" required
|
||
title="请输入有效的 API URL"
|
||
pattern="https?://.+">
|
||
</div>
|
||
<div class="api-key-field" style="display: ${ai.type === 'ollama' ? 'none' : 'block'}">
|
||
${createPasswordField(`ais[${index}].api_key`, ai.api_key, 'API Key')}
|
||
</div>
|
||
<div class="form-group">
|
||
<label>模型</label>
|
||
<input type="text" class="form-control" name="ais[${index}].model"
|
||
value="${ai.model || ''}" required
|
||
title="请输入模型名称">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Temperature</label>
|
||
<input type="number" class="form-control" name="ais[${index}].temperature"
|
||
value="${ai.temperature ?? 0}" min="0" max="2" step="0.1" required>
|
||
<small class="form-text text-muted">控制输出的随机性(0-2),0 表示最确定的输出</small>
|
||
</div>
|
||
<div class="form-check mb-3">
|
||
<input type="checkbox" class="form-check-input" name="ais[${index}].stream" ${ai.stream ? 'checked' : ''}>
|
||
<label class="form-check-label">启用流式响应</label>
|
||
<small class="form-text text-muted d-block">启用后可以逐步获取 AI 的响应</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>系统提示词</label>
|
||
<textarea class="form-control" name="ais[${index}].system_msg" rows="3"
|
||
placeholder="设置与 AI 对话的系统提示词,用于定义 AI 的角色和行为"
|
||
>${ai.system_msg || defaultSystemMsg}</textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>权重</label>
|
||
<input type="number" class="form-control" name="ais[${index}].weight" value="${ai.weight || 1}" min="1" required>
|
||
<small class="form-text text-muted">在相同优先级的 AI 中,按权重比例分配请求</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>优先级</label>
|
||
<input type="number" class="form-control" name="ais[${index}].priority" value="${ai.priority || 0}" min="0" required>
|
||
<small class="form-text text-muted">数字越大优先级越高,优先级相同的 AI 将按权重负载均衡</small>
|
||
</div>
|
||
<div class="form-check mb-3">
|
||
<input type="checkbox" class="form-check-input" name="ais[${index}].auto_disable" ${ai.auto_disable ? 'checked' : ''}>
|
||
<label class="form-check-label">使用自定义自动禁用配置</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>最大失败次数(可选)</label>
|
||
<input type="number" class="form-control" name="ais[${index}].max_failures" min="1" value="${ai.max_failures || ''}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>重置时间(分钟,可选)</label>
|
||
<input type="number" class="form-control" name="ais[${index}].reset_after" min="1" value="${ai.reset_after || ''}">
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function addAIService() {
|
||
const aiServices = document.getElementById('aiServices');
|
||
const index = aiServices.children.length;
|
||
aiServices.insertAdjacentHTML('beforeend', createAIServiceHTML(index, {
|
||
system_msg: defaultSystemMsg,
|
||
temperature: 0,
|
||
stream: false
|
||
}));
|
||
}
|
||
|
||
function removeAIService(index) {
|
||
const service = document.querySelector(`.ai-service[data-index="${index}"]`);
|
||
if (service) {
|
||
service.remove();
|
||
}
|
||
}
|
||
|
||
// 添加检查平台名称唯一性的函数
|
||
function isGitPlatformNameUnique(name, currentIndex) {
|
||
const platforms = document.querySelectorAll('.git-platform');
|
||
for (let i = 0; i < platforms.length; i++) {
|
||
if (i === currentIndex) continue;
|
||
const nameInput = platforms[i].querySelector('input[name$="].name"]');
|
||
if (nameInput && nameInput.value.trim() === name.trim()) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// 修改 createGitPlatformHTML 函数,添加 oninput 事件处理
|
||
function createGitPlatformHTML(index, platform = {}) {
|
||
// 添加调试日志
|
||
console.log('Creating Git platform HTML with:', platform);
|
||
|
||
return `
|
||
<div class="git-platform mb-4" data-index="${index}">
|
||
<h5 class="d-flex justify-content-between">
|
||
<div class="form-group" style="flex: 1; margin-right: 1rem;">
|
||
<input type="text" class="form-control" name="git[${index}].name"
|
||
value="${platform.name || ''}" placeholder="平台名称" required
|
||
title="请输入 Git 平台名称"
|
||
oninput="validateGitPlatformName(this, ${index})"
|
||
data-original-value="${platform.name || ''}">
|
||
<div class="invalid-feedback">平台名称已存在,请使用其他名称</div>
|
||
</div>
|
||
<button type="button" class="btn btn-sm btn-danger" onclick="removeGitPlatform(${index})">删除</button>
|
||
</h5>
|
||
<div class="form-group">
|
||
<label>平台类型</label>
|
||
<select class="form-control" name="git[${index}].type" required
|
||
title="请选择 Git 平台类型">
|
||
<option value="gitlab" ${platform.type === 'gitlab' ? 'selected' : ''}>GitLab</option>
|
||
<option value="gitea" ${platform.type === 'gitea' ? 'selected' : ''}>Gitea</option>
|
||
</select>
|
||
</div>
|
||
${createPasswordField(`git[${index}].token`, platform.token, 'Token', '请输入平台访问令牌')}
|
||
${createPasswordField(`git[${index}].webhook_secret`, platform.webhook_secret, 'Webhook Secret', '请输入 Webhook 密钥')}
|
||
<div class="form-group">
|
||
<label>API Base URL</label>
|
||
<input type="url" class="form-control" name="git[${index}].api_base"
|
||
value="${platform.api_base || ''}" required
|
||
title="请输入有效的 API Base URL"
|
||
pattern="https?://.+">
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 添加验证平台名称的函数
|
||
function validateGitPlatformName(input, index) {
|
||
const name = input.value.trim();
|
||
const originalValue = input.dataset.originalValue;
|
||
|
||
// 如果是编辑现有平台且名称未改变,则不需要验证
|
||
if (name === originalValue) {
|
||
input.setCustomValidity('');
|
||
input.classList.remove('is-invalid');
|
||
return true;
|
||
}
|
||
|
||
if (!isGitPlatformNameUnique(name, index)) {
|
||
input.setCustomValidity('平台名称必须唯一');
|
||
input.classList.add('is-invalid');
|
||
return false;
|
||
} else {
|
||
input.setCustomValidity('');
|
||
input.classList.remove('is-invalid');
|
||
return true;
|
||
}
|
||
}
|
||
|
||
function addGitPlatform() {
|
||
const gitPlatforms = document.getElementById('gitPlatforms');
|
||
const index = gitPlatforms.children.length;
|
||
gitPlatforms.insertAdjacentHTML('beforeend', createGitPlatformHTML(index));
|
||
}
|
||
|
||
function removeGitPlatform(index) {
|
||
const platform = document.querySelector(`.git-platform[data-index="${index}"]`);
|
||
if (platform) {
|
||
platform.remove();
|
||
}
|
||
}
|
||
|
||
// 修改表单提交处理,添加额外的验证
|
||
document.getElementById('configForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
|
||
// 验证所有 Git 平台名称的唯一性
|
||
const gitPlatformNames = new Set();
|
||
const platforms = document.querySelectorAll('.git-platform input[name$="].name"]');
|
||
let hasNameConflict = false;
|
||
|
||
platforms.forEach((input, index) => {
|
||
const name = input.value.trim();
|
||
if (gitPlatformNames.has(name)) {
|
||
hasNameConflict = true;
|
||
input.setCustomValidity('平台名称必须唯一');
|
||
input.classList.add('is-invalid');
|
||
} else {
|
||
gitPlatformNames.add(name);
|
||
}
|
||
});
|
||
|
||
if (hasNameConflict) {
|
||
e.target.reportValidity();
|
||
return;
|
||
}
|
||
|
||
// 使用 HTML5 原生表单验证
|
||
const form = e.target;
|
||
if (!form.checkValidity()) {
|
||
form.reportValidity();
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData(form);
|
||
const formEntries = Array.from(formData.entries()).reduce((acc, [key, value]) => {
|
||
// 处理复选框和数字类型的值
|
||
if (key.endsWith('.enabled') || key.endsWith('.auto_disable') || key.endsWith('.stream')) {
|
||
acc[key] = value === 'on';
|
||
} else if (key.includes('.temperature')) {
|
||
acc[key] = parseFloat(value);
|
||
} else if (key.includes('.weight') || key.includes('.max_failures') ||
|
||
key.includes('.reset_after') || key === 'port') {
|
||
acc[key] = value ? parseInt(value) : null;
|
||
} else {
|
||
acc[key] = value;
|
||
}
|
||
return acc;
|
||
}, {});
|
||
|
||
const config = unflattenObject(formEntries);
|
||
|
||
// 转换 Git 配置格式
|
||
if (config.git && Array.isArray(config.git)) {
|
||
config.git = {
|
||
platforms: config.git.map(platform => ({
|
||
name: platform.name,
|
||
type: platform.type,
|
||
token: platform.token,
|
||
webhook_secret: platform.webhook_secret,
|
||
api_base: platform.api_base,
|
||
signature_header: platform.type === 'gitlab' ? 'X-Gitlab-Token' : 'X-Gitea-Signature',
|
||
event_header: platform.type === 'gitlab' ? 'X-Gitlab-Event' : 'X-Gitea-Event'
|
||
}))
|
||
};
|
||
}
|
||
|
||
// 处理 AI 配置中的空值
|
||
if (config.ais) {
|
||
config.ais = config.ais.map(ai => {
|
||
// 如果 max_failures 和 reset_after 为空,则设置为 null
|
||
if (!ai.max_failures) ai.max_failures = null;
|
||
if (!ai.reset_after) ai.reset_after = null;
|
||
return ai;
|
||
});
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/config', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Admin-Token': adminToken
|
||
},
|
||
body: JSON.stringify(config)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json();
|
||
throw new Error(errorData.message || '更新配置失败');
|
||
}
|
||
|
||
alert('配置已更新');
|
||
// 重新加载配置以显示最新状态
|
||
loadConfig();
|
||
} catch (error) {
|
||
alert(error.message);
|
||
console.error('保存配置失败:', error);
|
||
}
|
||
});
|
||
|
||
// 辅助函数:展平对象
|
||
function flattenObject(obj, prefix = '') {
|
||
return Object.keys(obj).reduce((acc, k) => {
|
||
const pre = prefix.length ? prefix + '.' : '';
|
||
if (typeof obj[k] === 'object' && obj[k] !== null) {
|
||
Object.assign(acc, flattenObject(obj[k], pre + k));
|
||
} else {
|
||
acc[pre + k] = obj[k];
|
||
}
|
||
return acc;
|
||
}, {});
|
||
}
|
||
|
||
// 辅助函数:还原对象
|
||
function unflattenObject(obj) {
|
||
const result = {};
|
||
Object.entries(obj).forEach(([key, value]) => {
|
||
const parts = key.split('.');
|
||
let current = result;
|
||
while (parts.length > 1) {
|
||
const part = parts.shift();
|
||
current = current[part] = current[part] || {};
|
||
}
|
||
current[parts[0]] = value;
|
||
});
|
||
return result;
|
||
}
|
||
|
||
// 添加切换密码显示的函数
|
||
function togglePassword(button) {
|
||
const input = button.previousElementSibling;
|
||
const type = input.getAttribute('type');
|
||
input.setAttribute('type', type === 'password' ? 'text' : 'password');
|
||
button.innerHTML = type === 'password' ?
|
||
'<i class="bi bi-eye-slash"></i>' :
|
||
'<i class="bi bi-eye"></i>';
|
||
}
|
||
|
||
// 创建带切换按钮的密码输入框
|
||
function createPasswordField(name, value, label, title = '') {
|
||
return `
|
||
<div class="form-group">
|
||
<label>${label}</label>
|
||
<div class="password-field">
|
||
<input type="password" class="form-control"
|
||
name="${name}"
|
||
value="${value || ''}"
|
||
${name.includes('api_key') ? '' : 'required'}
|
||
title="${title || `请输入${label}`}">
|
||
<button type="button" class="toggle-password" onclick="togglePassword(this)">
|
||
<i class="bi bi-eye"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 添加切换 API Key 字段显示的函数
|
||
function toggleAPIKeyField(index) {
|
||
const aiService = document.querySelector(`.ai-service[data-index="${index}"]`);
|
||
const typeSelect = aiService.querySelector('select[name^="ais"][name$=".type"]');
|
||
const apiKeyField = aiService.querySelector('.api-key-field');
|
||
const apiKeyInput = apiKeyField.querySelector('input');
|
||
|
||
if (typeSelect.value === 'ollama') {
|
||
apiKeyField.style.display = 'none';
|
||
if (apiKeyInput) {
|
||
apiKeyInput.value = '';
|
||
apiKeyInput.removeAttribute('required');
|
||
}
|
||
} else {
|
||
apiKeyField.style.display = 'block';
|
||
if (apiKeyInput) {
|
||
apiKeyInput.setAttribute('required', '');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 主题切换功能
|
||
function setTheme(theme) {
|
||
const body = document.body;
|
||
const button = document.querySelector('.theme-switcher .dropdown-toggle i');
|
||
|
||
// 更新下拉菜单选中状态
|
||
document.querySelectorAll('.theme-switcher .dropdown-item').forEach(item => {
|
||
item.classList.toggle('active', item.dataset.theme === theme);
|
||
});
|
||
|
||
if (theme === 'auto') {
|
||
// 检测系统主题
|
||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||
body.setAttribute('data-bs-theme', prefersDark ? 'dark' : 'light');
|
||
button.className = 'bi bi-circle-half';
|
||
} else {
|
||
body.setAttribute('data-bs-theme', theme);
|
||
button.className = theme === 'light' ? 'bi bi-sun-fill' : 'bi bi-moon-fill';
|
||
}
|
||
|
||
// 保存主题设置
|
||
localStorage.setItem('theme', theme);
|
||
}
|
||
|
||
// 初始化主题
|
||
function initTheme() {
|
||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||
setTheme(savedTheme);
|
||
|
||
// 监听系统主题变化
|
||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||
if (localStorage.getItem('theme') === 'auto') {
|
||
document.body.setAttribute('data-bs-theme', e.matches ? 'dark' : 'light');
|
||
}
|
||
});
|
||
|
||
// 添加主题切换事件监听
|
||
document.querySelectorAll('.theme-switcher .dropdown-item').forEach(item => {
|
||
item.addEventListener('click', () => setTheme(item.dataset.theme));
|
||
});
|
||
}
|
||
|
||
// 页面加载时初始化主题
|
||
document.addEventListener('DOMContentLoaded', initTheme);
|
||
</script>
|
||
|
||
<!-- 在 body 末尾添加 Bootstrap Icons CDN -->
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
|
||
</body>
|
||
</html> |