736 lines
35 KiB
HTML
736 lines
35 KiB
HTML
<!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>
|
||
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
|
||
<style>
|
||
.sidebar-section-title {
|
||
padding: 0.5rem 1rem;
|
||
font-size: 0.75rem;
|
||
color: rgba(191, 203, 217, 0.7);
|
||
font-weight: 500;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
.nav-link:hover {
|
||
background-color: rgba(0, 21, 41, 0.2);
|
||
}
|
||
.sidebar-item-active {
|
||
background-color: rgba(24, 144, 255, 0.1);
|
||
color: #1890ff;
|
||
border-left: 4px solid #1890ff;
|
||
}
|
||
.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">
|
||
<!-- 侧边导航 -->
|
||
<aside class="w-64 text-[#bfcbd9] flex-shrink-0 hidden md:block flex flex-col h-full" style="background-color: #001529;">
|
||
<!-- 平台LOGO区域 -->
|
||
<div class="pt-5 pb-3 border-b border-[#001529]/30 flex items-center justify-center pl-2">
|
||
<img src="../assets/logo/logo.png" alt="Logo" class="w-8 h-8 object-contain mr-2">
|
||
<span class="text-white font-medium text-base">远光软件微调平台</span>
|
||
</div>
|
||
|
||
<!-- 导航主区域 -->
|
||
<nav class="flex-1 overflow-y-auto py-2 relative">
|
||
<!-- 第一分区:模型服务 -->
|
||
<div class="sidebar-section-title">模型服务</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="#" data-page="fine-tune" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-cogs w-5 text-center"></i>
|
||
<span class="ml-2">模型调优</span>
|
||
</a>
|
||
</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="main.html?page=my-models" data-page="my-models" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-database w-5 text-center"></i>
|
||
<span class="ml-2">我的模型</span>
|
||
</a>
|
||
</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="#" data-page="model-eval" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-line-chart w-5 text-center"></i>
|
||
<span class="ml-2">模型评测</span>
|
||
</a>
|
||
</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="#" data-page="model-compare" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-server w-5 text-center"></i>
|
||
<span class="ml-2">模型对比</span>
|
||
</a>
|
||
</div>
|
||
|
||
<!-- 第二分区:资源管理 -->
|
||
<div class="sidebar-section-title mt-6">资源管理</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="#" data-page="model-manage" class="nav-link sidebar-item-active flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-cube w-5 text-center"></i>
|
||
<span class="ml-2">模型管理</span>
|
||
</a>
|
||
</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="#" data-page="dataset-manage" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-file-text w-5 text-center"></i>
|
||
<span class="ml-2">数据集管理</span>
|
||
</a>
|
||
</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="#" data-page="data-generate" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-database w-5 text-center"></i>
|
||
<span class="ml-2">其他工具</span>
|
||
</a>
|
||
</div>
|
||
|
||
<!-- 第三分区:系统设置 -->
|
||
<div class="sidebar-section-title mt-6">系统设置</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="#" data-page="config" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-bar-chart w-5 text-center"></i>
|
||
<span class="ml-2">平台性能</span>
|
||
</a>
|
||
</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="#" data-page="logs" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-file-text w-5 text-center"></i>
|
||
<span class="ml-2">查看日志</span>
|
||
</a>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- 底部信息区域 -->
|
||
<div class="p-4 border-t border-[#001529]/30 text-xs mt-auto">
|
||
<div class="mb-2 text-[#bfcbd9]/80">默认业务空间</div>
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-[#bfcbd9]">版本 v1.0.0</span>
|
||
<i class="fa fa-question-circle-o text-[#bfcbd9]/70"></i>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- 主内容区 -->
|
||
<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>
|