1 .修改了新建评估指标删除的bug

This commit is contained in:
2026-01-22 16:46:12 +08:00
parent 7109bdc9aa
commit 1a847996c8
10 changed files with 800 additions and 71 deletions

View File

@@ -39,7 +39,9 @@
"Bash(xargs:*)", "Bash(xargs:*)",
"Bash(/root/miniconda3/bin/pip install psutil)", "Bash(/root/miniconda3/bin/pip install psutil)",
"Bash(python -m py_compile:*)", "Bash(python -m py_compile:*)",
"Bash(git checkout:*)" "Bash(git checkout:*)",
"Bash(pip show:*)",
"Bash(pip install:*)"
] ]
} }
} }

View File

@@ -4,6 +4,7 @@ API 路由包
from .datasets import datasets_bp from .datasets import datasets_bp
from .model_manage import model_manage_bp from .model_manage import model_manage_bp
from .model_chat import model_chat_bp from .model_chat import model_chat_bp
from .dimension import dimension_bp
# 注册所有蓝图 # 注册所有蓝图
def register_blueprints(app): def register_blueprints(app):
@@ -11,3 +12,4 @@ def register_blueprints(app):
app.register_blueprint(datasets_bp) app.register_blueprint(datasets_bp)
app.register_blueprint(model_manage_bp) app.register_blueprint(model_manage_bp)
app.register_blueprint(model_chat_bp) app.register_blueprint(model_chat_bp)
app.register_blueprint(dimension_bp)

144
src/api/dimension.py Normal file
View File

@@ -0,0 +1,144 @@
"""
评测维度管理 API 路由
"""
import os
import pymysql
import yaml
from flask import Blueprint, request, jsonify
# 获取项目根目录
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 创建蓝图
dimension_bp = Blueprint('dimension', __name__, url_prefix='/api/dimension')
def get_db_connection():
"""获取数据库连接"""
CONFIG_PATH = os.path.join(PROJECT_ROOT, 'config.yaml')
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
CONFIG = yaml.safe_load(f)
db_config = CONFIG['database']
return pymysql.connect(
host=db_config['host'],
port=db_config['port'],
user=db_config['username'],
password=db_config['password'],
database=db_config['name'],
charset=db_config.get('charset', 'utf8mb4'),
cursorclass=pymysql.cursors.DictCursor
)
def generic_get_all(table_name, order_by='create_time DESC'):
"""通用查询所有"""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM {table_name} ORDER BY {order_by}")
result = cursor.fetchall()
cursor.close()
conn.close()
return result
def generic_create(table_name, data):
"""通用创建"""
conn = get_db_connection()
cursor = conn.cursor()
columns = ', '.join(data.keys())
placeholders = ', '.join(['%s'] * len(data))
sql = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
cursor.execute(sql, list(data.values()))
conn.commit()
new_id = cursor.lastrowid
cursor.close()
conn.close()
return new_id
def generic_update(table_name, id_val, data):
"""通用更新"""
conn = get_db_connection()
cursor = conn.cursor()
set_clause = ', '.join([f"{k} = %s" for k in data.keys()])
sql = f"UPDATE {table_name} SET {set_clause} WHERE id = %s"
values = list(data.values()) + [id_val]
cursor.execute(sql, values)
conn.commit()
cursor.close()
conn.close()
def generic_delete(table_name, id_val):
"""通用删除"""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(f"DELETE FROM {table_name} WHERE id = %s", (id_val,))
conn.commit()
cursor.close()
conn.close()
def generic_get_by_id(table_name, id_val):
"""通用按ID查询"""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM {table_name} WHERE id = %s", (id_val,))
result = cursor.fetchone()
cursor.close()
conn.close()
return result
# ============ 评测维度 CRUD ============
@dimension_bp.route('', methods=['GET'])
def get_dimensions():
"""获取所有评测维度"""
return jsonify({'code': 0, 'data': generic_get_all('model_dimension')})
@dimension_bp.route('/<int:id>', methods=['GET'])
def get_dimension_by_id(id):
"""获取单个评测维度"""
dimension = generic_get_by_id('model_dimension', id)
if dimension:
return jsonify({'code': 0, 'data': dimension})
return jsonify({'code': 1, 'message': '维度不存在'})
@dimension_bp.route('', methods=['POST'])
def create_dimension():
"""创建评测维度"""
data = request.json
insert_data = {
'name': data.get('name'),
'type': data.get('type'),
'description': data.get('description', '')
}
new_id = generic_create('model_dimension', insert_data)
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
@dimension_bp.route('/<int:id>', methods=['PUT'])
def update_dimension(id):
"""更新评测维度"""
data = request.json
update_data = {}
if 'name' in data:
update_data['name'] = data['name']
if 'type' in data:
update_data['type'] = data['type']
if 'description' in data:
update_data['description'] = data['description']
if update_data:
generic_update('model_dimension', id, update_data)
return jsonify({'code': 0, 'message': '更新成功'})
@dimension_bp.route('/<int:id>', methods=['DELETE'])
def delete_dimension(id):
"""删除评测维度"""
generic_delete('model_dimension', id)
return jsonify({'code': 0, 'message': '删除成功'})

