Files
YG_FT_Platform/web/pages/model-manage.html

643 lines
30 KiB
HTML
Raw Normal View History

<!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>
<script>
// 禁用 Tailwind 开发模式警告
if (typeof console !== 'undefined' && console.warn) {
const originalWarn = console.warn;
console.warn = function(...args) {
if (args[0] && args[0].includes && args[0].includes('cdn.tailwindcss.com')) {
return;
}
originalWarn.apply(console, args);
};
}
</script>
<script>
// 设置当前页面,供侧边栏高亮使用
window.sidebarCurrentPage = 'model-manage';
</script>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<!-- 侧边栏加载器 -->
<script src="../js/components/sidebar-loader.js"></script>
<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 flex h-screen overflow-hidden">
<!-- 侧边栏容器 -->
<div id="sidebar-container"></div>
<div class="flex-1 flex flex-col overflow-hidden">
<!-- 顶部导航 -->
<header class="bg-white border-b border-gray-200 shadow-sm">
<div class="flex items-center justify-between px-6 h-14">
<div class="flex items-center space-x-6">
<button class="md:hidden text-gray-500 hover:text-gray-700">
<i class="fa fa-bars"></i>
</button>
</div>
<div class="flex items-center space-x-4">
<!-- 系统性能监控 -->
<a href="?page=config" class="flex items-center space-x-4 text-xs text-gray-500 hover:text-primary transition-colors">
<div class="flex items-center" title="CPU使用率">
<i class="fa fa-microchip mr-1 text-blue-500"></i>
<span id="cpuUsage">--</span>%
</div>
<div class="flex items-center" title="内存使用率">
<i class="fa fa-database mr-1 text-green-500"></i>
<span id="memUsage">--</span>%
</div>
<div class="flex items-center" title="磁盘使用率">
<i class="fa fa-hdd-o mr-1 text-orange-500"></i>
<span id="diskUsage">--</span>%
</div>
</a>
<div class="h-6 w-px bg-gray-200"></div>
<div class="relative group">
<img src="https://picsum.photos/id/1005/32/32" class="w-8 h-8 rounded-full cursor-pointer" alt="用户头像">
<div class="absolute right-0 top-full pt-2 hidden group-hover:block z-50">
<div class="bg-white rounded shadow-lg py-1 border border-gray-100 min-w-[140px]">
<a href="login.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 whitespace-nowrap">
<i class="fa fa-sign-out mr-1"></i>退出登录
</a>
</div>
</div>
</div>
</div>
</div>
</header>
<!-- 页面内容 -->
<main class="flex-1 overflow-y-auto">
<!-- 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>
<button onclick="switchTab('trained')" id="tab-trained" 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 id="modelsTableContainer" 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>
<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 id="trainedModelsContainer" class="hidden bg-white rounded-lg shadow-sm">
<div class="p-4 border-b border-gray-200">
<p class="text-sm text-gray-500">已训练模型存储在 /app/base/saves 目录下</p>
</div>
<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>
</tr>
</thead>
<tbody id="trainedModelsBody" class="bg-white divide-y divide-gray-200">
<!-- 动态加载 -->
</tbody>
</table>
</div>
<!-- 空状态 -->
<div id="trainedEmptyState" 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>
</main>
<script>
console.log('[DEBUG] model-manage.html 脚本开始加载');
// 使用 IIFE 避免全局变量污染
(function() {
console.log('[DEBUG] model-manage.html IIFE 开始执行');
// API 基础地址 - 优先使用 main.html 中定义的全局变量
const getApiBase = () => {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
return `${protocol}//${hostname}:7861/api`;
};
const API_BASE = typeof window.API_BASE !== 'undefined' ? window.API_BASE : getApiBase();
let allModels = [];
let trainedModels = [];
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');
// 显示/隐藏搜索框和添加按钮
const toolbar = document.querySelector('div[style*="justify-content: space-between"]');
if (toolbar) {
toolbar.style.display = tab === 'trained' ? 'none' : 'flex';
}
// 显示/隐藏表格容器
const modelsTable = document.getElementById('modelsTableContainer');
const trainedModelsContainer = document.getElementById('trainedModelsContainer');
if (tab === 'trained') {
modelsTable.classList.add('hidden');
trainedModelsContainer.classList.remove('hidden');
loadTrainedModels();
} else {
modelsTable.classList.remove('hidden');
trainedModelsContainer.classList.add('hidden');
renderModels();
}
}
// 加载模型数据
async function loadModels() {
try {
// 并行加载数据库模型和已训练模型
const [dbResponse, trainedResponse] = await Promise.all([
fetch(`${API_BASE}/model-manage`),
fetch(`${API_BASE}/model-manage/trained-models`)
]);
const dbResult = await dbResponse.json();
const trainedResult = await trainedResponse.json();
console.log('[DEBUG] 数据库模型数量:', dbResult.data?.length || 0);
console.log('[DEBUG] 已训练模型API响应:', trainedResult);
// 数据库模型
let dbModels = [];
if (dbResult.code === 0) {
dbModels = dbResult.data || [];
}
// 已训练模型 - 转换为统一格式
trainedModels = [];
if (trainedResult.code === 0) {
const trainedData = trainedResult.data?.models || [];
console.log('[DEBUG] 已训练模型数据:', trainedData);
trainedData.forEach(model => {
// 每个训练方法作为一个模型条目
if (model.train_methods && model.train_methods.length > 0) {
model.train_methods.forEach(method => {
trainedModels.push({
id: `trained_${model.name}_${method.name}`.replace(/[^a-zA-Z0-9]/g, '_'),
name: `${model.name} (${method.name})`,
type: 'LLM',
purpose: 'inference',
model_source: 'local',
path: method.path,
description: `基于 ${model.name} 的${getMethodDisplayName(method.name)}训练模型`,
create_time: new Date().toISOString(),
isTrained: true,
baseModel: model.name,
trainMethod: method.name
});
});
} else {
// 没有训练方法的也添加为模型
trainedModels.push({
id: `trained_${model.name}`.replace(/[^a-zA-Z0-9]/g, '_'),
name: model.name,
type: 'LLM',
purpose: 'inference',
model_source: 'local',
path: model.path,
description: '已训练模型',
create_time: new Date().toISOString(),
isTrained: true,
baseModel: model.name,
trainMethod: ''
});
}
});
}
// 合并所有模型
allModels = [...dbModels, ...trainedModels];
console.log('[DEBUG] 数据库模型:', dbModels.length);
console.log('[DEBUG] 已训练模型:', trainedModels.length);
console.log('[DEBUG] 合并后的模型总数:', allModels.length);
renderModels();
} catch (error) {
console.error('加载模型失败:', error);
}
}
// 获取训练方法显示名称
function getMethodDisplayName(method) {
const methodMap = {
'lora': 'LoRA',
'qlora': 'QLoRA',
'full': '全量微调',
'prefix': 'Prefix Tuning',
'adapter': 'Adapter',
'lora_plus': 'LoRA+',
'peft': 'PEFT',
'adalora': 'AdaLoRA',
'longlora': 'LongLoRA'
};
return methodMap[method] || method;
}
// 加载已训练模型数据仅用于Trained Tab
async function loadTrainedModels() {
try {
const response = await fetch(`${API_BASE}/model-manage/trained-models`);
const result = await response.json();
console.log('[DEBUG] 已训练模型:', result);
if (result.code === 0) {
trainedModels = result.data?.models || [];
renderTrainedModels();
}
} catch (error) {
console.error('加载已训练模型失败:', error);
}
}
// 筛选模型
function filterModels() {
renderModels();
}
// 渲染模型列表
function renderModels() {
const searchInput = document.getElementById('searchInput');
const searchValue = searchInput ? 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 (!tbody) return;
if (filteredModels.length === 0) {
tbody.innerHTML = '';
if (emptyState) emptyState.classList.remove('hidden');
return;
}
if (emptyState) 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 || '-';
// 判断是否是已训练模型
const isTrained = item.isTrained === true;
const trainedBadge = isTrained ? '<span class="ml-2 px-2 py-0.5 text-xs font-medium rounded bg-green-100 text-green-700">已训练</span>' : '';
// 操作按钮
let actionButtons = '';
if (isTrained) {
// 已训练模型:显示查看和复制路径按钮
actionButtons = `
<button onclick="viewTrainedModel('${(item.path || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'")}')" class="text-primary hover:text-primary/80 mr-3">
<i class="fa fa-folder-open"></i> 查看
</button>
<button onclick="copyModelPath('${(item.path || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'")}')" class="text-gray-500 hover:text-gray-700">
<i class="fa fa-copy"></i> 复制路径
</button>
`;
} else {
// 普通模型:编辑和删除
actionButtons = `
<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>
`;
}
return `
<tr class="hover:bg-gray-50 ${isTrained ? 'bg-green-50/30' : ''}">
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">${item.name || '-'}${trainedBadge}</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" title="${item.description || ''}">${item.description || '-'}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-500 max-w-xs truncate ${isTrained ? 'font-mono text-green-600' : ''}" title="${item.path || ''}">${item.path ? (isTrained ? item.path.split('/').pop() : item.path) : '-'}</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">
${actionButtons}
</td>
</tr>
`;
}).join('');
}
// 渲染已训练模型列表
function renderTrainedModels() {
const tbody = document.getElementById('trainedModelsBody');
const emptyState = document.getElementById('trainedEmptyState');
// 收集所有训练方法
let allTrainMethods = [];
trainedModels.forEach(model => {
if (model.train_methods && model.train_methods.length > 0) {
model.train_methods.forEach(method => {
allTrainMethods.push({
baseModel: model.name,
trainMethod: method.name,
path: method.path
});
});
}
});
if (allTrainMethods.length === 0) {
tbody.innerHTML = '';
emptyState.classList.remove('hidden');
return;
}
emptyState.classList.add('hidden');
tbody.innerHTML = allTrainMethods.map(item => {
// 训练方法显示
const methodMap = {
'lora': 'LoRA',
'qlora': 'QLoRA',
'full': '全量微调',
'prefix': 'Prefix Tuning',
'adapter': 'Adapter'
};
const methodDisplay = methodMap[item.trainMethod] || item.trainMethod;
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.baseModel}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 text-xs font-medium rounded bg-green-100 text-green-700">${methodDisplay}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-500 max-w-xs truncate" title="${item.path}">${item.path}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<button onclick="viewTrainedModel('${item.path.replace(/\\/g, '\\\\')}')" class="text-primary hover:text-primary/80 mr-3">
<i class="fa fa-folder-open"></i> 查看
</button>
</td>
</tr>
`;
}).join('');
}
// 查看已训练模型
function viewTrainedModel(path) {
// 显示模型路径详情
const content = `
<div class="text-left">
<p class="mb-3 text-gray-600">模型路径:</p>
<div class="bg-gray-100 p-3 rounded text-sm font-mono break-all mb-3">${path}</div>
<p class="text-sm text-gray-500">您可以复制此路径用于推理或评测。</p>
</div>
`;
// 使用简单的提示框
showModal('模型详情', content);
}
// 复制模型路径
function copyModelPath(path) {
navigator.clipboard.writeText(path).then(() => {
showToast('模型路径已复制到剪贴板');
}).catch(() => {
// 降级方案
const textarea = document.createElement('textarea');
textarea.value = path;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showToast('模型路径已复制到剪贴板');
});
}
// 显示弹窗
function showModal(title, content) {
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
modal.innerHTML = `
<div class="bg-white rounded-lg p-6 max-w-lg w-full mx-4">
<h3 class="text-lg font-medium mb-4">${title}</h3>
<div class="mb-4">${content}</div>
<div class="flex justify-end">
<button onclick="this.closest('.fixed').remove()" class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300">关闭</button>
</div>
</div>
`;
document.body.appendChild(modal);
}
// 显示提示
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'fixed bottom-4 right-4 bg-gray-800 text-white px-4 py-2 rounded shadow-lg z-50';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2000);
}
// 编辑模型
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('删除失败');
}
}
// 页面加载时初始化
try {
loadModels();
} catch (e) {
console.error('初始化失败:', e);
}
})();
</script>
</body>
</html>