Files
ai-code-review/static/index.html

680 lines
30 KiB
HTML
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.

<!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-20 表示最确定的输出</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>