View File

@@ -116,7 +116,8 @@ def create_model_manage():
'name': data.get('name'), 'name': data.get('name'),
'type': data.get('type'), 'type': data.get('type'),
'model_source': data.get('model_source', 'local'), 'model_source': data.get('model_source', 'local'),
'description': data.get('description') 'description': data.get('description'),
'purpose': data.get('purpose', 'inference') # 默认推理用途
} }
if data.get('model_source') == 'local': if data.get('model_source') == 'local':
@@ -138,6 +139,17 @@ def update_model_manage(id):
return jsonify({'code': 0, 'message': '更新成功'}) return jsonify({'code': 0, 'message': '更新成功'})
@model_manage_bp.route('/<int:id>/purpose', methods=['PUT'])
def update_model_purpose(id):
"""更新模型用途"""
data = request.json
purpose = data.get('purpose')
if purpose not in ['training', 'inference', 'evaluation']:
return jsonify({'code': 1, 'message': '无效的用途类型'})
generic_update('model_manage', id, {'purpose': purpose})
return jsonify({'code': 0, 'message': '更新成功'})
@model_manage_bp.route('/<int:id>', methods=['DELETE']) @model_manage_bp.route('/<int:id>', methods=['DELETE'])
def delete_model_manage(id): def delete_model_manage(id):
"""删除模型""" """删除模型"""

View File

@@ -174,6 +174,17 @@ def init_database():
api_key VARCHAR(500), api_key VARCHAR(500),
model_name VARCHAR(255), model_name VARCHAR(255),
description TEXT, description TEXT,
purpose VARCHAR(50) DEFAULT 'inference',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
# 评测维度表
"""CREATE TABLE IF NOT EXISTS model_dimension (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(100),
description TEXT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP, create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""", ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
@@ -204,6 +215,13 @@ def init_database():
except Exception as e: except Exception as e:
print(f"{i+1} 创建失败: {e}") print(f"{i+1} 创建失败: {e}")
# 为已存在的 model_manage 表添加 purpose 列
try:
cursor.execute("ALTER TABLE model_manage ADD COLUMN purpose VARCHAR(50) DEFAULT 'inference'")
print(" model_manage 表添加 purpose 列成功")
except Exception as e:
print(f" model_manage 表 purpose 列处理: {e}")
# 插入默认管理员用户 # 插入默认管理员用户
cursor.execute("SELECT * FROM users WHERE username = 'admin'") cursor.execute("SELECT * FROM users WHERE username = 'admin'")
if not cursor.fetchone(): if not cursor.fetchone():
@@ -221,7 +239,14 @@ def init_database():
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = CONFIG['secret_key'] app.config['SECRET_KEY'] = CONFIG['secret_key']
CORS(app, resources={r"/api/*": {"origins": "*"}}) app.config['CORS_HEADERS'] = 'Content-Type'
CORS(app, resources={
r"/api/*": {
"origins": "*",
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"allow_headers": ["Content-Type", "Authorization"]
}
}, supports_credentials=False)
# 注册蓝图 # 注册蓝图
register_blueprints(app) register_blueprints(app)

View File

