1 .修改了新建评估指标删除的bug
This commit is contained in:
@@ -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:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
144
src/api/dimension.py
Normal 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': '删除成功'})
|
||||||
@@ -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):
|
||||||
"""删除模型"""
|
"""删除模型"""
|
||||||
|
|||||||
27
src/main.py
27
src/main.py
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
// 更新按钮状态
|
// 更新按钮状态
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
286
web/pages/model-manage.html
Normal 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>
|
||||||
Reference in New Issue
Block a user