Files
YG_FT_Platform/web/pages/fine-tune-create.html
WIN-JHFT4D3SIVT\caoxiaozhu 513e96082c 重构了main.html的主函数
重构了大量的页面的sidebar
优化了代码结构
2026-02-02 09:22:52 +08:00

1302 lines
77 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html 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>
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 = 'fine-tune';
</script>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<style>
.form-input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
transition: border-color 0.2s, outline 0.2s;
}
.form-input:focus {
border-color: #1890ff;
outline: none;
}
.form-select {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
transition: border-color 0.2s, outline 0.2s;
appearance: none;
background-color: white;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
}
.form-select:focus {
border-color: #1890ff;
outline: none;
}
.card-radio {
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.card-radio.active {
border-color: #1890ff;
background-color: rgba(24, 144, 255, 0.05);
}
.card-radio:hover {
border-color: #d1d5db;
}
.bg-primary { background-color: #1890ff; }
.text-primary { color: #1890ff; }
.border-primary { border-color: #1890ff; }
:root { --primary: #1890ff; --danger: #f5222d; --success: #52c41a; }
</style>
<!-- 侧边栏加载器 -->
<script src="../js/components/sidebar-loader.js"></script>
</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-4">
<a href="main.html" class="text-gray-500 hover:text-gray-700 flex items-center">
<i class="fa fa-arrow-left"></i>
<span class="ml-1">上一步</span>
</a>
</div>
<div class="flex items-center space-x-4">
<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 mt-2 bg-white rounded shadow-lg py-1 hidden group-hover:block 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>
</header>
<!-- 内容区域 -->
<main class="flex-1 overflow-y-auto p-6 bg-gray-50">
<!-- 页面标题 -->
<div class="bg-white rounded-lg shadow-sm w-full p-4 border-b border-gray-100 mb-4">
<div class="flex items-center text-sm">
<span class="text-primary cursor-pointer hover:underline" onclick="window.location.href='main.html'">模型调优</span>
<span class="mx-2 text-gray-300">/</span>
<span class="text-gray-800 font-medium">创建训练任务</span>
</div>
</div>
<!-- 表单内容 -->
<div class="bg-white rounded-lg shadow-sm w-full">
<div class="p-6">
<form id="createForm">
<!-- 基本信息 -->
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">基本信息</h3>
<div class="mb-4">
<label class="block text-sm text-gray-600 mb-3">
任务名称
<span class="text-gray-400 text-xs ml-1">(英文、数字、下划线)</span>
</label>
<div>
<input type="text" name="name" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none" placeholder="请输入任务名称" maxlength="50">
<p class="text-xs text-gray-400 mt-1"><span id="nameCount">0</span> / 50</p>
<p id="nameFormatError" class="text-xs text-red-500 mt-1 hidden">任务名称只能包含英文、数字和下划线</p>
</div>
</div>
<div>
<label class="block text-sm text-gray-600 mb-3">任务描述</label>
<div>
<textarea name="description" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none resize-none" placeholder="请输入任务描述(选填)" maxlength="200" rows="3"></textarea>
<p class="text-xs text-gray-400 mt-1"><span id="descriptionCount">0</span> / 200</p>
</div>
</div>
</div>
<!-- 训练配置 -->
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">训练配置</h3>
<!-- GPU硬件选择 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">
<span class="text-red-500 mr-1">*</span>GPU硬件选择
</label>
<div id="gpuSelectionArea" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<!-- GPU列表将通过JS动态加载 -->
<div class="text-sm text-gray-400 col-span-full">正在加载GPU设备...</div>
</div>
<p class="text-xs text-gray-400 mt-2">支持选择多个GPU进行训练多卡训练</p>
</div>
<!-- 训练方式 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">训练方式</label>
<div class="grid grid-cols-3 gap-4">
<div class="card-radio active" data-value="SFT">
<div class="flex items-start">
<input type="radio" name="train_type" value="SFT" checked class="mt-1 mr-2">
<div>
<div class="font-medium text-sm">SFT 微调训练</div>
<div class="text-xs text-gray-400 mt-1">在监督指令下,增强模型指令跟随的能力,提升全参数微调训练方式</div>
</div>
</div>
</div>
<div class="card-radio" data-value="DPO">
<div class="flex items-start">
<input type="radio" name="train_type" value="DPO" class="mt-1 mr-2">
<div>
<div class="font-medium text-sm">DPO 偏好训练</div>
<div class="text-xs text-gray-400 mt-1">引入人类反馈,降低幻觉,使得模型输出更符合人类偏好</div>
</div>
</div>
</div>
<div class="card-radio" data-value="CPT">
<div class="flex items-start">
<input type="radio" name="train_type" value="CPT" class="mt-1 mr-2">
<div>
<div class="font-medium text-sm">CPT 继续预训练</div>
<div class="text-xs text-gray-400 mt-1">通过无标注数据进行无监督继续训练,强化或新增模型特定能力</div>
</div>
</div>
</div>
</div>
</div>
<!-- 选择模型 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">选择模型</label>
<div class="flex items-center">
<select name="base_model" id="baseModelSelect" class="form-select flex-1 max-w-md">
<option value="">请选择模型</option>
</select>
<button type="button" class="ml-2 text-primary text-sm flex items-center hover:text-primary/80" onclick="loadModels()">
<i class="fa fa-refresh"></i>
</button>
<button type="button" class="ml-3 bg-white border border-primary text-primary rounded px-3 py-1.5 text-sm hover:bg-primary/5" onclick="window.location.href='model-manage-create.html?from=fine-tune'">
+ 新增模型
</button>
</div>
</div>
<!-- 选择训练模板 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">
<span class="text-red-500 mr-1">*</span>训练模板
</label>
<select name="template" id="templateSelect" class="form-select flex-1 max-w-md">
<optgroup label="Qwen 系列">
<option value="qwen">qwen (Qwen/Qwen2)</option>
<option value="qwen3">qwen3 (Qwen3)</option>
<option value="qwen3_nothink">qwen3_nothink (Qwen3-Thinking)</option>
<option value="qwen2_vl">qwen2_vl (Qwen2-VL)</option>
<option value="qwen3_vl">qwen3_vl (Qwen3-VL)</option>
<option value="qwen2_audio">qwen2_audio (Qwen2-Audio)</option>
<option value="qwen2_omni">qwen2_omni (Qwen2.5-Omni)</option>
<option value="qwen3_omni">qwen3_omni (Qwen3-Omni)</option>
</optgroup>
<optgroup label="LLaMA 系列">
<option value="llama">llama (LLaMA)</option>
<option value="llama2">llama2 (LLaMA 2)</option>
<option value="llama3">llama3 (LLaMA 3/3.3)</option>
<option value="llama4">llama4 (LLaMA 4)</option>
<option value="mllama">mllama (LLaMA 3.2 Vision)</option>
<option value="llava">llava (LLaVA-1.5)</option>
<option value="llava_next">llava_next (LLaVA-NeXT)</option>
<option value="llava_next_video">llava_next_video (LLaVA-NeXT-Video)</option>
</optgroup>
<optgroup label="DeepSeek 系列">
<option value="deepseek">deepseek (DeepSeek LLM/Code/MoE)</option>
<option value="deepseek3">deepseek3 (DeepSeek 3-3.2)</option>
<option value="deepseekr1">deepseekr1 (DeepSeek R1 Distill)</option>
</optgroup>
<optgroup label="GLM 系列">
<option value="glm4">glm4 (GLM-4/GLM-4-0414/GLM-Z1)</option>
<option value="glm4_moe">glm4_moe (GLM-4.5)</option>
<option value="glm4_5v">glm4_5v (GLM-4.5V)</option>
</optgroup>
<optgroup label="Gemma 系列">
<option value="gemma">gemma (Gemma/Gemma 2/CodeGemma)</option>
<option value="gemma2">gemma2 (Gemma 2)</option>
<option value="gemma3">gemma3 (Gemma 3)</option>
<option value="gemma3n">gemma3n (Gemma 3n)</option>
</optgroup>
<optgroup label="Phi 系列">
<option value="phi">phi (Phi-3/Phi-3.5)</option>
<option value="phi_small">phi_small (Phi-3-small)</option>
<option value="phi4_mini">phi4_mini (Phi-4-mini)</option>
<option value="phi4">phi4 (Phi-4)</option>
</optgroup>
<optgroup label="InternLM 系列">
<option value="intern2">intern2 (InternLM 2-3)</option>
<option value="intern_vl">intern_vl (InternVL 2.5-3.5)</option>
<option value="intern_s1">intern_s1 (Intern-S1-mini)</option>
</optgroup>
<optgroup label="Mistral 系列">
<option value="mistral">mistral (Mistral/Mixtral)</option>
<option value="ministral3">ministral3 (Ministral 3)</option>
</optgroup>
<optgroup label="其他系列">
<option value="yi">yi (Yi)</option>
<option value="baichuan">baichuan (Baichuan)</option>
<option value="falcon">falcon (Falcon)</option>
<option value="falcon_h1">falcon_h1 (Falcon H1)</option>
<option value="pixtral">pixtral (Pixtral)</option>
<option value="paligemma">paligemma (PaliGemma)</option>
<option value="minicpm_o">minicpm_o (MiniCPM-o-2.6)</option>
<option value="minicpm_v">minicpm_v (MiniCPM-V-2.6)</option>
<option value="seed_oss">seed_oss (Seed OSS)</option>
<option value="seed_coder">seed_coder (Seed Coder)</option>
<option value="kimi_vl">kimi_vl (Kimi-VL)</option>
<option value="hunyuan">hunyuan (Hunyuan)</option>
<option value="hunyuan_small">hunyuan_small (Hunyuan1.5)</option>
<option value="granite3">granite3 (Granite 3)</option>
<option value="granite4">granite4 (Granite 3-4)</option>
<option value="mimo">mimo (MiMo)</option>
<option value="mimo_v2">mimo_v2 (MiMo V2)</option>
<option value="lfm2">lfm2 (LFM 2.5)</option>
<option value="lfm2_vl">lfm2_vl (LFM 2.5 VL)</option>
<option value="bailing_v2">bailing_v2 (Ling 2.0)</option>
<option value="yuan">yuan (Yuan 2)</option>
<option value="ernie_nothink">ernie_nothink (ERNIE-4.5)</option>
<option value="gpt_oss">gpt_oss (GPT-OSS)</option>
</optgroup>
</select>
<p class="text-xs text-gray-400 mt-1">选择与您的模型匹配的对话模板,确保训练数据格式正确</p>
</div>
<!-- 训练方法 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">训练方法</label>
<div class="flex items-center space-x-6">
<label class="flex items-center cursor-pointer">
<input type="radio" name="train_method" value="lora" class="mr-2" onchange="toggleTrainMethod()" checked>
<span class="text-sm">高效训练</span>
</label>
<label class="flex items-center cursor-pointer">
<input type="radio" name="train_method" value="full" class="mr-2" onchange="toggleTrainMethod()">
<span class="text-sm">全参训练</span>
</label>
</div>
</div>
<!-- 训练方法参数配置 -->
<div id="trainMethodParams" class="mb-6">
<div class="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden">
<div class="flex items-center justify-between px-4 py-3 bg-gray-50 border-b border-gray-200">
<div class="flex items-center">
<i class="fa fa-sliders text-primary mr-2"></i>
<label class="block text-sm font-medium text-gray-700">训练参数配置</label>
</div>
<div class="flex items-center space-x-3">
<button type="button" onclick="resetParams()" class="text-gray-500 hover:text-primary text-sm flex items-center transition-colors" title="恢复默认配置">
<i class="fa fa-rotate-left mr-1"></i>
<span>重置</span>
</button>
<button type="button" onclick="toggleParamsCollapse()" class="text-gray-500 hover:text-primary text-sm flex items-center transition-colors">
<span id="paramsToggleText">收起</span>
<i id="paramsToggleIcon" class="fa fa-chevron-up ml-1 text-xs transition-transform"></i>
</button>
</div>
</div>
<div id="paramsContent" class="overflow-x-auto">
<table class="w-full text-sm divide-y divide-gray-100">
<thead class="bg-gradient-to-r from-blue-50 to-indigo-50">
<tr>
<th class="text-left py-3 px-4 font-medium text-gray-600">参数名称</th>
<th class="text-left py-3 px-4 font-medium text-gray-600">参数值</th>
<th class="text-left py-3 px-4 font-medium text-gray-600">取值范围</th>
<th class="text-left py-3 px-4 font-medium text-gray-600">参数说明</th>
</tr>
</thead>
<tbody id="commonParamsBody" class="divide-y divide-gray-100">
<!-- 公共参数 -->
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">batch_size</span>
<span class="text-red-500 ml-1">*</span>
</td>
<td class="py-3 px-4">
<input type="number" name="batch_size" value="1" min="1" max="64" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[1, 64]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">单步训练样本数量,数值越大训练速度越快,但显存占用越高</td>
</tr>
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">learning_rate</span>
<span class="text-red-500 ml-1">*</span>
</td>
<td class="py-3 px-4">
<input type="number" name="learning_rate" value="0.0001" step="0.00001" min="0.000001" max="1" class="w-24 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[0.000001, 1]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">模型参数更新步长,过大可能导致训练不稳定,过小收敛速度慢</td>
</tr>
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">n_epochs</span>
<span class="text-red-500 ml-1">*</span>
</td>
<td class="py-3 px-4">
<input type="number" name="n_epochs" value="1" min="1" max="100" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[1, 100]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">完整遍历训练数据集的次数建议设置在1-10之间</td>
</tr>
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">save_steps</span>
<span class="text-red-500 ml-1">*</span>
</td>
<td class="py-3 px-4">
<input type="number" name="save_steps" value="100" min="10" max="10000" class="w-24 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[10, 10000]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">每训练多少步进行一次模型保存建议设置为100的倍数</td>
</tr>
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">lr_scheduler_type</span>
</td>
<td class="py-3 px-4">
<select name="lr_scheduler_type" class="w-28 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all bg-white">
<option value="cosine">cosine</option>
<option value="linear">linear</option>
<option value="constant">constant</option>
</select>
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">3种可选</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">学习率变化策略cosine为余弦退火linear为线性下降constant为保持不变</td>
</tr>
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">max_length</span>
<span class="text-red-500 ml-1">*</span>
</td>
<td class="py-3 px-4">
<input type="number" name="max_length" value="512" min="64" max="4096" class="w-24 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[64, 4096]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">单条训练数据的最大token数超出部分将被截断</td>
</tr>
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">warmup_ratio</span>
</td>
<td class="py-3 px-4">
<input type="number" name="warmup_ratio" value="0.05" step="0.01" min="0" max="1" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[0, 1]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">学习率预热步数占总步数的比例设置为0则不预热</td>
</tr>
<tr class="hover:bg-blue-50/30 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">weight_decay</span>
</td>
<td class="py-3 px-4">
<input type="number" name="weight_decay" value="0.01" step="0.001" min="0" max="1" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[0, 1]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">防止过拟合的正则化技术,值越大对模型参数约束越强</td>
</tr>
</tbody>
<tbody id="loraParamsBody" class="hidden divide-y divide-gray-100">
<!-- LoRA参数 -->
<tr class="bg-blue-50/50 hover:bg-blue-50/70 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">lora_alpha</span>
<span class="text-red-500 ml-1">*</span>
</td>
<td class="py-3 px-4">
<select name="lora_alpha" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all bg-white">
<option value="16" selected>16</option>
<option value="32">32</option>
<option value="64">64</option>
<option value="128">128</option>
</select>
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">4种可选</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">LoRA缩放系数用于控制低秩适配矩阵的权重影响模型对微调数据的敏感度</td>
</tr>
<tr class="bg-blue-50/50 hover:bg-blue-50/70 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">lora_dropout</span>
</td>
<td class="py-3 px-4">
<input type="number" name="lora_dropout" value="0.1" step="0.01" min="0" max="1" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[0, 1]</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">LoRA层 dropout 概率,在低秩适配矩阵中随机丢弃部分神经元以防止过拟合</td>
</tr>
<tr class="bg-blue-50/50 hover:bg-blue-50/70 transition-colors">
<td class="py-3 px-4">
<span class="text-gray-700 font-mono text-sm">lora_rank</span>
<span class="text-red-500 ml-1">*</span>
</td>
<td class="py-3 px-4">
<select name="lora_rank" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all bg-white">
<option value="8" selected>8</option>
<option value="16">16</option>
<option value="32">32</option>
<option value="64">64</option>
</select>
</td>
<td class="py-3 px-4 text-xs text-gray-500">
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">4种可选</span>
</td>
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">LoRA低秩矩阵的秩值越大表示低秩矩阵的维度越高微调能力越强</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 数据配置 -->
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">数据配置</h3>
<!-- 训练集 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-1">
<span class="text-red-500 mr-1">*</span>训练集
</label>
<div class="flex items-center">
<select name="train_dataset_id" id="trainDatasetSelect" class="form-select flex-1 max-w-md">
<option value="">请选择训练数据集</option>
</select>
<button type="button" class="ml-2 text-primary text-sm flex items-center hover:text-primary/80" onclick="loadDatasets()">
<i class="fa fa-refresh"></i>
</button>
<button type="button" class="ml-3 bg-white border border-primary text-primary rounded px-3 py-1.5 text-sm hover:bg-primary/5" onclick="window.location.href='dataset-create.html?from=fine-tune'">
+ 新增数据集
</button>
</div>
</div>
</div>
<!-- 训练产出 -->
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">训练产出</h3>
<p class="text-sm text-gray-500 mb-4">训练完成后,模型将保存为: <code class="bg-gray-100 px-2 py-0.5 rounded text-primary" id="modelNamePreview">任务名称</code></p>
<!-- 训练命令预览 -->
<div class="mt-4">
<div class="flex items-center mb-2">
<span class="text-sm font-medium text-gray-600">训练命令预览</span>
<button type="button" onclick="updateCommandPreview()" class="ml-2 px-2 py-0.5 bg-blue-50 text-blue-600 text-xs rounded hover:bg-blue-100">
<i class="fa fa-refresh mr-1"></i>刷新
</button>
</div>
<div class="bg-gray-900 rounded-lg p-3 overflow-x-auto">
<pre id="commandPreview" class="text-xs text-green-400 font-mono whitespace-pre-wrap break-all">请选择完整配置后查看预览命令</pre>
</div>
</div>
</div>
<!-- 底部按钮 -->
<div class="flex items-center justify-between pt-6 border-t border-gray-100">
<div class="flex items-center space-x-3">
<button type="button" onclick="submitForm()" class="px-4 py-2 bg-primary text-white rounded-lg text-sm hover:bg-primary/90">
开始训练
</button>
<a href="main.html" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300">
取消
</a>
</div>
</div>
</form>
</div>
</div>
</main>
</div>
<script>
// 使用 IIFE 避免全局变量污染
(function() {
// 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();
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
// 卡片式单选框
document.querySelectorAll('.card-radio').forEach(card => {
card.addEventListener('click', () => {
const parent = card.parentElement;
parent.querySelectorAll('.card-radio').forEach(c => c.classList.remove('active'));
card.classList.add('active');
card.querySelector('input').checked = true;
});
});
// 任务名称字数统计和实时预览(只能输入英文、数字、下划线)
const nameInput = document.querySelector('input[name="name"]');
const nameFormatError = document.getElementById('nameFormatError');
const nameRegex = /^[a-zA-Z0-9_]*$/;
nameInput.addEventListener('input', () => {
const value = nameInput.value;
// 验证格式
if (value.length > 0 && !nameRegex.test(value)) {
nameInput.classList.add('border-red-500');
nameInput.classList.remove('border-gray-300');
nameFormatError.classList.remove('hidden');
} else {
nameInput.classList.remove('border-red-500');
nameInput.classList.add('border-gray-300');
nameFormatError.classList.add('hidden');
}
// 过滤非法字符:只允许英文、数字、下划线
const filteredValue = value.replace(/[^a-zA-Z0-9_]/g, '');
if (value !== filteredValue) {
nameInput.value = filteredValue;
}
document.getElementById('nameCount').textContent = nameInput.value.length;
// 更新模型名称预览
document.getElementById('modelNamePreview').textContent = nameInput.value || '任务名称';
updateCommandPreview();
});
// 任务描述字数统计
const descInput = document.querySelector('textarea[name="description"]');
descInput.addEventListener('input', () => {
document.getElementById('descriptionCount').textContent = descInput.value.length;
});
// 加载数据集列表
loadDatasets();
// 加载模型列表
loadModels();
// 初始化训练方法参数显示
toggleTrainMethod();
// 加载GPU列表
loadGPUList();
// 初始化训练命令预览
initCommandPreview();
// 设置侧边栏当前页高亮
const currentPage = 'fine-tune';
document.querySelectorAll('.nav-link').forEach(link => {
if (link.dataset.page === currentPage) {
link.classList.add('bg-[#1890ff]/10', 'text-[#1890ff]');
link.classList.remove('hover:bg-[#001529]/20', 'transition-colors');
}
});
updateSidebarSlider();
// 绑定导航点击事件
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', function(e) {
if (!this.href.includes('fine-tune-create')) {
e.preventDefault();
window.location.href = this.href;
}
});
});
});
// 更新侧边栏滑块位置
function updateSidebarSlider() {
const slider = document.getElementById('sidebar-slider');
if (!slider) return;
const activeLink = document.querySelector('.nav-link.bg-\\[\\#1890ff\\]\\/10');
if (activeLink) {
const wrapper = activeLink.closest('.nav-item-wrapper');
if (wrapper) {
slider.style.top = wrapper.offsetTop + 'px';
slider.style.height = wrapper.offsetHeight + 'px';
}
}
}
// 切换训练方法 - 显示/隐藏LoRA参数
function toggleTrainMethod() {
const trainMethod = document.querySelector('input[name="train_method"]:checked').value;
const loraParamsBody = document.getElementById('loraParamsBody');
if (trainMethod === 'lora') {
loraParamsBody.classList.remove('hidden');
} else {
loraParamsBody.classList.add('hidden');
}
}
// 切换参数配置展开/收起
function toggleParamsCollapse() {
const content = document.getElementById('paramsContent');
const toggleText = document.getElementById('paramsToggleText');
const toggleIcon = document.getElementById('paramsToggleIcon');
if (content.classList.contains('hidden')) {
content.classList.remove('hidden');
toggleText.textContent = '收起';
toggleIcon.className = 'fa fa-chevron-up ml-1 text-xs';
} else {
content.classList.add('hidden');
toggleText.textContent = '展开';
toggleIcon.className = 'fa fa-chevron-down ml-1 text-xs';
}
}
// 重置参数为默认值
function resetParams() {
const defaults = {
'batch_size': 1,
'learning_rate': 0.0001,
'n_epochs': 1,
'save_steps': 100,
'lr_scheduler_type': 'cosine',
'max_length': 512,
'warmup_ratio': 0.05,
'weight_decay': 0.01,
'lora_alpha': '32',
'lora_dropout': 0.1,
'lora_rank': '8'
};
for (const [name, value] of Object.entries(defaults)) {
const input = document.querySelector(`input[name="${name}"]`);
if (input) {
if (input.type === 'number') {
input.value = value;
} else if (input.type === 'select-one') {
input.value = value;
}
}
const select = document.querySelector(`select[name="${name}"]`);
if (select) select.value = value;
}
}
// 渲染数据集单选列表
function renderDatasetRadio(datasets, containerId, name, selectedId) {
const container = document.getElementById(containerId);
if (!datasets || datasets.length === 0) {
container.innerHTML = '<span class="text-sm text-gray-400">暂无可用数据集</span>';
return;
}
container.innerHTML = datasets.map(d => `
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" name="${name}" value="${d.id}" ${d.id === selectedId ? 'checked' : ''} class="text-primary focus:ring-primary">
<span class="text-sm text-gray-700">${d.name}</span>
</label>
`).join('');
}
// 加载数据集列表
async function loadDatasets() {
try {
const response = await fetch(`${API_BASE}/dataset-manage`);
const result = await response.json();
if (result.code === 0) {
// 更新训练集下拉框
const trainSelect = document.getElementById('trainDatasetSelect');
if (trainSelect) {
trainSelect.innerHTML = '<option value="">请选择训练数据集</option>' +
result.data.map(d => `<option value="${d.id}">${d.name}</option>`).join('');
}
}
} catch (e) {
console.error('加载数据集失败:', e);
}
}
// 加载模型列表
async function loadModels() {
try {
const response = await fetch(`${API_BASE}/model-manage`);
const result = await response.json();
if (result.code === 0) {
const modelSelect = document.getElementById('baseModelSelect');
if (modelSelect) {
modelSelect.innerHTML = '<option value="">请选择模型</option>' +
result.data.map(m => `<option value="${m.id}">${m.name}</option>`).join('');
}
}
} catch (e) {
console.error('加载模型失败:', e);
}
}
// 加载GPU列表
async function loadGPUList() {
const container = document.getElementById('gpuSelectionArea');
if (!container) return;
try {
// 模拟GPU数据 - 实际项目中可以从API获取
const gpuData = await fetchGPUs();
renderGPUList(gpuData);
} catch (error) {
console.error('加载GPU列表失败:', error);
container.innerHTML = '<div class="text-sm text-red-500 col-span-full">加载GPU设备失败请刷新重试</div>';
}
}
// 获取GPU数据从真实API获取
async function fetchGPUs() {
try {
const response = await fetch(`${API_BASE}/system-info`);
const result = await response.json();
if (result.code === 0 && result.data.gpu && result.data.gpu.length > 0) {
// 将真实GPU数据转换为前端所需格式
return result.data.gpu.map((gpu, index) => ({
id: `gpu${index}`,
name: gpu.name || `GPU ${index}`,
memory: `${gpu.memory_total_gb}GB`,
cuda_cores: 'N/A',
available: gpu.power_w > 0 || gpu.gpu_percent >= 0, // 有数据即为可用
real_data: gpu // 保存真实数据供显示
}));
}
// 如果没有真实数据,尝试获取驱动版本
const driverVersion = result.data.gpu?.[0]?.driver_version || '';
if (driverVersion) {
return [{
id: 'gpu0',
name: 'NVIDIA GPU (Detected)',
memory: 'Unknown',
cuda_cores: 'N/A',
available: true,
real_data: result.data.gpu?.[0] || null
}];
}
throw new Error('未检测到GPU设备');
} catch (error) {
console.warn('获取GPU信息失败使用模拟数据:', error);
// 失败时返回模拟数据作为后备
return [
{ id: 'gpu0', name: 'NVIDIA GPU (未检测到)', memory: 'Unknown', cuda_cores: 'N/A', available: false }
];
}
}
// 渲染GPU列表点击卡片选中无需复选框
function renderGPUList(gpus) {
const container = document.getElementById('gpuSelectionArea');
if (!container) return;
container.innerHTML = gpus.map(gpu => {
// 从真实数据中提取监控信息
const realData = gpu.real_data || {};
const memoryUsed = realData.memory_used_gb || 0;
const memoryTotal = realData.memory_total_gb || 0;
const temp = realData.temperature || 0;
const power = realData.power_w || 0;
const gpuPercent = realData.gpu_percent || 0;
const fanSpeed = realData.fan_speed || 0;
const clock = realData.clock_mhz || 0;
return `
<div id="gpu_card_${gpu.id}"
class="gpu-card ${!gpu.available ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:border-primary'} border rounded-lg p-3 transition-all"
onclick="toggleGPUSelection('${gpu.id}')"
${!gpu.available ? 'title="该GPU不可用"' : ''}
data-gpu-id="${gpu.id}">
<div class="flex items-center">
<i class="fa fa-microchip text-primary mr-2"></i>
<div class="flex-1">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">${gpu.name}</span>
${gpu.available
? '<i class="fa fa-check-circle text-green-600 text-xs"></i>'
: '<i class="fa fa-times-circle text-red-500 text-xs"></i>'}
</div>
<div class="text-xs text-gray-500 mt-1 flex flex-wrap gap-2">
<span class="mr-2"><i class="fa fa-floppy-o mr-1"></i>${memoryUsed}/${memoryTotal} GB</span>
<span class="mr-2"><i class="fa fa-thermometer-half mr-1"></i>${temp}°C</span>
<span class="mr-2"><i class="fa fa-bolt mr-1"></i>${power} W</span>
<span><i class="fa fa-tachometer mr-1"></i>${clock} MHz</span>
</div>
<!-- GPU利用率进度条 -->
<div class="mt-2">
<div class="flex justify-between text-[10px] text-gray-400 mb-0.5">
<span>GPU: ${gpuPercent}%</span>
<span>Fan: ${fanSpeed}%</span>
</div>
<div class="h-1.5 bg-gray-200 rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-green-400 via-yellow-400 to-red-400 transition-all"
style="width: ${gpuPercent}%"></div>
</div>
</div>
</div>
</div>
</div>
`}).join('');
}
// 切换GPU选择状态
function toggleGPUSelection(gpuId) {
const card = document.getElementById(`gpu_card_${gpuId}`);
if (!card || card.classList.contains('opacity-50')) return;
// 切换选中状态
if (card.classList.contains('border-primary')) {
// 取消选中
card.classList.remove('border-primary', 'bg-blue-50');
// 恢复图标为可选中状态(绿色勾选圈)
const icon = card.querySelector('.fa-check, .fa-check-circle');
if (icon) {
icon.classList.remove('fa-check', 'text-primary');
icon.classList.add('fa-check-circle', 'text-green-600');
}
} else {
// 选中
card.classList.add('border-primary', 'bg-blue-50');
// 切换图标为已选中状态(蓝色勾选)
const icon = card.querySelector('.fa-check-circle');
if (icon) {
icon.classList.remove('fa-check-circle', 'text-green-600');
icon.classList.add('fa-check', 'text-primary');
}
}
}
// 获取选中的GPU列表
function getSelectedGPUs() {
const cards = document.querySelectorAll('.gpu-card.border-primary');
return Array.from(cards).map(card => {
const gpuId = card.dataset.gpuId;
// 获取GPU名称和显存信息用于显示
const nameEl = card.querySelector('.text-gray-700');
const name = nameEl ? nameEl.textContent : gpuId;
// 返回GPU信息对象
return {
id: gpuId,
name: name
};
});
}
// 提交表单
async function submitForm() {
const form = document.getElementById('createForm');
const formData = new FormData(form);
// 获取选中的GPU
const selectedGPUs = getSelectedGPUs();
// 收集训练参数
const trainParams = {
batch_size: parseInt(formData.get('batch_size')) || 1,
learning_rate: parseFloat(formData.get('learning_rate')) || 0.0001,
n_epochs: parseFloat(formData.get('n_epochs')) || 1.0,
save_steps: parseInt(formData.get('save_steps')) || 100,
lr_scheduler_type: formData.get('lr_scheduler_type') || 'cosine',
max_length: parseInt(formData.get('max_length')) || 512,
warmup_ratio: parseFloat(formData.get('warmup_ratio')) || 0.05,
weight_decay: parseFloat(formData.get('weight_decay')) || 0.01,
lora_alpha: formData.get('lora_alpha') || '32',
lora_dropout: parseFloat(formData.get('lora_dropout')) || 0.1,
lora_rank: formData.get('lora_rank') || '8'
};
const taskName = formData.get('name');
const data = {
name: taskName,
description: formData.get('description'),
base_model: formData.get('base_model'),
template: formData.get('template'),
train_type: formData.get('train_type'),
train_method: formData.get('train_method'),
gpus: selectedGPUs,
train_dataset_id: formData.get('train_dataset_id'),
output_model_name: taskName, // 使用任务名称作为模型名称
...trainParams,
status: 'pending',
progress: 0
};
if (!data.name) {
showMessage('提示', '请输入任务名称', 'warning');
return;
}
// 验证任务名称格式
const nameRegex = /^[a-zA-Z0-9_]+$/;
if (!nameRegex.test(data.name)) {
showMessage('提示', '任务名称只能包含英文、数字和下划线', 'warning');
return;
}
// 检查任务名称是否重复
try {
const checkResponse = await fetch(`${API_BASE}/fine-tune/check-name?name=${encodeURIComponent(data.name)}`);
const checkResult = await checkResponse.json();
if (checkResult.code === 0 && checkResult.data.exists) {
showMessage('提示', '任务名称已存在,请使用其他名称', 'warning');
return;
}
} catch (error) {
console.error('检查任务名称失败:', error);
}
if (selectedGPUs.length === 0) {
showMessage('提示', '请选择至少一个GPU硬件', 'warning');
return;
}
if (!data.base_model) {
showMessage('提示', '请选择基础模型', 'warning');
return;
}
if (!data.template) {
showMessage('提示', '请选择训练模板', 'warning');
return;
}
if (!data.train_dataset_id) {
showMessage('提示', '请选择训练集', 'warning');
return;
}
try {
// 显示加载中状态
const submitBtn = document.querySelector('button[onclick="submitForm()"]');
const originalText = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i>训练任务创建中...';
// 第一步:创建训练任务记录
const createResponse = await fetch(`${API_BASE}/fine-tune`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const createResult = await createResponse.json();
if (createResult.code !== 0) {
submitBtn.disabled = false;
submitBtn.innerHTML = originalText;
showMessage('错误', createResult.message || '创建任务失败', 'error');
return;
}
const taskId = createResult.id;
// 第二步:启动训练
const startData = {
task_id: taskId,
name: data.name, // 任务名称,用于日志文件名和模型名称
base_model: data.base_model,
template: data.template,
train_type: data.train_type,
train_method: data.train_method,
train_dataset_id: data.train_dataset_id,
output_model_name: data.name, // 使用任务名称作为模型名称
...trainParams
};
const startResponse = await fetch(`${API_BASE}/fine-tune/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(startData)
});
const startResult = await startResponse.json();
// 恢复按钮状态
submitBtn.disabled = false;
submitBtn.innerHTML = originalText;
if (startResult.code === 0) {
showMessage('成功', '训练任务已启动!', 'success', () => {
window.location.href = 'main.html';
});
} else {
// 更新任务状态为失败
await fetch(`${API_BASE}/fine-tune/${taskId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'failed' })
});
showMessage('错误', startResult.message || '启动训练失败', 'error');
}
} catch (error) {
// 恢复按钮状态
const submitBtn = document.querySelector('button[onclick="submitForm()"]');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = '开始训练';
}
showMessage('错误', '操作失败: ' + error.message, 'error');
}
}
// 生成训练命令预览
function buildCommandPreview() {
const form = document.getElementById('createForm');
const formData = new FormData(form);
// 获取选中的GPU
const selectedGPUs = getSelectedGPUs();
let gpuIds = '0';
if (selectedGPUs.length > 0) {
gpuIds = selectedGPUs.map(g => g.id.replace('gpu', '')).filter(g => /^\d+$/.test(g)).join(',');
}
// 获取模型路径
const baseModelSelect = form.querySelector('select[name="base_model"]');
let modelPath = formData.get('base_model') || '';
if (baseModelSelect && baseModelSelect.selectedOptions.length > 0) {
const selectedOption = baseModelSelect.selectedOptions[0];
const pathValue = selectedOption.getAttribute('data-path');
if (pathValue) {
modelPath = pathValue;
}
}
// 获取模板
const template = formData.get('template') || 'qwen3';
// 获取训练类型
const trainType = formData.get('train_type') || 'SFT';
const stageMap = { 'SFT': 'sft', 'DPO': 'dpo', 'CPT': 'cpt' };
// 获取训练方法
const trainMethod = formData.get('train_method') || 'lora';
const methodMap = { 'lora': 'lora', 'full': 'full' };
// 获取输出模型名称(使用任务名称,路径格式: {train_method}/{taskName}
const taskName = formData.get('name') || 'task_name';
const outputModelName = `${trainMethod}/${taskName}`;
const outputDir = outputModelName.startsWith('/') ? outputModelName : `/app/base/saves/${outputModelName}`;
// 获取数据集名称
const trainDatasetSelect = form.querySelector('select[name="train_dataset_id"]');
let datasetName = formData.get('train_dataset_id') || 'dataset_name';
if (trainDatasetSelect && trainDatasetSelect.selectedOptions.length > 0) {
const selectedOption = trainDatasetSelect.selectedOptions[0];
const datasetValue = selectedOption.getAttribute('data-name');
if (datasetValue) {
datasetName = datasetValue;
}
}
// 获取训练参数
const batchSize = parseInt(formData.get('batch_size')) || 1;
const learningRate = parseFloat(formData.get('learning_rate')) || 0.0001;
const nEpochs = parseFloat(formData.get('n_epochs')) || 1.0;
const maxLength = parseInt(formData.get('max_length')) || 512;
const warmupSteps = parseInt(formData.get('warmup_steps')) || 20;
const saveSteps = parseInt(formData.get('save_steps')) || 100;
const gradientAccumulationSteps = parseInt(formData.get('gradient_accumulation_steps')) || 8;
const lrSchedulerType = formData.get('lr_scheduler_type') || 'cosine';
// LoRA参数
const loraAlpha = formData.get('lora_alpha') || '32';
const loraDropout = parseFloat(formData.get('lora_dropout')) || 0.1;
const loraRank = formData.get('lora_rank') || '8';
// 构建命令
let cmd = `CUDA_VISIBLE_DEVICES=${gpuIds} llamafactory-cli train \\\n`;
cmd += ` --stage ${stageMap[trainType] || 'sft'} \\\n`;
cmd += ` --do_train \\\n`;
cmd += ` --model_name_or_path ${modelPath} \\\n`;
cmd += ` --dataset ${datasetName} \\\n`;
cmd += ` --dataset_dir ./datasets \\\n`;
cmd += ` --template ${template} \\\n`;
cmd += ` --finetuning_type ${methodMap[trainMethod] || 'lora'} \\\n`;
// LoRA参数仅lora方法时显示
if (trainMethod === 'lora') {
cmd += ` --lora_alpha ${loraAlpha} \\\n`;
cmd += ` --lora_dropout ${loraDropout} \\\n`;
cmd += ` --lora_rank ${loraRank} \\\n`;
}
cmd += ` --output_dir ${outputDir} \\\n`;
cmd += ` --overwrite_cache \\\n`;
cmd += ` --overwrite_output_dir \\\n`;
cmd += ` --cutoff_len ${maxLength} \\\n`;
cmd += ` --preprocessing_num_workers 16 \\\n`;
cmd += ` --per_device_train_batch_size ${batchSize} \\\n`;
cmd += ` --per_device_eval_batch_size 1 \\\n`;
cmd += ` --gradient_accumulation_steps ${gradientAccumulationSteps} \\\n`;
cmd += ` --lr_scheduler_type ${lrSchedulerType} \\\n`;
cmd += ` --logging_steps 50 \\\n`;
cmd += ` --warmup_steps ${warmupSteps} \\\n`;
cmd += ` --save_steps ${saveSteps} \\\n`;
cmd += ` --learning_rate ${learningRate} \\\n`;
cmd += ` --num_train_epochs ${nEpochs} \\\n`;
cmd += ` --plot_loss`;
return cmd;
}
// 更新命令预览
function updateCommandPreview() {
const preview = document.getElementById('commandPreview');
const cmd = buildCommandPreview();
preview.textContent = cmd;
}
// 监听表单变化自动更新预览
function initCommandPreview() {
const form = document.getElementById('createForm');
// 监听所有 input 和 select 的变化
const inputs = form.querySelectorAll('input, select');
inputs.forEach(input => {
input.addEventListener('change', () => setTimeout(updateCommandPreview, 100));
if (input.type === 'text' || input.type === 'number') {
input.addEventListener('input', () => setTimeout(updateCommandPreview, 100));
}
});
// 监听卡片式单选框的点击事件 (训练类型、训练方法)
document.querySelectorAll('.card-radio').forEach(card => {
card.addEventListener('click', () => setTimeout(updateCommandPreview, 100));
});
// 监听 GPU 卡片的点击事件
document.querySelectorAll('.gpu-card').forEach(card => {
card.addEventListener('click', () => setTimeout(updateCommandPreview, 100));
});
// 初始化时更新一次
setTimeout(updateCommandPreview, 500);
}
// 将函数暴露到全局作用域,供 onclick 调用
window.toggleGPUSelection = toggleGPUSelection;
window.submitForm = submitForm;
window.toggleTrainMethod = toggleTrainMethod;
})();
</script>
<!-- 自定义消息弹窗 -->
<div id="customModal" class="hidden fixed inset-0 bg-black/50 z-50 flex items-center justify-center" onclick="if(event.target === this) closeModal();">
<div class="bg-white rounded-xl shadow-xl max-w-sm w-full mx-4 overflow-hidden transform transition-all">
<div class="flex flex-col items-center justify-center min-h-[160px] py-6">
<div id="modalIcon"></div>
<h3 id="modalTitle" class="text-base 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 flex-col space-y-2 mx-4">
<button id="modalConfirmBtn" class="px-4 py-2 w-full text-white rounded transition-colors text-sm">确定</button>
<button id="modalCancelBtn" class="px-4 py-2 w-full border border-gray-300 text-gray-700 rounded hover:bg-gray-50 transition-colors text-sm">取消</button>
</div>
<div id="modalSingleBtnGroup" class="px-6 pb-6 flex justify-center">
<button id="modalConfirmBtn2" class="px-6 py-2 w-full text-white rounded transition-colors text-sm max-w-[160px]">确定</button>
</div>
</div>
</div>
<script>
// 显示消息弹窗
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('modalConfirmBtn');
const modalConfirmBtn2 = document.getElementById('modalConfirmBtn2');
const modalBtnGroup = document.getElementById('modalBtnGroup');
const modalSingleBtnGroup = document.getElementById('modalSingleBtnGroup');
modalTitle.textContent = title;
modalTitle.className = 'text-lg font-medium text-gray-800 mb-2';
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>';
}
modalMessage.innerHTML = message;
modalBtnGroup.classList.add('hidden');
modalSingleBtnGroup.classList.remove('hidden');
const confirmBtn = modalConfirmBtn2;
confirmBtn.className = 'px-6 py-2 w-full text-white rounded transition-colors text-sm max-w-[160px]';
if (type === 'success') {
confirmBtn.classList.add('bg-primary');
} else if (type === 'error') {
confirmBtn.classList.add('bg-red-500');
} else if (type === 'warning') {
confirmBtn.classList.add('bg-yellow-500');
} else {
confirmBtn.classList.add('bg-primary');
}
confirmBtn.onclick = () => {
closeModal();
if (onConfirm) onConfirm();
};
modal.classList.remove('hidden');
}
function closeModal() {
const modal = document.getElementById('customModal');
modal.classList.add('hidden');
}
</script>
</body>
</html>