@@ -482,13 +482,23 @@
const displayText = textMap[val] || val || '-'; const displayText = textMap[val] || val || '-';
return '<span class="px-2 py-1 rounded text-xs bg-blue-100 text-blue-700">' + displayText + '</span>'; return '<span class="px-2 py-1 rounded text-xs bg-blue-100 text-blue-700">' + displayText + '</span>';
}}, }},
{ title: '用途', key: 'purpose', render: (val) => {
const purposeMap = {
'training': { text: '训练', class: 'bg-blue-100 text-blue-700' },
'inference': { text: '推理', class: 'bg-green-100 text-green-700' },
'evaluation': { text: '评测', class: 'bg-purple-100 text-purple-700' }
};
const display = purposeMap[val] || { text: val || '-', class: 'bg-gray-100 text-gray-700' };
return '<span class="px-2 py-1 rounded text-xs ' + display.class + '">' + display.text + '</span>';
}},
{ title: '模型来源', key: 'model_source', render: (val) => { { title: '模型来源', key: 'model_source', render: (val) => {
const textMap = { const textMap = {
'local': '本地模型', 'local': '本地模型',
'api': '在线模型',
'online': '在线模型' 'online': '在线模型'
}; };
const displayText = textMap[val] || val || '-'; const displayText = textMap[val] || val || '-';
return '<span class="px-2 py-1 rounded text-xs bg-green-100 text-green-700">' + displayText + '</span>'; return '<span class="px-2 py-1 rounded text-xs bg-gray-100 text-gray-700">' + displayText + '</span>';
}}, }},
{ title: '描述', key: 'description', render: (val) => val || '-' }, { title: '描述', key: 'description', render: (val) => val || '-' },
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' } { title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
@@ -778,6 +788,29 @@
}); });
} }
// 更新模型用途
async function updateModelPurpose(id, purpose) {
try {
const response = await fetch(`${API_BASE}/model-manage/${id}/purpose`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ purpose })
});
const result = await response.json();
if (result.code === 0) {
// 刷新当前页面
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
if (activeLink) {
loadPage(activeLink.dataset.page);
}
} else {
showMessage('错误', result.message || '更新失败', 'error');
}
} catch (error) {
showMessage('错误', '更新失败: ' + error.message, 'error');
}
}
// 切换单个项的选中状态 // 切换单个项的选中状态
function toggleItemSelection(id, api) { function toggleItemSelection(id, api) {
if (selectedItems.has(id)) { if (selectedItems.has(id)) {
@@ -2229,8 +2262,8 @@
} }
// ============ 自定义消息弹窗 ============ // ============ 自定义消息弹窗 ============
// 显示消息弹窗 // 显示消息弹窗 - 使用 window 确保全局可访问
function showMessage(title, message, type = 'info', onConfirm) { window.showMessage = function(title, message, type = 'info', onConfirm) {
const modal = document.getElementById('customModal'); const modal = document.getElementById('customModal');
const modalTitle = document.getElementById('modalTitle'); const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage'); const modalMessage = document.getElementById('modalMessage');
@@ -2263,7 +2296,7 @@
modalSingleBtnGroup.classList.remove('hidden'); modalSingleBtnGroup.classList.remove('hidden');
const confirmBtn = modalConfirmBtn2; const confirmBtn = modalConfirmBtn2;
if (type === 'error') { if (type === 'error') {
confirmBtn.className = 'px-6 py-2 bg-danger text-white rounded-lg hover:bg-danger/90 transition-colors'; confirmBtn.className = 'px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors';
} else { } else {
confirmBtn.className = 'px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors'; confirmBtn.className = 'px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors';
} }
@@ -2272,10 +2305,17 @@
modal.classList.remove('hidden'); modal.classList.remove('hidden');
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
// 绑定确认按钮事件 // 保存回调到按钮属性
confirmBtn.onclick = () => { confirmBtn._onConfirm = onConfirm;
// 使用 function 而不是箭头函数
confirmBtn.onclick = function() {
closeModal(); closeModal();
if (onConfirm) onConfirm(); const callback = this._onConfirm;
if (typeof callback === 'function') {
callback();
}
this._onConfirm = null;
}; };
} }
@@ -2286,8 +2326,8 @@
document.body.style.overflow = ''; document.body.style.overflow = '';
} }
// 确认弹窗(两个按钮) // 确认弹窗(两个按钮)- 使用 window 确保全局可访问
function showConfirm(title, message, onConfirm, onCancel) { window.showConfirm = function(title, message, onConfirm, onCancel, type = 'info') {
const modal = document.getElementById('customModal'); const modal = document.getElementById('customModal');
const modalTitle = document.getElementById('modalTitle'); const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage'); const modalMessage = document.getElementById('modalMessage');
@@ -2296,9 +2336,20 @@
const modalCancelBtn = document.getElementById('modalCancelBtn'); const modalCancelBtn = document.getElementById('modalCancelBtn');
const modalBtnGroup = document.getElementById('modalBtnGroup'); const modalBtnGroup = document.getElementById('modalBtnGroup');
if (!modalConfirmBtn) {
console.error('modalConfirmBtn not found');
return;
}
modalTitle.textContent = title; modalTitle.textContent = title;
modalMessage.innerHTML = message; modalMessage.innerHTML = message;
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-blue-100 flex items-center justify-center"><i class="fa fa-question text-xl text-blue-600"></i></div>';
// 根据类型设置图标
if (type === 'warning') {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-yellow-100 flex items-center justify-center"><i class="fa fa-exclamation text-xl text-yellow-600"></i></div>';
} else {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-blue-100 flex items-center justify-center"><i class="fa fa-question text-xl text-blue-600"></i></div>';
}
modalBtnGroup.classList.remove('hidden'); modalBtnGroup.classList.remove('hidden');
modalConfirmBtn.textContent = '确定'; modalConfirmBtn.textContent = '确定';
@@ -2307,14 +2358,29 @@
modal.classList.remove('hidden'); modal.classList.remove('hidden');
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
modalConfirmBtn.onclick = () => { // 保存回调到按钮属性
modalConfirmBtn._onConfirm = onConfirm;
modalConfirmBtn._onCancel = onCancel;
// 使用 function 而不是箭头函数,确保 this 指向正确
modalConfirmBtn.onclick = function() {
closeModal(); closeModal();
if (onConfirm) onConfirm(); const callback = this._onConfirm;
if (typeof callback === 'function') {
callback();
}
this._onConfirm = null;
this._onCancel = null;
}; };
modalCancelBtn.onclick = () => { modalCancelBtn.onclick = function() {
closeModal(); closeModal();
if (onCancel) onCancel(); const callback = this._onCancel || modalConfirmBtn._onCancel;
if (typeof callback === 'function') {
callback();
}
modalConfirmBtn._onConfirm = null;
modalConfirmBtn._onCancel = null;
}; };
} }
@@ -2382,6 +2448,29 @@
window.location.href = 'model-dimension-create.html'; window.location.href = 'model-dimension-create.html';
} }
// 删除评测维度
async function deleteDimension(id) {
showConfirm('确认删除', '确定要删除此评测维度吗?', async () => {
try {
const response = await fetch(`${API_BASE}/dimension/${id}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.code === 0) {
showMessage('成功', '删除成功', 'success', () => {
// 刷新维度列表 - 切换到 dimensions tab
switchTab(document.querySelector('[data-tab="dimensions"]'), 'dimensions');
});
} else {
showMessage('错误', result.message || '删除失败', 'error');
}
} catch (error) {
console.error('删除维度失败:', error);
showMessage('错误', '删除失败: ' + error.message, 'error');
}
});
}
// 切换 Tab // 切换 Tab
function switchTab(btn, tabId) { function switchTab(btn, tabId) {
// 更新按钮状态 // 更新按钮状态

View File

@@ -337,12 +337,8 @@
<label class="form-label">选择大模型 <span class="text-red-500">*</span></label> <label class="form-label">选择大模型 <span class="text-red-500">*</span></label>
<select name="eval_model" id="evalModel" class="form-select"> <select name="eval_model" id="evalModel" class="form-select">
<option value="">请选择评估使用的大模型</option> <option value="">请选择评估使用的大模型</option>
<option value="gpt-4">GPT-4</option>
<option value="claude-3">Claude-3</option>
<option value="ernie">文心一言</option>
<option value="qwen">通义千问</option>
<option value="chatglm">ChatGLM</option>
</select> </select>
<p class="text-xs text-gray-400 mt-1">请在模型管理中配置"评测模型"用途的模型</p>
</div> </div>
<div> <div>
<label class="form-label">评估方式 <span class="text-red-500">*</span></label> <label class="form-label">评估方式 <span class="text-red-500">*</span></label>
@@ -462,6 +458,35 @@
}; };
const API_BASE = getApiBase(); const API_BASE = getApiBase();
// 加载评测模型列表(用途为 evaluation 的模型)
async function loadEvalModels() {
try {
const response = await fetch(`${API_BASE}/model-manage`);
const result = await response.json();
if (result.code === 0 && result.data) {
const evalModels = result.data.filter(m => m.purpose === 'evaluation');
const select = document.getElementById('evalModel');
if (evalModels.length === 0) {
select.innerHTML = '<option value="">暂无可用的评测模型</option>';
return;
}
select.innerHTML = '<option value="">请选择评估使用的大模型</option>';
evalModels.forEach(model => {
const option = document.createElement('option');
option.value = model.id;
option.textContent = `${model.name} (${model.model_source === 'api' ? 'API' : '本地'})`;
select.appendChild(option);
});
}
} catch (error) {
console.error('加载评测模型列表失败:', error);
document.getElementById('evalModel').innerHTML = '<option value="">加载失败</option>';
}
}
// 返回列表页 // 返回列表页
function goBack() { function goBack() {
window.location.href = 'main.html?page=model-eval'; window.location.href = 'main.html?page=model-eval';
@@ -935,13 +960,16 @@
}; };
// 初始化函数 // 初始化函数
function initPage() { async function initPage() {
// 绑定指标类型下拉框事件 // 绑定指标类型下拉框事件
const dimensionType = document.getElementById('dimensionType'); const dimensionType = document.getElementById('dimensionType');
if (dimensionType) { if (dimensionType) {
dimensionType.addEventListener('change', toggleEvalConfig); dimensionType.addEventListener('change', toggleEvalConfig);
} }
// 加载评测模型列表
await loadEvalModels();
// 绑定 Markdown 编辑器事件 // 绑定 Markdown 编辑器事件
const evalPromptEditor = document.getElementById('evalPromptEditor'); const evalPromptEditor = document.getElementById('evalPromptEditor');
if (evalPromptEditor) { if (evalPromptEditor) {
@@ -1076,13 +1104,14 @@
const name = formData.get('name').trim(); const name = formData.get('name').trim();
const type = formData.get('type').trim(); const type = formData.get('type').trim();
// 验证
if (!name) { if (!name) {
alert('请输入维度名称'); showMessage('提示', '请输入维度名称', 'warning');
return; return;
} }
if (!type) { if (!type) {
alert('请选择指标类型'); showMessage('提示', '请选择指标类型', 'warning');
return; return;
} }
@@ -1093,22 +1122,22 @@
const evalMethod = formData.get('eval_method'); const evalMethod = formData.get('eval_method');
if (!evalModel) { if (!evalModel) {
alert('请选择评估使用的大模型'); showMessage('提示', '请选择评估使用的大模型', 'warning');
return; return;
} }
if (!evalPrompt) { if (!evalPrompt) {
alert('请输入评估 Prompt'); showMessage('提示', '请输入评估 Prompt', 'warning');
return; return;
} }
if (!evalMethod) { if (!evalMethod) {
alert('请选择评估方式'); showMessage('提示', '请选择评估方式', 'warning');
return; return;
} }
} else if (type === 'text_similarity') { } else if (type === 'text_similarity') {
// 规则评估:获取所有选中的评估方式 // 规则评估:获取所有选中的评估方式
const checkedMethods = Array.from(document.querySelectorAll('input[name="eval_method"]:checked')).map(cb => cb.value); const checkedMethods = Array.from(document.querySelectorAll('input[name="eval_method"]:checked')).map(cb => cb.value);
if (checkedMethods.length === 0) { if (checkedMethods.length === 0) {
alert('请至少选择一个评估方式'); showMessage('提示', '请至少选择一个评估方式', 'warning');
return; return;
} }
} }
@@ -1147,22 +1176,92 @@
} }
try { try {
const response = await fetch(`${API_BASE}/model-eval/dimension`, { const response = await fetch(`${API_BASE}/dimension`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data) body: JSON.stringify(data)
}); });
const result = await response.json(); const result = await response.json();
if (result.code === 0) { if (result.code === 0) {
alert('评测维度创建成功!'); showMessage('成功', '评测维度创建成功!', 'success', () => {
goBack(); goBack();
});
} else { } else {
alert('创建失败: ' + (result.message || '未知错误')); showMessage('错误', result.message || '创建失败', 'error');
} }
} catch (error) { } catch (error) {
alert('创建失败: ' + error.message); showMessage('错误', '创建失败: ' + error.message, 'error');
} }
} }
// ============ 自定义消息弹窗 ============
function showMessage(title, message, type = 'info', onConfirm) {
const modal = document.getElementById('customModal');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
const modalIcon = document.getElementById('modalIcon');
const modalConfirmBtn = document.getElementById('modalConfirmBtn2');
const modalBtnGroup = document.getElementById('modalBtnGroup');
const modalSingleBtnGroup = document.getElementById('modalSingleBtnGroup');
if (!modalConfirmBtn) {
console.error('modalConfirmBtn2 not found');
return;
}
modalTitle.textContent = title;
modalMessage.innerHTML = message;
if (type === 'success') {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-green-100 flex items-center justify-center"><i class="fa fa-check text-xl text-green-600"></i></div>';
} else if (type === 'error') {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-red-100 flex items-center justify-center"><i class="fa fa-times text-xl text-red-600"></i></div>';
} else if (type === 'warning') {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-yellow-100 flex items-center justify-center"><i class="fa fa-exclamation text-xl text-yellow-600"></i></div>';
} else {
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-blue-100 flex items-center justify-center"><i class="fa fa-info text-xl text-blue-600"></i></div>';
}
// 隐藏双按钮组,显示单按钮组
if (modalBtnGroup) modalBtnGroup.classList.add('hidden');
modalSingleBtnGroup.classList.remove('hidden');
modalConfirmBtn.className = type === 'error' ? 'px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors' : 'px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors';
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
// 保存回调到按钮属性
modalConfirmBtn._onConfirm = onConfirm;
// 使用 function 确保 this 指向正确
modalConfirmBtn.onclick = function() {
modal.classList.add('hidden');
document.body.style.overflow = '';
const callback = this._onConfirm;
if (typeof callback === 'function') {
callback();
}
this._onConfirm = null;
};
}
</script> </script>
<!-- 自定义消息弹窗 -->
<div id="customModal" class="hidden fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
<div class="bg-white rounded-xl shadow-xl max-w-md w-full mx-4 overflow-hidden">
<div class="p-6 text-center">
<div id="modalIcon"></div>
<h3 id="modalTitle" class="text-lg font-medium text-gray-800 mb-2"></h3>
<p id="modalMessage" class="text-gray-600 text-sm"></p>
</div>
<div id="modalBtnGroup" class="hidden px-6 pb-6 flex justify-center space-x-4">
<button id="modalCancelBtn" class="px-6 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors">取消</button>
<button id="modalConfirmBtn" class="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors">确定</button>
</div>
<div id="modalSingleBtnGroup" class="px-6 pb-6 flex justify-center">
<button id="modalConfirmBtn2" class="px-6 py-2 text-white rounded-lg transition-colors">确定</button>
</div>
</div>
</div>
</body> </body>
</html> </html>

View File

@@ -114,6 +114,10 @@
activeContent.classList.remove('hidden'); activeContent.classList.remove('hidden');
activeContent.classList.add('active'); activeContent.classList.add('active');
} }
// 切换到评测维度tab时刷新数据
if (tabId === 'dimensions') {
loadDimensions();
}
}; };
// 加载评测任务数据 // 加载评测任务数据
@@ -195,38 +199,49 @@
`).join(''); `).join('');
} }
// 加载评测维度数据(模拟数据) // 加载评测维度数据
async function loadDimensions() { async function loadDimensions() {
// 模拟评测维度数据 try {
const mockData = [ const response = await fetch(`${API_BASE}/dimension`);
{ id: 1, name: '准确率', type: 'accuracy', desc: '模型预测正确的比例', status: '启用' }, const result = await response.json();
{ id: 2, name: '召回率', type: 'recall', desc: '找出所有正例的能力', status: '启用' },
{ id: 3, name: 'F1分数', type: 'f1', desc: '准确率和召回率的调和平均', status: '启用' },
{ id: 4, name: '推理速度', type: 'speed', desc: '模型推理响应时间', status: '启用' },
{ id: 5, name: 'BLEU分数', type: 'bleu', desc: '机器翻译质量评估', status: '停用' }
];
const tbody = document.getElementById('dimensionsBody'); const tbody = document.getElementById('dimensionsBody');
tbody.innerHTML = mockData.map(item => `
<tr class="hover:bg-gray-50"> if (result.code !== 0 || !result.data || result.data.length === 0) {
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${item.name}</td> tbody.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.type}</td> <tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.desc}</td> <td colspan="5" class="px-6 py-12 text-center text-gray-400">
<td class="px-6 py-4 whitespace-nowrap"> <i class="fa fa-inbox text-4xl mb-3"></i>
<span class="px-2 py-1 text-xs font-medium rounded-full ${item.status === '启用' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'}"> <p>暂无评测维度</p>
${item.status} </td>
</span> </tr>
</td> `;
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> return;
<button onclick="editDimension(${item.id})" class="text-primary hover:text-primary/80 mr-3"> }
<i class="fa fa-edit"></i> 编辑
</button> tbody.innerHTML = result.data.map(item => `
<button onclick="deleteDimension(${item.id})" class="text-red-500 hover:text-red-600"> <tr class="hover:bg-gray-50">
<i class="fa fa-trash"></i> 删除 <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${item.name || '-'}</td>
</button> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.type || '-'}</td>
</td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.description || '-'}</td>
</tr> <td class="px-6 py-4 whitespace-nowrap">
`).join(''); <span class="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-700">
启用
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<button onclick="editDimension(${item.id})" class="text-primary hover:text-primary/80 mr-3">
<i class="fa fa-edit"></i> 编辑
</button>
<button onclick="deleteDimension(${item.id})" class="text-red-500 hover:text-red-600">
<i class="fa fa-trash"></i> 删除
</button>
</td>
</tr>
`).join('');
} catch (error) {
console.error('加载评测维度失败:', error);
}
} }
// 状态样式 // 状态样式
@@ -267,9 +282,36 @@
alert('编辑维度功能开发中'); alert('编辑维度功能开发中');
} }
function deleteDimension(id) { async function deleteDimension(id) {
if (confirm('确定要删除此维度吗?')) { showConfirm('确认删除', '确定要删除此评测维度吗?', () => {
alert('删除维度功能开发中'); executeDelete(id);
});
}
async function executeDelete(id) {
try {
const response = await fetch(`${API_BASE}/dimension/${id}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.code === 0) {
// 切换到评测维度tab并刷新列表
switchToDimensionsTab();
showMessage('成功', '删除成功', 'success');
} else {
showMessage('错误', result.message || '删除失败', 'error');
}
} catch (error) {
console.error('删除维度失败:', error);
showMessage('错误', '删除失败: ' + error.message, 'error');
}
}
// 切换到评测维度tab
function switchToDimensionsTab() {
const dimBtn = document.querySelector('[data-tab="dimensions"]');
if (dimBtn) {
dimBtn.click();
} }
} }

View File

@@ -204,7 +204,7 @@
<!-- 基本信息 --> <!-- 基本信息 -->
<div class="mb-6"> <div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">基本信息</h3> <h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">基本信息</h3>
<div class="grid grid-cols-2 gap-4"> <div class="max-w-xl">
<div> <div>
<label class="form-label"> <label class="form-label">
<span class="text-red-500 mr-1">*</span>模型名称 <span class="text-red-500 mr-1">*</span>模型名称
@@ -212,7 +212,7 @@
<input type="text" name="name" class="form-input" placeholder="请输入模型名称" maxlength="100"> <input type="text" name="name" class="form-input" placeholder="请输入模型名称" maxlength="100">
<p class="text-xs text-gray-400 mt-1">支持中文、英文、数字、下划线最多100个字符</p> <p class="text-xs text-gray-400 mt-1">支持中文、英文、数字、下划线最多100个字符</p>
</div> </div>
<div> <div class="mt-4">
<label class="form-label"> <label class="form-label">
<span class="text-red-500 mr-1">*</span>模型类型 <span class="text-red-500 mr-1">*</span>模型类型
</label> </label>
@@ -225,6 +225,26 @@
</select> </select>
</div> </div>
</div> </div>
<!-- 模型用途 -->
<div class="mt-8 mb-6">
<label class="form-label flex items-center mb-3">
<span class="text-red-500 mr-1">*</span>模型用途
</label>
<div class="flex items-center space-x-6">
<label class="flex items-center cursor-pointer">
<input type="radio" name="purpose" value="training" class="mr-2">
<span class="text-sm">训练基座模型</span>
</label>
<label class="flex items-center cursor-pointer">
<input type="radio" name="purpose" value="inference" class="mr-2" checked>
<span class="text-sm">推理对比模型</span>
</label>
<label class="flex items-center cursor-pointer">
<input type="radio" name="purpose" value="evaluation" class="mr-2">
<span class="text-sm">评测模型</span>
</label>
</div>
</div>
</div> </div>
<div class="mb-6"> <div class="mb-6">
@@ -430,6 +450,12 @@
const descInput = document.querySelector('textarea[name="description"]'); const descInput = document.querySelector('textarea[name="description"]');
descInput.value = model.description || ''; descInput.value = model.description || '';
document.getElementById('descCount').textContent = descInput.value.length; document.getElementById('descCount').textContent = descInput.value.length;
// 填充用途(兼容旧数据没有 purpose 字段的情况)
const purpose = model.purpose || 'inference';
document.querySelectorAll('input[name="purpose"]').forEach(radio => {
radio.checked = radio.value === purpose;
});
} }
} catch (error) { } catch (error) {
console.error('加载模型数据失败:', error); console.error('加载模型数据失败:', error);
@@ -458,13 +484,15 @@
const form = document.getElementById('modelForm'); const form = document.getElementById('modelForm');
const formData = new FormData(form); const formData = new FormData(form);
const modelSource = formData.get('model_source'); const modelSource = formData.get('model_source');
console.log('modelSource:', modelSource); const purpose = formData.get('purpose');
console.log('modelSource:', modelSource, 'purpose:', purpose);
const data = { const data = {
name: formData.get('name'), name: formData.get('name'),
type: formData.get('type'), type: formData.get('type'),
model_source: modelSource, model_source: modelSource,
description: formData.get('description') description: formData.get('description'),
purpose: purpose
}; };
// 根据模型来源设置不同的字段 // 根据模型来源设置不同的字段

286
web/pages/model-manage.html Normal file
View File

@@ -0,0 +1,286 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模型管理 / 远光软件微调平台</title>
<script src="../lib/tailwindcss/tailwind.js"></script>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<style>
.bg-primary { background-color: #1890ff; }
.text-primary { color: #1890ff; }
.border-primary { border-color: #1890ff; }
:root { --primary: #1890ff; }
.form-input {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
line-height: 1.25rem;
color: #1f2937;
background-color: #fff;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
transition: border-color 0.15s ease-in-out;
}
.form-input:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.1);
}
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
margin-bottom: 0.25rem;
}
.tab-btn {
padding: 8px 20px !important;
font-size: 14px !important;
font-weight: 500 !important;
border-radius: 6px !important;
border: 1px solid transparent !important;
cursor: pointer !important;
background: transparent !important;
color: #4b5563 !important;
transition: all 0.2s !important;
min-width: 100px;
}
.tab-btn:hover {
background: white !important;
border-color: #d1d5db !important;
}
.tab-btn.tab-active {
background: white !important;
color: #1890ff !important;
border-color: #1890ff !important;
box-shadow: 0 1px 3px rgba(24,144,255,0.2) !important;
}
</style>
</head>
<body class="antialiased bg-gray-50">
<!-- Tab 导航 -->
<div style="background: #fff; border-bottom: 1px solid #e5e7eb; padding: 16px;">
<h2 style="font-size: 18px; font-weight: 600; color: #1f2937; margin-bottom: 12px;">模型管理</h2>
<div style="display: flex; gap: 8px; background: #f3f4f6; padding: 6px; border-radius: 8px;">
<button onclick="switchTab('all')" id="tab-all" class="tab-btn tab-active" style="display: inline-flex; align-items: center; justify-content: center;">
全部模型
</button>
<button onclick="switchTab('training')" id="tab-training" class="tab-btn" style="display: inline-flex; align-items: center; justify-content: center;">
训练基座
</button>
<button onclick="switchTab('inference')" id="tab-inference" class="tab-btn" style="display: inline-flex; align-items: center; justify-content: center;">
推理对比
</button>
<button onclick="switchTab('evaluation')" id="tab-evaluation" class="tab-btn" style="display: inline-flex; align-items: center; justify-content: center;">
评测模型
</button>
</div>
</div>
<!-- 表格容器 -->
<div style="padding: 16px;">
<!-- 工具栏 -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<div style="display: flex; align-items: center; gap: 16px;">
<input type="text" id="searchInput" placeholder="搜索模型名称..." style="width: 256px; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px;" oninput="filterModels()">
</div>
<button onclick="window.location.href='model-manage-create.html'" style="padding: 8px 16px; background: #1890ff; color: white; border: none; border-radius: 6px; cursor: pointer; display: flex; align-items: center; font-size: 14px;">
<span style="margin-right: 8px;">+</span>添加模型
</button>
</div>
<!-- 模型表格 -->
<div class="bg-white rounded-lg shadow-sm">
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">模型名称</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">模型类型</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用途</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">模型来源</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">描述</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">创建时间</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody id="modelsBody" class="bg-white divide-y divide-gray-200">
<!-- 动态加载 -->
</tbody>
</table>
</div>
<!-- 空状态 -->
<div id="emptyState" class="hidden px-6 py-12 text-center">
<i class="fa fa-inbox text-4xl text-gray-300 mb-3"></i>
<p class="text-gray-500">暂无模型数据</p>
</div>
</div>
</div>
<script>
// 动态获取 API 基础地址
const getApiBase = () => {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
return `${protocol}//${hostname}:8080/api`;
};
const API_BASE = getApiBase();
let allModels = [];
let currentTab = 'all';
// Tab 切换
function switchTab(tab) {
currentTab = tab;
// 更新按钮样式
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('tab-active');
});
const activeTab = document.getElementById(`tab-${tab}`);
activeTab.classList.add('tab-active');
renderModels();
}
// 加载模型数据
async function loadModels() {
try {
const response = await fetch(`${API_BASE}/model-manage`);
const result = await response.json();
if (result.code === 0) {
allModels = result.data || [];
renderModels();
}
} catch (error) {
console.error('加载模型失败:', error);
}
}
// 筛选模型
function filterModels() {
renderModels();
}
// 渲染模型列表
function renderModels() {
const searchValue = document.getElementById('searchInput').value.toLowerCase();
let filteredModels = allModels;
// 按 Tab 筛选
if (currentTab !== 'all') {
filteredModels = filteredModels.filter(m => m.purpose === currentTab);
}
// 按搜索关键词筛选
if (searchValue) {
filteredModels = filteredModels.filter(m =>
m.name?.toLowerCase().includes(searchValue) ||
m.description?.toLowerCase().includes(searchValue)
);
}
const tbody = document.getElementById('modelsBody');
const emptyState = document.getElementById('emptyState');
if (filteredModels.length === 0) {
tbody.innerHTML = '';
emptyState.classList.remove('hidden');
return;
}
emptyState.classList.add('hidden');
tbody.innerHTML = filteredModels.map(item => {
// 模型类型
const typeMap = {
'LLM': '大语言模型',
'CV': '计算机视觉',
'NLP': '自然语言处理',
'Embedding': '向量模型',
'Other': '其他'
};
const typeDisplay = typeMap[item.type] || item.type || '-';
// 用途
const purposeMap = {
'training': { text: '训练', class: 'bg-blue-100 text-blue-700' },
'inference': { text: '推理', class: 'bg-green-100 text-green-700' },
'evaluation': { text: '评测', class: 'bg-purple-100 text-purple-700' }
};
const purposeDisplay = purposeMap[item.purpose] || purposeMap['inference'];
// 模型来源
const sourceMap = {
'local': '本地模型',
'api': '在线模型',
'online': '在线模型'
};
const sourceDisplay = sourceMap[item.model_source] || item.model_source || '-';
return `
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">${item.name || '-'}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 text-xs font-medium rounded bg-blue-100 text-blue-700">${typeDisplay}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 text-xs font-medium rounded ${purposeDisplay.class}">${purposeDisplay.text}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 text-xs font-medium rounded bg-gray-100 text-gray-700">${sourceDisplay}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-500 max-w-xs truncate">${item.description || '-'}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
${item.create_time ? new Date(item.create_time).toLocaleString('zh-CN') : '-'}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<button onclick="editModel(${item.id})" class="text-primary hover:text-primary/80 mr-3">
<i class="fa fa-edit"></i> 编辑
</button>
<button onclick="deleteModel(${item.id})" class="text-red-500 hover:text-red-600">
<i class="fa fa-trash"></i> 删除
</button>
</td>
</tr>
`;
}).join('');
}
// 编辑模型
function editModel(id) {
window.location.href = `model-manage-create.html?id=${id}`;
}
// 删除模型
async function deleteModel(id) {
if (!confirm('确定要删除此模型吗?')) return;
try {
const response = await fetch(`${API_BASE}/model-manage/${id}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.code === 0) {
loadModels();
} else {
alert('删除失败: ' + result.message);
}
} catch (error) {
console.error('删除模型失败:', error);
alert('删除失败');
}
}
// 页面加载时初始化
loadModels();
</script>
</body>
</html>