重构了main.html的主函数

重构了大量的页面的sidebar
优化了代码结构
This commit is contained in:
2026-02-02 09:22:52 +08:00
33 changed files with 5566 additions and 2383 deletions

View File

@@ -4,9 +4,7 @@
import os
import sys
import logging
import yaml
from datetime import datetime
from logging.handlers import TimedRotatingFileHandler, RotatingFileHandler
from flask import Blueprint, request, jsonify
# 获取项目根目录
@@ -14,74 +12,40 @@ PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(_
sys.path.insert(0, PROJECT_ROOT)
# 加载配置
import yaml
CONFIG_PATH = os.path.join(PROJECT_ROOT, 'config.yaml')
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
CONFIG = yaml.safe_load(f)
# 日志目录
LOG_BASE_DIR = os.path.join(PROJECT_ROOT, 'logs')
# 日志目录 - 使用与 main.py 相同的配置
def get_log_base_dir():
"""获取日志基础目录"""
# 1. 检查环境变量
if 'LOG_BASE_DIR' in os.environ:
return os.environ['LOG_BASE_DIR']
# 2. 检查是否在容器环境中
mount_base = os.environ.get('MOUNT_BASE', '/app/base')
if os.path.exists(mount_base):
return os.path.join(mount_base, 'logs')
# 3. 使用本地项目路径
return os.path.join(PROJECT_ROOT, 'logs')
LOG_BASE_DIR = get_log_base_dir()
# 创建蓝图
logs_bp = Blueprint('logs', __name__, url_prefix='/api')
def setup_logs_logger():
"""配置日志系统,按日期分目录存储"""
today = datetime.now().strftime('%Y-%m-%d')
log_dir = os.path.join(LOG_BASE_DIR, today)
os.makedirs(log_dir, exist_ok=True)
logger = logging.getLogger('logs_api')
logger.setLevel(logging.DEBUG)
logger.handlers.clear()
# 全部日志处理器
all_log_path = os.path.join(log_dir, 'all.log')
all_handler = TimedRotatingFileHandler(all_log_path, when='midnight', interval=1, backupCount=30, encoding='utf-8')
all_handler.setLevel(logging.DEBUG)
all_handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
# 控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(levelname)s: %(message)s',
datefmt='%H:%M:%S'
))
logger.addHandler(all_handler)
logger.addHandler(console_handler)
return logger
def get_logs_logger():
"""从 main.py 获取日志记录器"""
return logging.getLogger('logs_api')
logs_logger = setup_logs_logger()
@logs_bp.route('/web-log', methods=['POST'])
def receive_web_log():
"""接收前端页面发送的日志"""
data = request.json
level = data.get('level', 'info')
message = data.get('message', '')
page = data.get('page', 'unknown')
timestamp = data.get('timestamp', datetime.now().isoformat())
log_message = f'[WEB-{page}] {message}'
if level == 'error':
logs_logger.error(log_message)
elif level == 'warning':
logs_logger.warning(log_message)
elif level == 'debug':
logs_logger.debug(log_message)
else:
logs_logger.info(log_message)
return jsonify({'code': 0, 'message': '日志接收成功'})
def get_request_logger():
"""获取请求日志记录器"""
return logging.getLogger('request')
def format_file_size(size_bytes):
@@ -94,6 +58,30 @@ def format_file_size(size_bytes):
return f'{size_bytes / (1024 * 1024):.1f} MB'
@logs_bp.route('/web-log', methods=['POST'])
def receive_web_log():
"""接收前端页面发送的日志"""
data = request.json
level = data.get('level', 'info')
message = data.get('message', '')
page = data.get('page', 'unknown')
timestamp = data.get('timestamp', '')
log_message = f'[WEB-{page}] {message}'
logger = get_logs_logger()
if level == 'error':
logger.error(log_message)
elif level == 'warning':
logger.warning(log_message)
elif level == 'debug':
logger.debug(log_message)
else:
logger.info(log_message)
return jsonify({'code': 0, 'message': '日志接收成功'})
@logs_bp.route('/log-files', methods=['GET'])
def get_log_files():
"""获取指定日期的日志文件列表"""
@@ -113,7 +101,8 @@ def get_log_files():
return jsonify({'code': 0, 'data': []})
log_files = []
file_names = ['all.log', 'error.log', 'request.log']
# 定义日志文件的优先级顺序
file_names = ['all.log', 'api.log', 'error.log', 'request.log', 'train.log']
for file_name in file_names:
file_path = os.path.join(log_dir, file_name)
@@ -178,15 +167,13 @@ TRAINING_LOGS_BASE_DIR = '/app/base/logs'
# 本地开发时的备用路径Windows
LOCAL_TRAINING_LOGS_BASE_DIR = os.path.join(PROJECT_ROOT, 'logs')
# 添加调试日志
logs_logger.info(f"[DEBUG] TRAINING_LOGS_BASE_DIR: {TRAINING_LOGS_BASE_DIR}")
logs_logger.info(f"[DEBUG] LOCAL_TRAINING_LOGS_BASE_DIR: {LOCAL_TRAINING_LOGS_BASE_DIR}")
@logs_bp.route('/training-log-files', methods=['GET'])
def get_training_log_files():
"""获取训练日志文件列表 - 从 logs/{日期} 目录下的 .log 文件"""
try:
logs_logger = get_logs_logger()
# 确定基础目录
logs_base_dir = TRAINING_LOGS_BASE_DIR
if not os.path.exists(logs_base_dir):
@@ -280,7 +267,7 @@ def get_training_log_files():
return jsonify({'code': 0, 'data': log_files})
except Exception as e:
logs_logger.error(f"[DEBUG] 获取训练日志列表失败: {e}")
get_logs_logger().error(f"[DEBUG] 获取训练日志列表失败: {e}")
return jsonify({'code': 1, 'message': f'获取训练日志列表失败: {str(e)}'})
@@ -291,6 +278,7 @@ def get_training_log_content():
if not file_name:
return jsonify({'code': 1, 'message': '缺少文件参数'})
logs_logger = get_logs_logger()
logs_logger.info(f"[DEBUG] ============ get_training_log_content ============")
logs_logger.info(f"[DEBUG] file: {file_name}")

View File

@@ -8,8 +8,12 @@ import json
import requests
import concurrent.futures
import subprocess
import logging
from flask import Blueprint, request, jsonify
# 获取模块 logger继承 main.py 的日志配置)
logger = logging.getLogger(__name__)
# 获取项目根目录
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -218,8 +222,6 @@ def preload_trained_model():
import sys as sys_module
import pymysql
import yaml
import logging
logger = logging.getLogger(__name__)
data = request.json
model_name = data.get('model_name') # 模型名称
@@ -572,3 +574,502 @@ if __name__ == "__main__":
return jsonify({'code': 1, 'message': '推理超时,请稍后重试'})
except Exception as e:
return jsonify({'code': 1, 'message': f'推理异常: {str(e)}'})
# ==================== Transformers 本地模型接口 ====================
@model_chat_bp.route('/local/preload', methods=['POST'])
def preload_local_model():
"""预加载本地模型(使用 transformers"""
import yaml
import subprocess
import sys as sys_module
data = request.json
model_path = data.get('model_path') # 模型路径
model_name = data.get('model_name', '本地模型') # 模型名称(用于显示)
if not model_path:
return jsonify({'code': 1, 'message': '缺少模型路径'})
logger.info(f"[PRELOAD_LOCAL] 开始预加载本地模型: {model_name}, 路径: {model_path}")
# 先生成唯一ID避免并发冲突必须在f-string之前定义
import uuid as uuid_module
temp_id = uuid_module.uuid4().hex[:8]
# 获取项目根目录
PROJECT_ROOT = os.path.dirname(os.dirname(os.path.dirname(os.path.abspath(__file__))))
CONFIG_PATH = os.path.join(PROJECT_ROOT, 'config.yaml')
try:
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
CONFIG = yaml.safe_load(f)
except Exception as e:
return jsonify({'code': 1, 'message': f'读取配置失败: {str(e)}'})
# 构建 transformers 预加载脚本不使用f-string避免import语句导致的模块shadow问题
preload_script = '''# -*- coding: utf-8 -*-
import sys
import uuid
import logging
import os
logging.basicConfig(level=logging.WARNING, format='%(message)s')
# 生成唯一的临时文件路径,避免并发冲突
temp_id = "TEMP_ID"
log_file = "/app/base/logs/preload_local_{}.log".format(temp_id)
# 设置环境变量
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
os.environ["TOKENIZERS_PARALLELISM"] = "false"
try:
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
model_id = "MODEL_PATH"
with open(log_file, "w", encoding="utf-8") as f:
f.write("开始加载模型: {}\\n".format(model_id))
# 加载 tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
with open(log_file, "a", encoding="utf-8") as f:
f.write("Tokenizer 加载完成\\n")
# 加载模型
model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype=torch.float16,
device_map="auto",
trust_remote_code=True
)
with open(log_file, "a", encoding="utf-8") as f:
f.write("模型加载完成\\n")
# 测试推理
test_input = "你好"
inputs = tokenizer(test_input, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=10, do_sample=False)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
with open(log_file, "a", encoding="utf-8") as f:
f.write("测试推理成功: {}\\n".format(response))
with open(log_file, "a", encoding="utf-8") as f:
f.write("SUCCESS\\n")
except Exception as e:
with open(log_file, "a", encoding="utf-8") as f:
f.write("ERROR: {}\\n".format(str(e)))
import traceback
with open(log_file, "a", encoding="utf-8") as f:
f.write(traceback.format_exc())
sys.exit(1)
'''
# 使用 replace 替换变量(避免 f-string 中包含 import 语句)
preload_script = preload_script.replace('TEMP_ID', temp_id).replace('MODEL_PATH', model_path)
# 写入临时脚本 - 使用唯一文件名避免并发冲突
work_dir = '/app/base'
script_path = os.path.join(work_dir, f'temp_preload_local_{temp_id}.py')
log_path = os.path.join('/app/base/logs', f'preload_local_{temp_id}.log')
try:
# 确保logs目录存在
os.makedirs(os.path.dirname(log_path), exist_ok=True)
with open(script_path, 'w', encoding='utf-8') as f:
f.write(preload_script)
# 查找系统 Python不使用虚拟环境
def get_system_python():
# 尝试常见的系统 Python 路径
common_pythons = [
'/usr/bin/python3',
'/usr/bin/python',
'/usr/local/bin/python3',
'/usr/local/bin/python',
]
for py in common_pythons:
if os.path.exists(py) and os.access(py, os.X_OK):
return py
# 如果都没找到,使用系统 PATH 中的 python3
import shutil
py_path = shutil.which('python3')
if py_path:
return py_path
return sys_module.executable # 兜底使用当前 Python
python_executable = get_system_python()
logger.info(f"[PRELOAD_LOCAL] 使用系统 Python: {python_executable}")
# 继承环境变量,但清除虚拟环境相关的变量
env = {**os.environ}
env['CUDA_VISIBLE_DEVICES'] = '0'
env['TOKENIZERS_PARALLELISM'] = 'false'
# 清除虚拟环境相关变量
env.pop('VIRTUAL_ENV', None)
env.pop('PYTHONHOME', None)
logger.info(f"[PRELOAD_LOCAL] 脚本路径: {script_path}")
logger.info(f"[PRELOAD_LOCAL] 日志路径: {log_path}")
logger.info(f"[PRELOAD_LOCAL] 工作目录: {work_dir}")
logger.info(f"[PRELOAD_LOCAL] Python: {python_executable}")
logger.info(f"[PRELOAD_LOCAL] 脚本是否存在: {os.path.exists(script_path)}")
# 执行预加载脚本
try:
result = subprocess.run(
[python_executable, script_path],
capture_output=True,
text=True,
timeout=600, # 10分钟超时
cwd=work_dir,
env=env
)
logger.info(f"[PRELOAD_LOCAL] 返回码: {result.returncode}")
logger.info(f"[PRELOAD_LOCAL] stdout: {result.stdout[:500] if result.stdout else 'empty'}")
logger.info(f"[PRELOAD_LOCAL] stderr: {result.stderr[:500] if result.stderr else 'empty'}")
except Exception as sub_err:
logger.error(f"[PRELOAD_LOCAL] subprocess执行异常: {sub_err}")
return jsonify({'code': 1, 'message': f'执行异常: {str(sub_err)}'})
# 读取日志文件获取实际输出
try:
if os.path.exists(log_path):
with open(log_path, 'r', encoding='utf-8') as f:
full_output = f.read()
logger.info(f"[PRELOAD_LOCAL] 日志文件内容: {full_output[:500]}")
else:
logger.warning(f"[PRELOAD_LOCAL] 日志文件不存在: {log_path}")
full_output = result.stdout + result.stderr
except Exception as read_err:
logger.error(f"[PRELOAD_LOCAL] 读取日志失败: {read_err}")
full_output = result.stdout + result.stderr
logger.info(f"[PRELOAD_LOCAL] 脚本输出: {full_output[:500] if len(full_output) > 500 else full_output}")
# 清理临时文件
try:
if os.path.exists(script_path):
os.remove(script_path)
except Exception:
pass
try:
if os.path.exists(log_path):
os.remove(log_path)
except Exception:
pass
if result.returncode == 0 and 'SUCCESS' in full_output:
logger.info(f"[PRELOAD_LOCAL] 模型预加载成功: {model_name}")
return jsonify({
'code': 0,
'message': '模型预加载成功',
'data': {'model_name': model_name}
})
else:
error_msg = '预加载失败'
for line in full_output.split('\n'):
if 'ERROR:' in line:
error_msg = line.split('ERROR:')[1].strip()
break
logger.error(f"[PRELOAD_LOCAL] 预加载失败: {error_msg}")
return jsonify({'code': 1, 'message': f'预加载失败: {error_msg}'})
except subprocess.TimeoutExpired:
# 清理临时文件
try:
if os.path.exists(script_path):
os.remove(script_path)
except Exception:
pass
try:
if os.path.exists(log_path):
os.remove(log_path)
except Exception:
pass
logger.error("[PRELOAD_LOCAL] 预加载超时")
return jsonify({'code': 1, 'message': '预加载超时,请确保模型路径正确且有足够显存'})
except Exception as e:
# 清理临时文件
try:
if os.path.exists(script_path):
os.remove(script_path)
except Exception:
pass
try:
if os.path.exists(log_path):
os.remove(log_path)
except Exception:
pass
logger.error(f"[PRELOAD_LOCAL] 预加载异常: {str(e)}")
return jsonify({'code': 1, 'message': f'预加载异常: {str(e)}'})
@model_chat_bp.route('/local/chat', methods=['POST'])
def chat_local_model():
"""使用本地模型进行对话推理(使用 transformers"""
import yaml
import subprocess
import sys as sys_module
import json
data = request.json
model_path = data.get('model_path') # 模型路径
system_prompt = data.get('system_prompt', '')
user_question = data.get('user_question')
temperature = data.get('temperature', 0.7)
max_tokens = data.get('max_tokens', 2048)
if not model_path:
return jsonify({'code': 1, 'message': '缺少模型路径'})
if not user_question:
return jsonify({'code': 1, 'message': '缺少用户提问'})
logger.info(f"[CHAT_LOCAL] 开始对话推理, 模型路径: {model_path}")
# 构建消息
messages = []
if system_prompt:
messages.append({'role': 'system', 'content': system_prompt})
messages.append({'role': 'user', 'content': user_question})
# 先生成唯一ID避免并发冲突必须在f-string之前定义
import uuid as uuid_module
temp_id = uuid_module.uuid4().hex[:8]
# 构建 transformers 推理脚本不使用f-string避免import语句导致的模块shadow问题
# 使用唯一占位符避免与其他内容冲突
import json as json_module
messages_json = json_module.dumps(messages, ensure_ascii=False)
inference_script = '''# -*- coding: utf-8 -*-
import sys
import os
import uuid
import logging
import json
logging.basicConfig(level=logging.WARNING, format='%(message)s')
# 生成唯一的临时文件路径
temp_id = "__TEMP_ID__"
log_file = "/app/base/logs/chat_local_{}.log".format(temp_id)
# 设置环境变量
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
os.environ["TOKENIZERS_PARALLELISM"] = "false"
try:
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
model_id = "__MODEL_PATH__"
with open(log_file, "w", encoding="utf-8") as f:
f.write("正在加载模型: {}\\n".format(model_id))
# 加载 tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
with open(log_file, "a", encoding="utf-8") as f:
f.write("Tokenizer 加载完成\\n")
# 加载模型
model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype=torch.float16,
device_map="auto",
trust_remote_code=True
)
with open(log_file, "a", encoding="utf-8") as f:
f.write("模型加载完成\\n")
# 构建消息格式
messages = __MESSAGES_JSON__
# 应用 chat template
if tokenizer.chat_template:
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
else:
# 手动构建
text = ""
for msg in messages:
text += "<|im_start|>{}<|im_end|>\\n".format(msg['role'])
text += "{}<|im_end|>\\n".format(msg['content'])
text += "<|im_start|>assistant\\n"
with open(log_file, "a", encoding="utf-8") as f:
f.write("构建输入完成\\n")
# 编码输入
inputs = tokenizer(text, return_tensors="pt").to(model.device)
# 生成回复
outputs = model.generate(
**inputs,
max_new_tokens=__MAX_TOKENS__,
temperature=__TEMPERATURE__,
do_sample=True,
top_p=0.9,
pad_token_id=tokenizer.eos_token_id
)
# 解码输出
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
# 提取 assistant 的回复
if "assistant\\n" in response:
response = response.split("assistant\\n")[-1]
elif "<|im_start|>assistant" in response:
response = response.split("<|im_start|>assistant")[-1]
response = response.strip()
with open(log_file, "a", encoding="utf-8") as f:
f.write("{}\\n".format(response))
with open(log_file, "a", encoding="utf-8") as f:
f.write("SUCCESS\\n")
except Exception as e:
with open(log_file, "a", encoding="utf-8") as f:
f.write("ERROR: {}\\n".format(str(e)))
import traceback
with open(log_file, "a", encoding="utf-8") as f:
f.write(traceback.format_exc())
sys.exit(1)
'''
# 使用 replace 替换变量(避免 f-string 中包含 import 语句)
inference_script = inference_script.replace('__TEMP_ID__', temp_id)
inference_script = inference_script.replace('__MODEL_PATH__', model_path)
inference_script = inference_script.replace('__MESSAGES_JSON__', messages_json)
inference_script = inference_script.replace('__MAX_TOKENS__', str(max_tokens))
inference_script = inference_script.replace('__TEMPERATURE__', str(temperature))
# 写入临时脚本 - 使用唯一文件名避免并发冲突
work_dir = '/app/base'
script_path = os.path.join(work_dir, f'temp_chat_local_{temp_id}.py')
log_path = os.path.join('/app/base/logs', f'chat_local_{temp_id}.log')
try:
# 确保logs目录存在
os.makedirs(os.path.dirname(log_path), exist_ok=True)
with open(script_path, 'w', encoding='utf-8') as f:
f.write(inference_script)
# 查找系统 Python不使用虚拟环境
def get_system_python():
# 尝试常见的系统 Python 路径
common_pythons = [
'/usr/bin/python3',
'/usr/bin/python',
'/usr/local/bin/python3',
'/usr/local/bin/python',
]
for py in common_pythons:
if os.path.exists(py) and os.access(py, os.X_OK):
return py
# 如果都没找到,使用系统 PATH 中的 python3
import shutil
py_path = shutil.which('python3')
if py_path:
return py_path
return sys_module.executable # 兜底使用当前 Python
python_executable = get_system_python()
logger.info(f"[CHAT_LOCAL] 使用系统 Python: {python_executable}")
# 继承环境变量,但清除虚拟环境相关的变量
env = {**os.environ}
env['CUDA_VISIBLE_DEVICES'] = '0'
env['TOKENIZERS_PARALLELISM'] = 'false'
# 清除虚拟环境相关变量
env.pop('VIRTUAL_ENV', None)
env.pop('PYTHONHOME', None)
# 执行推理脚本
result = subprocess.run(
[python_executable, script_path],
capture_output=True,
text=True,
timeout=600, # 10分钟超时
cwd=work_dir,
env=env
)
# 读取日志文件获取实际输出
try:
if os.path.exists(log_path):
with open(log_path, 'r', encoding='utf-8') as f:
full_output = f.read()
else:
full_output = result.stdout + result.stderr
except Exception as read_err:
logger.error(f"[CHAT_LOCAL] 读取日志失败: {read_err}")
full_output = result.stdout + result.stderr
logger.info(f"[CHAT_LOCAL] 脚本输出: {full_output[:500] if len(full_output) > 500 else full_output}")
# 清理临时文件
try:
if os.path.exists(script_path):
os.remove(script_path)
except Exception:
pass
try:
if os.path.exists(log_path):
os.remove(log_path)
except Exception:
pass
if result.returncode == 0 and 'SUCCESS' in full_output:
# 提取实际回复(去掉最后的 SUCCESS
lines = full_output.strip().split('\n')
response_lines = [line for line in lines if line.strip() and line.strip() != 'SUCCESS']
assistant_content = '\n'.join(response_lines).strip()
logger.info(f"[CHAT_LOCAL] 对话成功, 回复长度: {len(assistant_content)}")
return jsonify({
'code': 0,
'data': {
'model_path': model_path,
'response': assistant_content
}
})
else:
error_msg = '推理失败'
for line in full_output.split('\n'):
if 'ERROR:' in line:
error_msg = line.split('ERROR:')[1].strip()
break
logger.error(f"[CHAT_LOCAL] 推理失败: {error_msg}")
return jsonify({'code': 1, 'message': f'推理失败: {error_msg}'})
except subprocess.TimeoutExpired:
# 清理临时文件
try:
if os.path.exists(script_path):
os.remove(script_path)
except Exception:
pass
try:
if os.path.exists(log_path):
os.remove(log_path)
except Exception:
pass
logger.error("[CHAT_LOCAL] 推理超时")
return jsonify({'code': 1, 'message': '推理超时请减少生成token数量或调整参数'})
except Exception as e:
# 清理临时文件
try:
if os.path.exists(script_path):
os.remove(script_path)
except Exception:
pass
try:
if os.path.exists(log_path):
os.remove(log_path)
except Exception:
pass
logger.error(f"[CHAT_LOCAL] 推理异常: {str(e)}")
return jsonify({'code': 1, 'message': f'推理异常: {str(e)}'})

View File

@@ -4,8 +4,12 @@
import os
import pymysql
import yaml
import logging
from flask import Blueprint, request, jsonify
# 获取模块 logger继承 main.py 的日志配置)
logger = logging.getLogger(__name__)
# 获取项目根目录 - 优先使用环境变量,否则从文件路径计算
MOUNT_BASE = os.environ.get('MOUNT_BASE', '/app/base')
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -47,8 +51,6 @@ def generic_get_all(table_name, order_by='create_time DESC'):
def get_model_path_by_name(model_name):
"""根据模型名称查询模型路径(用于获取基座模型路径)"""
import logging
logger = logging.getLogger(__name__)
logger.info(f"[DEBUG get_model_path_by_name] 查询模型: {model_name}")
try:
@@ -165,6 +167,23 @@ def get_model_manage_by_id(id):
return jsonify({'code': 1, 'message': '模型不存在'})
@model_manage_bp.route('/name/<model_name>', methods=['GET'])
def get_model_manage_by_name(model_name):
"""根据名称获取模型"""
logger.info(f"[DEBUG] 按名称查询模型: {model_name}")
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM model_manage WHERE name = %s LIMIT 1", (model_name,))
model = cursor.fetchone()
cursor.close()
conn.close()
if model:
return jsonify({'code': 0, 'data': model})
return jsonify({'code': 1, 'message': '模型不存在'})
@model_manage_bp.route('', methods=['POST'])
def create_model_manage():
"""创建模型"""
@@ -575,25 +594,57 @@ def merge_model():
@model_manage_bp.route('/trained-models/<model_name>', methods=['DELETE'])
def delete_trained_model(model_name):
"""删除已训练模型从local_trained_models目录"""
"""删除已训练模型
type=merged: 删除合并模型local_trained_models目录
type=lora: 删除权重saves目录下的lora等权重文件
"""
import shutil
import logging
logger = logging.getLogger(__name__)
# 获取删除类型参数
delete_type = request.args.get('type', 'merged') # 默认删除合并模型
try:
# 删除 local_trained_models 目录下的模型
model_path = os.path.join(PROJECT_ROOT, 'local_trained_models', model_name)
if delete_type == 'lora':
# 删除权重:删除 saves 目录下的权重
saves_path = os.path.join(PROJECT_ROOT, 'saves')
train_methods = ['lora', 'full', 'qlora', 'dpo', 'cpt', 'prefix', 'adapter', 'peft']
if not os.path.exists(model_path):
return jsonify({'code': 1, 'message': f'模型不存在: {model_name}'})
deleted = False
for method in train_methods:
weight_path = os.path.join(saves_path, method, model_name)
if os.path.exists(weight_path):
shutil.rmtree(weight_path)
logger.info(f"[DELETE] 已删除权重: {weight_path}")
deleted = True
# 删除目录
shutil.rmtree(model_path)
logger.info(f"[DELETE] 已删除模型: {model_path}")
if not deleted:
# 也可能是老结构,直接在 saves 下的 model_name 目录
old_path = os.path.join(saves_path, model_name)
if os.path.exists(old_path):
shutil.rmtree(old_path)
logger.info(f"[DELETE] 已删除老结构权重: {old_path}")
deleted = True
return jsonify({'code': 0, 'message': '删除成功'})
if deleted:
return jsonify({'code': 0, 'message': '权重已删除'})
else:
return jsonify({'code': 1, 'message': f'权重不存在: {model_name}'})
else:
# 默认删除合并模型local_trained_models目录
model_path = os.path.join(PROJECT_ROOT, 'local_trained_models', model_name)
if not os.path.exists(model_path):
return jsonify({'code': 1, 'message': f'合并模型不存在: {model_name}'})
# 删除目录
shutil.rmtree(model_path)
logger.info(f"[DELETE] 已删除合并模型: {model_path}")
return jsonify({'code': 0, 'message': '合并模型已删除'})
except Exception as e:
logger.error(f"[DELETE] 删除模型失败: {str(e)}")
logger.error(f"[DELETE] 删除失败: {str(e)}")
return jsonify({'code': 1, 'message': f'删除失败: {str(e)}'})

View File

@@ -37,7 +37,25 @@ CONFIG = load_config()
TRAINING_LOGS_DIR = CONFIG.get('training_logs_path', '/app/base/training_logs')
# ============ 日志系统配置 ============
LOG_BASE_DIR = os.path.join(PROJECT_ROOT, 'logs')
# 日志目录逻辑:
# 1. 优先使用环境变量 LOG_BASE_DIR
# 2. 如果是容器环境(存在 /app/base使用 /app/base/logs
# 3. 否则使用本地项目路径 PROJECT_ROOT/logs
def get_log_base_dir():
"""获取日志基础目录"""
# 1. 检查环境变量
if 'LOG_BASE_DIR' in os.environ:
return os.environ['LOG_BASE_DIR']
# 2. 检查是否在容器环境中
mount_base = os.environ.get('MOUNT_BASE', '/app/base')
if os.path.exists(mount_base):
return os.path.join(mount_base, 'logs')
# 3. 使用本地项目路径
return os.path.join(PROJECT_ROOT, 'logs')
LOG_BASE_DIR = get_log_base_dir()
def setup_logger(name='app'):
@@ -98,6 +116,15 @@ def setup_logger(name='app'):
datefmt='%Y-%m-%d %H:%M:%S'
))
# 6. API日志处理器 - 专门记录API相关日志
api_log_path = os.path.join(log_dir, 'api.log')
api_handler = RotatingFileHandler(api_log_path, maxBytes=10*1024*1024, backupCount=10, encoding='utf-8')
api_handler.setLevel(logging.DEBUG)
api_handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
# 添加处理器到 logger
logger.addHandler(all_handler)
logger.addHandler(error_handler)
@@ -117,6 +144,13 @@ def setup_logger(name='app'):
train_logger.addHandler(train_handler)
train_logger.addHandler(console_handler)
# 为 logs_api 创建单独的 logger (供 src/api/logs.py 使用)
logs_api_logger = logging.getLogger('logs_api')
logs_api_logger.setLevel(logging.DEBUG)
logs_api_logger.handlers.clear()
logs_api_logger.addHandler(api_handler)
logs_api_logger.addHandler(console_handler)
return logger
@@ -588,6 +622,8 @@ def system_info():
# ============ 通用 CRUD 操作 ============
import json
def generic_get_all(table_name, order_by='create_time DESC'):
"""通用查询所有"""
conn = get_db_connection()
@@ -596,6 +632,19 @@ def generic_get_all(table_name, order_by='create_time DESC'):
result = cursor.fetchall()
cursor.close()
conn.close()
# 自动解析 JSON 字段
for row in result:
for key, value in row.items():
if isinstance(value, str) and value.startswith('[') and value.endswith(']'):
try:
row[key] = json.loads(value)
except:
pass
elif isinstance(value, str) and value.startswith('{') and value.endswith('}'):
try:
row[key] = json.loads(value)
except:
pass
return result
@@ -607,6 +656,19 @@ def generic_get_by_id(table_name, id_val):
result = cursor.fetchone()
cursor.close()
conn.close()
# 自动解析 JSON 字段
if result:
for key, value in result.items():
if isinstance(value, str) and value.startswith('[') and value.endswith(']'):
try:
result[key] = json.loads(value)
except:
pass
elif isinstance(value, str) and value.startswith('{') and value.endswith('}'):
try:
result[key] = json.loads(value)
except:
pass
return result
@@ -1053,36 +1115,19 @@ def delete_model_deploy(id):
return jsonify({'code': 0, 'message': '删除成功'})
# ============ 模型管理接口 ============
@app.route('/api/model-manage', methods=['GET'])
def get_model_manage():
return jsonify({'code': 0, 'data': generic_get_all('model_manage')})
@app.route('/api/model-manage', methods=['POST'])
def create_model_manage():
data = request.json
new_id = generic_create('model_manage', data)
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
@app.route('/api/model-manage/<int:id>', methods=['PUT'])
def update_model_manage(id):
data = request.json
generic_update('model_manage', id, data)
return jsonify({'code': 0, 'message': '更新成功'})
@app.route('/api/model-manage/<int:id>', methods=['DELETE'])
def delete_model_manage(id):
generic_delete('model_manage', id)
return jsonify({'code': 0, 'message': '删除成功'})
# ============ 模型对比接口 ============
@app.route('/api/model-compare', methods=['GET'])
def get_model_compare():
return jsonify({'code': 0, 'data': generic_get_all('model_compare')})
result = generic_get_all('model_compare')
# 确保 models 字段被正确解析为 JSON
for row in result:
if 'models' in row and isinstance(row['models'], str):
try:
row['models'] = json.loads(row['models'])
logger.debug(f"[model-compare] 解析 models 字段成功: {row['models']}")
except Exception as e:
logger.error(f"[model-compare] 解析 models 字段失败: {e}, 原始值: {row['models']}")
return jsonify({'code': 0, 'data': result})
@app.route('/api/model-compare/<int:id>', methods=['GET'])
@@ -1090,13 +1135,25 @@ def get_model_compare_by_id(id):
"""获取单个模型对比任务"""
result = generic_get_by_id('model_compare', id)
if result:
# 确保 models 字段被正确解析为 JSON
if 'models' in result and isinstance(result['models'], str):
try:
result['models'] = json.loads(result['models'])
except Exception as e:
logger.error(f"[model-compare] 解析 models 字段失败: {e}")
return jsonify({'code': 0, 'data': result})
return jsonify({'code': 1, 'message': '任务不存在'})
@app.route('/api/model-compare', methods=['POST'])
def create_model_compare():
data = request.json
data = request.json.copy()
# 字段映射: name -> model_name
if 'name' in data:
data['model_name'] = data.pop('name')
# 设置默认加载状态
if 'status' not in data:
data['status'] = 'pending'
new_id = generic_create('model_compare', data)
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
@@ -1110,10 +1167,278 @@ def update_model_compare(id):
@app.route('/api/model-compare/<int:id>', methods=['DELETE'])
def delete_model_compare(id):
# 先停止加载的模型服务
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT models, load_status FROM model_compare WHERE id = %s", (id,))
row = cursor.fetchone()
cursor.close()
conn.close()
if row and row[1] == 'loaded':
# 停止模型服务
from subprocess import Popen
import signal
load_status = json.loads(row[0]) if isinstance(row[0], str) else row[0]
if isinstance(load_status, dict) and 'processes' in load_status:
for proc in load_status['processes']:
if 'pid' in proc:
try:
os.kill(proc['pid'], signal.SIGTERM)
except:
pass
except Exception as e:
logger.error(f"[model-compare] 停止模型服务失败: {e}")
generic_delete('model_compare', id)
return jsonify({'code': 0, 'message': '删除成功'})
# ============ 模型加载接口 ============
import subprocess
import signal
# 存储加载状态 (生产环境应使用数据库或Redis)
model_compare_processes = {}
@app.route('/api/model-compare/<int:id>/load', methods=['POST'])
def load_model_compare(id):
"""加载模型 - 启动模型服务"""
try:
# 获取对比任务
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT models FROM model_compare WHERE id = %s", (id,))
row = cursor.fetchone()
cursor.close()
conn.close()
if not row:
return jsonify({'code': 1, 'message': '任务不存在'})
models = json.loads(row[0]) if isinstance(row[0], str) else row[0]
if not models or len(models) < 2:
return jsonify({'code': 1, 'message': '模型配置无效'})
# 更新状态为 loading
generic_update('model_compare', id, {'status': 'loading'})
# 启动模型服务
processes = []
loaded_models = []
for i, model in enumerate(models):
model_path = model.get('model_path', '')
gpu_id = model.get('gpu_id', 0)
port = model.get('port', 7862 + i * 10)
if not model_path:
continue
# 构建启动命令
# 获取模型名称和模板
model_name = model.get('model_name', '')
template = detect_template(model_path)
cmd = [
sys.executable,
'-m', 'llamafactory.api',
'--model_name_or_path', model_path,
'--template', template or 'default',
'--port', str(port),
'--gpu_ids', str(gpu_id)
]
logger.info(f"[model-compare] 启动模型服务: {' '.join(cmd)}")
# 启动进程
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=PROJECT_ROOT
)
processes.append({
'pid': proc.pid,
'port': port,
'model_name': model_name,
'model_path': model_path
})
loaded_models.append({
'port': port,
'model_name': model_name,
'status': 'starting'
})
# 保存进程信息
model_compare_processes[id] = {
'processes': processes,
'loaded_models': loaded_models,
'start_time': datetime.now().timestamp()
}
# 更新数据库中的加载状态
load_status = {
'processes': processes,
'loaded_models': loaded_models,
'started_at': datetime.now().isoformat()
}
generic_update('model_compare', id, {
'status': 'loading',
'load_status': json.dumps(load_status, ensure_ascii=False)
})
return jsonify({
'code': 0,
'message': '正在加载模型...',
'data': {
'models': loaded_models,
'check_url': f'/api/model-compare/{id}/load-status'
}
})
except Exception as e:
logger.error(f"[model-compare] 加载模型失败: {e}")
generic_update('model_compare', id, {'status': 'pending'})
return jsonify({'code': 1, 'message': f'加载失败: {str(e)}'})
def detect_template(model_path):
"""根据模型路径检测模板类型"""
model_lower = model_path.lower()
if 'qwen' in model_lower:
return 'qwen'
elif 'llama' in model_lower or 'llama' in model_lower:
return 'llama'
elif 'chatglm' in model_lower:
return 'chatglm'
elif 'baichuan' in model_lower:
return 'baichuan'
elif 'mistral' in model_lower:
return 'mistral'
elif 'yi' in model_lower:
return 'yi'
elif 'deepseek' in model_lower:
return 'deepseek'
return 'default'
@app.route('/api/model-compare/<int:id>/load-status', methods=['GET'])
def get_load_status(id):
"""获取模型加载状态"""
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT models, load_status FROM model_compare WHERE id = %s", (id,))
row = cursor.fetchone()
cursor.close()
conn.close()
if not row:
return jsonify({'code': 1, 'message': '任务不存在'})
models = json.loads(row[0]) if isinstance(row[0], str) else row[0]
load_status = json.loads(row[1]) if row[1] and isinstance(row[1], str) else {}
loaded_models = load_status.get('loaded_models', [])
processes = load_status.get('processes', [])
# 检查每个模型服务是否就绪
all_ready = True
for i, model in enumerate(loaded_models):
port = model.get('port')
try:
import requests
resp = requests.get(f'http://localhost:{port}/health', timeout=2)
if resp.status_code == 200:
model['status'] = 'ready'
else:
model['status'] = 'starting'
all_ready = False
except:
# 检查进程是否还在运行
if 'pid' in processes[i] if i < len(processes) else False:
model['status'] = 'starting'
all_ready = False
else:
model['status'] = 'failed'
all_ready = False
# 更新状态
if all_ready:
generic_update('model_compare', id, {'status': 'loaded'})
else:
generic_update('model_compare', id, {'status': 'loading'})
# 更新加载状态
load_status['loaded_models'] = loaded_models
load_status['all_ready'] = all_ready
generic_update('model_compare', id, {
'load_status': json.dumps(load_status, ensure_ascii=False)
})
return jsonify({
'code': 0,
'data': {
'status': 'loaded' if all_ready else 'loading',
'models': loaded_models,
'all_ready': all_ready
}
})
except Exception as e:
logger.error(f"[model-compare] 获取加载状态失败: {e}")
return jsonify({'code': 1, 'message': str(e)})
@app.route('/api/model-compare/<int:id>/unload', methods=['POST'])
def unload_model_compare(id):
"""停止加载的模型服务"""
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT load_status FROM model_compare WHERE id = %s", (id,))
row = cursor.fetchone()
cursor.close()
conn.close()
if row and row[0]:
load_status = json.loads(row[0]) if isinstance(row[0], str) else row[0]
processes = load_status.get('processes', [])
# 停止所有进程
for proc in processes:
pid = proc.get('pid')
if pid:
try:
os.kill(pid, signal.SIGTERM)
logger.info(f"[model-compare] 已停止进程 {pid}")
except ProcessLookupError:
pass
except Exception as e:
logger.warning(f"[model-compare] 停止进程 {pid} 失败: {e}")
# 清理内存
if id in model_compare_processes:
del model_compare_processes[id]
# 更新状态
generic_update('model_compare', id, {
'status': 'pending',
'load_status': None
})
return jsonify({'code': 0, 'message': '已停止模型服务'})
except Exception as e:
logger.error(f"[model-compare] 停止模型服务失败: {e}")
return jsonify({'code': 1, 'message': str(e)})
# ============ 数据生成接口 ============
@app.route('/api/data-generate', methods=['GET'])
def get_data_generate():

View File

@@ -120,15 +120,16 @@ def start_api():
print(f"❌ 找不到主程序文件: {main_py}")
return False
# 启动进程
# 启动进程(不重定向输出,让 Python logging 模块自己处理日志文件)
env = os.environ.copy()
env['PYTHONUNBUFFERED'] = '1'
log_file = open(os.devnull, 'w') # 忽略输出
proc = subprocess.Popen(
[str(venv_python), str(main_py)],
cwd=str(SCRIPT_DIR),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdout=log_file,
stderr=log_file,
text=True,
bufsize=1,
env=env
@@ -172,11 +173,12 @@ def start_web():
env = os.environ.copy()
env['PYTHONUNBUFFERED'] = '1'
log_file = open(os.devnull, 'w') # 忽略输出
proc = subprocess.Popen(
[sys.executable, '-m', 'http.server', str(web_port)],
cwd=str(web_root),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdout=log_file,
stderr=log_file,
text=True,
bufsize=1,
env=env

View File

@@ -63,11 +63,13 @@ start_api() {
return 1
fi
LOG_DIR="$SCRIPT_DIR/logs/$(date +%Y-%m-%d)"
mkdir -p "$LOG_DIR"
python src/main.py > "$LOG_DIR/api.log" 2>&1 &
# 设置日志目录环境变量
export LOG_BASE_DIR="$SCRIPT_DIR/logs"
# 不再重定向输出,让 Python logging 模块自己处理日志文件
nohup python src/main.py > /dev/null 2>&1 &
API_PID=$!
echo "✅ 后端服务已启动 (PID: $API_PID, 端口: $API_PORT)"
echo "✅ 后端服务已启动 (PID: $API_PID, 端口: $API_PORT, 日志目录: $LOG_BASE_DIR)"
echo "$API_PID" > /tmp/ygft_api.pid
}

157
web/css/main.css Normal file
View File

@@ -0,0 +1,157 @@
/* 主页面样式 - 从 main.html 分离 */
.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;
}
.table-row-hover:hover {
background-color: #f9fafb;
transition: background-color 0.2s;
}
.table-header-bg {
background-color: #fafafa !important;
}
.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;
}
.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;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
margin-bottom: 0.25rem;
}
.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;
}
.icon-option {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.icon-option:hover {
border-color: #1890ff;
background-color: rgba(24, 144, 255, 0.05);
}
.icon-option.selected {
border-color: #1890ff;
background-color: rgba(24, 144, 255, 0.1);
}
.tab-active {
background-color: rgba(24, 144, 255, 0.1);
color: #1890ff;
font-weight: 500;
}
.radio-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background-color: transparent;
transition: all 0.2s;
}
.upload-area:hover,
.upload-area.drag-over {
border-color: #1890ff;
background-color: rgba(24, 144, 255, 0.05);
}
.bg-primary {
background-color: #1890ff;
}
.text-primary {
color: #1890ff;
}
.border-primary {
border-color: #1890ff;
}
.text-danger {
color: #f5222d;
}
.hover\:bg-primary\/90:hover {
background-color: rgba(24, 144, 255, 0.9);
}
:root {
--primary: #1890ff;
--danger: #f5222d;
--success: #52c41a;
}
/* 侧边栏滑块动画 */
.sidebar-slider {
position: absolute;
width: 4px;
height: 0;
background-color: var(--primary);
border-radius: 0 2px 2px 0;
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
z-index: 10;
}
/* 菜单项相对定位 */
.nav-item-wrapper {
position: relative;
}
/* 选中项背景动画 */
.nav-link {
position: relative;
z-index: 1;
}

21
web/js/api.js Normal file
View File

@@ -0,0 +1,21 @@
/**
* API 配置模块
* 提供 API 基础地址和常用配置
*/
// API 基础地址配置
(function() {
if (typeof window.getApiBase !== 'function') {
window.getApiBase = () => {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
return `${protocol}//${hostname}:7861/api`;
};
}
if (typeof window.API_BASE === 'undefined') {
window.API_BASE = window.getApiBase();
}
})();
// 导出 API_BASE 供其他模块使用
window.API_BASE = window.getApiBase();

View File

@@ -0,0 +1,236 @@
/**
* 共享侧边栏加载器
* 动态加载侧边栏组件,支持高亮当前页面
*/
(function() {
'use strict';
// 侧边栏容器占位样式(防止加载时闪烁)
const containerStyles = `
#sidebar-container {
width: 16rem;
min-width: 16rem;
height: 100vh;
background-color: #001529;
flex-shrink: 0;
}
@media (max-width: 768px) {
#sidebar-container {
display: none;
}
}
`;
// 侧边栏样式
const sidebarStyles = `
.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) !important;
color: #1890ff !important;
border-left: 4px solid #1890ff;
}
.sidebar-slider {
position: absolute;
left: 0;
width: 4px;
height: 0;
background-color: #1890ff;
border-radius: 0 2px 2px 0;
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s ease;
pointer-events: none;
z-index: 10;
opacity: 0;
}
.nav-item-wrapper {
position: relative;
}
.nav-link {
position: relative;
z-index: 1;
}
`;
// 立即注入容器样式(防止闪烁)
(function injectContainerStyles() {
const styleEl = document.createElement('style');
styleEl.id = 'sidebar-container-styles';
styleEl.textContent = containerStyles;
document.head.appendChild(styleEl);
})();
// 根据当前页面路径确定活动页面
function getCurrentPage() {
const path = window.location.pathname;
const search = window.location.search;
const fileName = path.split('/').pop().replace('.html', '');
// 检查 URL 参数
const urlParams = new URLSearchParams(search);
const pageParam = urlParams.get('page');
if (pageParam) {
return pageParam;
}
// 根据文件名映射
const pageMap = {
'main': 'fine-tune',
'hardware': 'hardware',
'logs': 'logs',
'tools': 'tools',
'fine-tune-create': 'fine-tune',
'training-log': 'fine-tune',
'dataset-create': 'dataset-manage',
'dataset-preview': 'dataset-manage',
'model-manage': 'model-manage',
'model-manage-create': 'model-manage',
'model-eval': 'model-eval',
'model-eval-create': 'model-eval',
'model-compare-create': 'model-compare',
'model-compare-chat': 'model-compare',
'model-compare-result': 'model-compare',
'model-dimension-create': 'model-eval',
'model-inference': 'model-manage',
'custom-tool-create': 'tools'
};
return pageMap[fileName] || 'fine-tune';
}
// 加载侧边栏
async function loadSidebar() {
const container = document.getElementById('sidebar-container');
if (!container) {
console.warn('未找到 sidebar-container 元素');
return;
}
try {
// 计算组件路径
const currentPath = window.location.pathname;
let basePath = '';
if (currentPath.includes('/pages/')) {
basePath = 'components/';
} else {
basePath = 'pages/components/';
}
const response = await fetch(basePath + 'sidebar.html');
if (!response.ok) {
throw new Error('加载侧边栏失败: ' + response.status);
}
const html = await response.text();
container.innerHTML = html;
// 注入样式
if (!document.getElementById('sidebar-styles')) {
const styleEl = document.createElement('style');
styleEl.id = 'sidebar-styles';
styleEl.textContent = sidebarStyles;
document.head.appendChild(styleEl);
}
// 修正 logo 路径
const logo = container.querySelector('#sidebar-logo');
if (logo) {
const depth = currentPath.split('/pages/').length > 1 ? '../' : '';
logo.src = depth + 'assets/logo/logo.png';
}
// 高亮当前页面
const currentPage = window.sidebarCurrentPage || getCurrentPage();
highlightCurrentPage(currentPage);
// 初始化滑块
initSlider();
} catch (error) {
console.error('加载侧边栏出错:', error);
}
}
// 高亮当前页面
function highlightCurrentPage(currentPage) {
document.querySelectorAll('.nav-link').forEach(link => {
const page = link.dataset.page;
if (page === currentPage) {
link.classList.add('sidebar-item-active');
link.classList.remove('hover:bg-[#001529]/20', 'transition-colors');
} else {
link.classList.remove('sidebar-item-active');
link.classList.add('hover:bg-[#001529]/20', 'transition-colors');
}
});
}
// 初始化滑块
function initSlider() {
const slider = document.getElementById('sidebar-slider');
if (!slider) return;
// 找到当前活动项
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
if (activeLink) {
const wrapper = activeLink.closest('.nav-item-wrapper');
if (wrapper) {
updateSliderPosition(wrapper);
}
}
// 绑定导航点击事件
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', function(e) {
const wrapper = this.closest('.nav-item-wrapper');
if (wrapper) {
updateSliderPosition(wrapper);
}
});
});
}
// 更新滑块位置
function updateSliderPosition(targetWrapper) {
const slider = document.getElementById('sidebar-slider');
if (!slider || !targetWrapper) return;
const nav = document.querySelector('nav');
if (!nav) return;
const navRect = nav.getBoundingClientRect();
const wrapperRect = targetWrapper.getBoundingClientRect();
const top = wrapperRect.top - navRect.top + nav.scrollTop;
const height = wrapperRect.height;
slider.style.top = top + 'px';
slider.style.height = height + 'px';
slider.style.opacity = '1';
}
// 导出到全局
window.SidebarLoader = {
load: loadSidebar,
highlight: highlightCurrentPage,
getCurrentPage: getCurrentPage
};
// DOM 加载完成后自动加载侧边栏
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadSidebar);
} else {
loadSidebar();
}
})();

753
web/js/components/table.js Normal file
View File

@@ -0,0 +1,753 @@
/**
* 表格组件
* 处理表格渲染、数据操作等
*/
// 当前页面状态
window.currentPage = 'fine-tune';
window.currentParentPage = null;
window.selectedItems = new Set(); // 存储选中的项ID
window.currentPageData = []; // 存储当前页面数据
window.modelListCache = []; // 模型列表缓存
window.currentModelTab = 'config'; // 模型管理页面当前tab: 'config'=配置模型, 'trained'=训练模型
// 获取 API 数据
async function fetchData(url) {
const response = await fetch(url);
const result = await response.json();
if (result.code !== 0) {
throw new Error(result.message || '获取数据失败');
}
return result.data || [];
}
// 删除数据
async function deleteItem(api, id) {
// 如果是我的模型,提示删除合并模型
const confirmMessage = api === 'model-manage/trained-models'
? '确定要删除合并模型吗?权重文件不会删除。'
: '确定要删除这条记录吗?';
window.showConfirm('确认删除', confirmMessage, async () => {
try {
// 如果是我的模型调用删除合并模型的API
if (api === 'model-manage/trained-models') {
const response = await fetch(`${window.API_BASE}/model-manage/trained-models/${id}?type=merged`, {
method: 'DELETE'
});
const result = await response.json();
if (result.code === 0) {
window.showMessage('成功', '删除成功', 'success');
// 清除合并状态缓存
sessionStorage.removeItem('merge_status_' + id);
// 刷新当前页面
clearSelection();
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
if (activeLink && typeof window.loadPage === 'function') {
window.loadPage(activeLink.dataset.page);
}
} else {
window.showMessage('错误', result.message || '删除失败', 'error');
}
} else {
const response = await fetch(`${window.API_BASE}/${api}/${id}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.code === 0) {
// 刷新当前页面
clearSelection(); // 清除选中状态
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
if (activeLink && typeof window.loadPage === 'function') {
window.loadPage(activeLink.dataset.page);
}
} else {
window.showMessage('错误', result.message || '删除失败', 'error');
}
}
} catch (error) {
window.showMessage('错误', '删除失败: ' + error.message, 'error');
}
});
}
// 更新模型用途
async function updateModelPurpose(id, purpose) {
try {
const response = await fetch(`${window.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 && typeof window.loadPage === 'function') {
window.loadPage(activeLink.dataset.page);
}
} else {
window.showMessage('错误', result.message || '更新失败', 'error');
}
} catch (error) {
window.showMessage('错误', '更新失败: ' + error.message, 'error');
}
}
// 切换单个项的选中状态
function toggleItemSelection(id, api) {
if (window.selectedItems.has(id)) {
window.selectedItems.delete(id);
} else {
window.selectedItems.add(id);
}
// 重新渲染当前页面以更新UI
refreshCurrentPage();
}
// 切换全选/取消全选
function toggleSelectAll(checkbox, api) {
// 使用保存的当前页面数据
if (checkbox.checked) {
// 全选当前页面的所有数据(支持 name 或 id
window.currentPageData.forEach(item => window.selectedItems.add(item.name || item.id));
} else {
// 取消全选,移除当前页面所有数据的选中状态
window.currentPageData.forEach(item => window.selectedItems.delete(item.name || item.id));
}
refreshCurrentPage();
}
// 清除所有选中项
function clearSelection() {
window.selectedItems.clear();
refreshCurrentPage();
}
// 批量删除选中的项
function batchDeleteItems(api) {
if (window.selectedItems.size === 0) {
window.showMessage('提示', '请先选择要删除的项', 'warning');
return;
}
window.showConfirm('批量删除', `确定要删除选中的 ${window.selectedItems.size} 条记录吗?`, async () => {
const ids = Array.from(window.selectedItems);
let successCount = 0;
let failCount = 0;
for (const id of ids) {
try {
const response = await fetch(`${window.API_BASE}/${api}/${id}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.code === 0) {
successCount++;
} else {
failCount++;
}
} catch (error) {
failCount++;
}
}
clearSelection();
// 刷新当前页面
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
if (activeLink && typeof window.loadPage === 'function') {
window.loadPage(activeLink.dataset.page);
}
if (failCount === 0) {
window.showMessage('成功', `成功删除 ${successCount} 条记录`, 'success');
} else {
window.showMessage('部分失败', `成功删除 ${successCount} 条,${failCount} 条删除失败`, 'warning');
}
});
}
// 刷新当前页面(重新渲染)
function refreshCurrentPage() {
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
if (activeLink) {
const pageName = activeLink.dataset.page;
const config = window.tableConfigs[pageName];
if (config && (config.hasModelTabs || config.api === 'model-manage' || config.api === 'dataset-manage')) {
const container = document.getElementById('page-content');
if (container && typeof renderTablePage === 'function') {
container.innerHTML = renderTablePage(config, window.currentPageData);
// 恢复复选框状态
updateCheckboxStates();
}
}
}
}
// 更新复选框状态(保持选中状态)
function updateCheckboxStates() {
const checkboxes = document.querySelectorAll('tbody input[type="checkbox"]');
checkboxes.forEach(cb => {
const match = cb.getAttribute('onchange')?.match(/toggleItemSelection\((\d+)/) || cb.getAttribute('onchange')?.match(/toggleItemSelection\(([^,]+)/);
const id = match ? parseInt(match[1]) || match[1] : null;
if (id !== null && window.selectedItems.has(id)) {
cb.checked = true;
cb.closest('tr')?.classList.add('bg-blue-50');
} else {
cb.checked = false;
cb.closest('tr')?.classList.remove('bg-blue-50');
}
});
// 更新批量操作栏的显示状态
const batchActions = document.getElementById('batchActions');
if (batchActions) {
if (window.selectedItems.size > 0) {
batchActions.classList.remove('hidden');
batchActions.querySelector('strong').textContent = window.selectedItems.size;
} else {
batchActions.classList.add('hidden');
}
}
// 更新批量删除按钮
const batchDeleteBtn = document.querySelector('#batchActions button[onclick^="batchDeleteItems"]');
if (batchDeleteBtn) {
if (window.selectedItems.size > 0) {
batchDeleteBtn.innerHTML = `<i class="fa fa-trash mr-1"></i>批量删除 (${window.selectedItems.size})`;
} else {
batchDeleteBtn.innerHTML = `<i class="fa fa-trash mr-1"></i>批量删除 (0)`;
}
}
}
// 编辑数据集
function editItem(api, id) {
if (api === 'dataset-manage') {
// 跳转到数据集创建页面进行编辑
window.location.href = `dataset-create.html?id=${id}`;
} else if (api === 'model-manage') {
// 跳转到模型创建页面进行编辑
window.location.href = `model-manage-create.html?id=${id}`;
} else {
window.showMessage('提示', '编辑功能开发中...', 'info');
}
}
// 下载数据集(打包下载)
function downloadDataset(datasetId) {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
window.open(`${protocol}//${hostname}:7861/api/dataset-manage/download/${datasetId}`, '_blank');
}
// 开始模型对比
async function startCompare(id) {
// 跳转到模型对比聊天页面(通过主框架加载)
window.location.href = `main.html?page=model-compare-chat&id=${id}`;
}
// 筛选表格
function filterTable() {
const searchInput = document.getElementById('tableSearchInput');
if (!searchInput) return;
const keyword = searchInput.value.toLowerCase().trim();
const tbody = document.querySelector('#page-content table tbody');
if (!tbody) return;
const rows = tbody.querySelectorAll('tr');
rows.forEach(row => {
const text = row.querySelector('td')?.textContent?.toLowerCase() || '';
if (keyword === '' || text.includes(keyword)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
}
// 刷新表格数据 - 重新加载当前页面
window.loadTableData = function() {
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
if (activeLink && typeof window.loadPage === 'function') {
window.loadPage(activeLink.dataset.page);
}
};
// 合并模型权重(保留兼容)
window.viewTrainedModel = function(name, method, path) {
if (typeof window.startMerge === 'function') {
window.startMerge(name, method, path);
}
};
// 编辑模型
window.editModel = function(modelId) {
window.location.href = `model-manage-create.html?id=${modelId}`;
};
// 预览数据集
window.previewDataset = function(datasetId) {
window.location.href = `dataset-preview.html?id=${datasetId}`;
};
// 下载数据集
window.downloadDataset = function(datasetId) {
window.open(`${window.API_BASE}/dataset-manage/download/${datasetId}`, '_blank');
};
// 导出模型权重
function exportModel(modelName) {
window.open(`${window.API_BASE}/model-manage/trained-models/${encodeURIComponent(modelName)}/export`, '_blank');
}
// 删除已训练模型的权重
async function deleteTrainedWeight(modelName) {
window.showConfirm('确认删除', `确定要删除模型 "${modelName}" 的权重文件吗?合并模型不受影响。`, async () => {
try {
const response = await fetch(`${window.API_BASE}/model-manage/trained-models/${encodeURIComponent(modelName)}?type=lora`, {
method: 'DELETE'
});
const result = await response.json();
if (result.code === 0) {
// 只显示成功消息,不刷新表格(因为后端可能因为没有权重文件而不返回这条记录)
window.showMessage('成功', '权重已删除', 'success');
// 清除该模型的合并状态缓存,让前端重新从后端获取状态
sessionStorage.removeItem('merge_status_' + modelName);
sessionStorage.removeItem('merge_status_' + modelName + '_time');
} else {
window.showMessage('错误', result.message || '删除失败', 'error');
}
} catch (error) {
console.error('删除权重失败:', error);
window.showMessage('错误', '删除失败: ' + error.message, 'error');
}
});
}
// 切换模型管理tab
function switchModelTab(tab) {
window.currentModelTab = tab;
// 清除选中状态
clearSelection();
// 重新加载模型管理页面
window.loadPage('model-manage');
}
// ========== 渲染函数 ==========
// 渲染表格页面
function renderTablePage(config, data) {
// 如果是模型管理页面根据tab动态决定列和配置
let columns = config.columns || [];
let createButton = '';
let supportsMultiSelect = false;
let currentApi = config.api;
if (config.hasModelTabs) {
if (window.currentModelTab === 'config') {
// 配置模型tab
columns = [
{ title: '模型名称', key: 'name' },
{ title: '模型类型', key: 'type', render: (val) => {
const textMap = { 'LLM': '大语言模型', 'CV': '计算机视觉', 'NLP': '自然语言处理', 'Embedding': '向量模型', 'Other': '其他' };
const displayText = textMap[val] || val || '-';
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) => {
const textMap = { 'local': '本地模型', 'api': '在线模型', 'online': '在线模型' };
const displayText = textMap[val] || val || '-';
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: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
];
createButton = `
<button onclick="showCreateModal('model-manage')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
<i class="fa fa-plus mr-1"></i>新建模型
</button>
`;
supportsMultiSelect = true;
currentApi = 'model-manage';
} else {
// 训练模型tab
columns = [
{ title: '模型名称', key: 'name' },
{ title: '训练方法', key: 'train_methods', render: (val) => val && val[0] ? val[0].name : '-' },
{ title: '基座模型', key: 'base_model_path', render: (val) => `<span class="text-xs text-gray-500 truncate block" title="${val}">${val || '-'}</span>` },
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
];
supportsMultiSelect = true;
currentApi = 'model-manage/trained-models';
}
} else {
// 非模型管理页面,使用原始配置
columns = config.columns || columns;
createButton = config.api === 'dataset-manage' ? `
<button onclick="showCreateModal('${config.api}')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
<i class="fa fa-plus mr-1"></i>创建数据集
</button>
` : (config.api === 'fine-tune' ? `
<button onclick="navigateToPage('fine-tune-create')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
<i class="fa fa-plus mr-1"></i>新建调优任务
</button>
` : (config.api === 'model-compare' ? `
<button onclick="navigateToPage('model-compare-create')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
<i class="fa fa-plus mr-1"></i>新建对比
</button>
` : ''));
supportsMultiSelect = config.api === 'model-manage' || config.api === 'model-manage/trained-models' || config.api === 'dataset-manage' || config.api === 'fine-tune';
}
// 搜索框
const searchBox = (config.api === 'model-manage' || config.api === 'model-manage/trained-models' || config.api === 'dataset-manage' || config.api === 'fine-tune' || config.hasModelTabs) ? `
<div class="relative">
<input type="text" id="tableSearchInput" placeholder="搜索${config.title}..."
class="w-72 pl-9 pr-3 py-1.5 rounded border border-gray-300 text-sm focus:outline-none focus:border-primary focus:ring-1 focus:border-primary"
oninput="filterTable()">
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
` : '';
// 批量删除按钮(仅当有选中项时显示)
const batchDeleteButton = supportsMultiSelect && window.selectedItems.size > 0 ? `
<button onclick="batchDeleteItems('${currentApi}')" class="bg-red-500 text-white px-4 py-2 rounded text-sm hover:bg-red-600 transition-colors font-medium shadow-sm">
<i class="fa fa-trash mr-1"></i>批量删除 (${window.selectedItems.size})
</button>
` : '';
const hasData = data && data.length > 0;
// 多选列头
const selectAllHeader = supportsMultiSelect ? `
<th class="px-4 py-3 text-center font-medium w-10">
<input type="checkbox" class="w-4 h-4 text-primary rounded border-gray-300 cursor-pointer"
onchange="toggleSelectAll(this, '${currentApi}')">
</th>
` : '';
return `
<div class="bg-white rounded-lg shadow-sm mb-6">
<div class="flex items-center justify-between p-4 border-b border-gray-100">
<h2 class="text-lg font-medium">${config.title}</h2>
<div class="flex items-center space-x-3">
${searchBox}
${createButton}
</div>
</div>
${config.hasModelTabs ? `
<div class="px-4 border-b border-gray-100">
<div class="flex space-x-1" id="modelTabs">
<button onclick="switchModelTab('config')" class="tab-btn px-4 py-2 text-sm font-medium rounded-t-lg transition-colors ${window.currentModelTab === 'config' ? 'bg-primary text-white' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}">
<i class="fa fa-cog mr-1"></i>配置模型
</button>
<button onclick="switchModelTab('trained')" class="tab-btn px-4 py-2 text-sm font-medium rounded-t-lg transition-colors ${window.currentModelTab === 'trained' ? 'bg-primary text-white' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}">
<i class="fa fa-rocket mr-1"></i>训练模型
</button>
</div>
</div>
<style>
.tab-btn { position: relative; transition: all 0.2s; }
</style>
` : ''}
${supportsMultiSelect ? `
<div id="batchActions" class="px-4 py-2 bg-blue-50 border-b border-blue-100 flex items-center justify-between ${window.selectedItems.size > 0 ? '' : 'hidden'}">
<div class="flex items-center text-sm text-blue-700">
<span>已选择 <strong>${window.selectedItems.size}</strong> 项</span>
<button onclick="clearSelection()" class="ml-3 text-blue-500 hover:text-blue-700 underline">取消选择</button>
</div>
<div class="flex items-center space-x-2">
${batchDeleteButton}
</div>
</div>
` : ''}
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="table-header-bg text-gray-500 text-sm">
${selectAllHeader}
${columns.map(col => `<th class="px-4 py-3 text-center font-medium">${col.title}</th>`).join('')}
<th class="px-4 py-3 text-center font-medium">操作</th>
</tr>
</thead>
<tbody>
${hasData ? data.map(item => `
<tr class="border-b border-gray-100 table-row-hover ${window.selectedItems.has(item.name || item.id) ? 'bg-blue-50' : ''}">
${supportsMultiSelect ? `
<td class="px-4 py-4 text-sm text-center">
<input type="checkbox" class="w-4 h-4 text-primary rounded border-gray-300 cursor-pointer"
${window.selectedItems.has(item.name || item.id) ? 'checked' : ''}
onchange="toggleItemSelection('${item.name || item.id}', '${currentApi}')">
</td>
` : ''}
${columns.map(col => `
<td class="px-4 py-4 text-sm text-center">
${col.render ? col.render(item[col.key], item) : (item[col.key] || '-')}
</td>
`).join('')}
<td class="px-4 py-4 text-sm text-center">
<div class="flex justify-center space-x-2">
${config.api === 'fine-tune' ? `
<button onclick="viewFineTuneLogs('${item.id}', '${item.name}')" class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600">查看日志</button>
<button onclick="deleteItem('${config.api}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除任务</button>
` : (currentApi === 'model-manage/trained-models' ? `
${getMergeButtonHtml(item.name, item.train_methods?.[0]?.name || 'lora', item.base_model_path || '', item.merged, item.merging)}
<button onclick="deleteTrainedWeight('${item.name}')" class="bg-orange-500 text-white px-3 py-1 rounded text-xs hover:bg-orange-600" title="删除权重文件">删除权重</button>
${item.merged ? `
<button onclick="deleteItem('${currentApi}', '${item.name || item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600" title="删除合并模型">删除模型</button>
` : ''}
${(item.merged && !item.merging) ? `
<button onclick="exportModel('${item.name}')" class="bg-green-500 text-white px-3 py-1 rounded text-xs hover:bg-green-600">导出</button>
` : ''}
` : (currentApi === 'model-manage' ? `
<button onclick="editModel('${item.id}')" class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600">编辑</button>
<button onclick="deleteItem('${currentApi}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
` : (config.api === 'dataset-manage' ? `
<button onclick="previewDataset('${item.id}')" class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600">预览</button>
<button onclick="downloadDataset('${item.id}')" class="bg-green-500 text-white px-3 py-1 rounded text-xs hover:bg-green-600">下载</button>
<button onclick="deleteItem('${config.api}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
` : (config.api === 'model-compare' ? `
${getCompareButtonHtml(item.id, item.status)}
` : ''))))}
</div>
</td>
</tr>
`).join('') : ''}
</tbody>
</table>
</div>
${!hasData ? `
<div class="p-8 text-center text-gray-400">
<div class="py-12">
<i class="fa fa-inbox text-5xl mb-4 text-gray-300"></i>
<p class="text-gray-500">暂无数据</p>
</div>
</div>
` : ''}
</div>
`;
}
// 获取合并按钮HTML
function getMergeButtonHtml(name, method, path, merged, merging) {
const storageKey = 'merge_status_' + name;
const tempStatus = sessionStorage.getItem(storageKey);
const tempStatusTime = sessionStorage.getItem(storageKey + '_time');
console.log('[DEBUG] getMergeButtonHtml:', name, 'tempStatus:', tempStatus, 'merged:', merged, 'merging:', merging);
// 检查临时状态是否过期超过5分钟视为过期
const now = Date.now();
const statusExpired = tempStatusTime && (now - parseInt(tempStatusTime)) > 5 * 60 * 1000;
// 如果状态过期或无效,清除并视为无状态
if (statusExpired || (tempStatus && !tempStatusTime)) {
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey + '_time');
// 继续检查后端状态
} else if (tempStatus === 'merging') {
// 如果后端已经完成合并但前端状态未更新,清除临时状态
if (merged) {
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey + '_time');
} else {
return `<button class="bg-gray-300 text-gray-500 px-3 py-1 rounded text-xs cursor-not-allowed flex items-center" disabled>
<i class="fa fa-spinner fa-spin mr-1"></i>合并中...
</button>`;
}
}
// 如果后端返回正在合并中(锁文件存在)
if (merging) {
return `<button class="bg-gray-300 text-gray-500 px-3 py-1 rounded text-xs cursor-not-allowed flex items-center" disabled>
<i class="fa fa-spinner fa-spin mr-1"></i>合并中...
</button>`;
}
if (tempStatus === 'success' && merged) {
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey + '_time');
return `<button class="bg-gray-300 text-gray-500 px-3 py-1 rounded text-xs cursor-not-allowed" disabled>合并成功</button>`;
}
if (tempStatus === 'success' && !merged) {
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey + '_time');
}
if (merged) {
return `<button class="bg-gray-300 text-gray-500 px-3 py-1 rounded text-xs cursor-not-allowed" disabled>合并成功</button>`;
}
return `<button onclick="startMerge('${name}', '${method}', '${path}')" class="bg-primary text-white px-3 py-1 rounded text-xs hover:bg-primary/90">合并权重</button>`;
}
// 获取模型对比操作按钮HTML
function getCompareButtonHtml(id, status) {
// status: pending(未加载), loading(加载中), loaded(已加载)
if (status === 'loading') {
return `<button class="bg-yellow-500 text-white px-3 py-1 rounded text-xs cursor-not-allowed flex items-center" disabled>
<i class="fa fa-spinner fa-spin mr-1"></i>加载中...
</button>`;
}
if (status === 'loaded') {
return `
<button onclick="startCompare(${id})" class="bg-green-500 text-white px-3 py-1 rounded text-xs hover:bg-green-600 flex items-center">
<i class="fa fa-play mr-1"></i>开始对比
</button>
<button onclick="unloadCompare(${id})" class="bg-gray-500 text-white px-3 py-1 rounded text-xs hover:bg-gray-600">
停止
</button>
`;
}
// pending - 显示"准备加载"按钮
return `<button onclick="loadCompare(${id})" class="bg-primary text-white px-3 py-1 rounded text-xs hover:bg-primary/90">
<i class="fa fa-cloud-download mr-1"></i>准备加载
</button>`;
}
// 加载模型对比任务
async function loadCompare(id) {
try {
const response = await fetch(`${window.API_BASE}/model-compare/${id}/load`, {
method: 'POST'
});
const result = await response.json();
if (result.code === 0) {
window.showMessage('提示', '正在加载模型,请稍候...', 'info');
// 轮询加载状态
pollLoadStatus(id);
} else {
window.showMessage('错误', result.message || '加载失败', 'error');
}
} catch (error) {
console.error('加载模型失败:', error);
window.showMessage('错误', '加载失败: ' + error.message, 'error');
}
}
// 轮询加载状态
async function pollLoadStatus(id, maxAttempts = 60) {
let attempts = 0;
const checkInterval = 3000; // 3秒检查一次
const poll = async () => {
attempts++;
try {
const response = await fetch(`${window.API_BASE}/model-compare/${id}/load-status`);
const result = await response.json();
if (result.code === 0) {
const data = result.data;
if (data.all_ready) {
window.showMessage('成功', '模型加载完成!可以开始对比了。', 'success');
window.loadTableData(); // 刷新表格显示"开始对比"按钮
} else if (attempts < maxAttempts) {
setTimeout(poll, checkInterval);
} else {
window.showMessage('警告', '模型加载超时,请重试', 'warning');
}
} else if (attempts < maxAttempts) {
setTimeout(poll, checkInterval);
} else {
window.showMessage('错误', result.message || '加载状态检查失败', 'error');
}
} catch (error) {
console.error('检查加载状态失败:', error);
if (attempts < maxAttempts) {
setTimeout(poll, checkInterval);
}
}
};
setTimeout(poll, checkInterval);
}
// 停止模型对比任务
async function unloadCompare(id) {
window.showConfirm('确认停止', '确定要停止模型服务吗?', async () => {
try {
const response = await fetch(`${window.API_BASE}/model-compare/${id}/unload`, {
method: 'POST'
});
const result = await response.json();
if (result.code === 0) {
window.showMessage('成功', '已停止模型服务', 'success');
window.loadTableData();
} else {
window.showMessage('错误', result.message || '停止失败', 'error');
}
} catch (error) {
console.error('停止模型服务失败:', error);
window.showMessage('错误', '停止失败: ' + error.message, 'error');
}
});
}
// 启动合并任务
async function startMerge(name, method, path) {
const storageKey = 'merge_status_' + name;
sessionStorage.setItem(storageKey, 'merging');
sessionStorage.setItem(storageKey + '_time', Date.now().toString());
window.loadTableData();
try {
const response = await fetch(`${window.API_BASE}/model-manage/merge`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model_name: name,
train_method: method || 'lora',
base_model_path: path
})
});
const result = await response.json();
if (result.code === 0) {
sessionStorage.setItem(storageKey, 'success');
setTimeout(() => window.loadTableData(), 1500);
} else {
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey + '_time');
window.showMessage('失败', result.message || '合并失败', 'error');
window.loadTableData();
}
} catch (error) {
console.error('[DEBUG] 合并失败:', error);
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey + '_time');
window.showMessage('错误', '合并失败: ' + error.message, 'error');
window.loadTableData();
}
}
// 导出表格组件
window.TableComponent = {
fetchData,
deleteItem,
updateModelPurpose,
toggleItemSelection,
toggleSelectAll,
clearSelection,
batchDeleteItems,
refreshCurrentPage,
updateCheckboxStates,
editItem,
downloadDataset,
startCompare,
filterTable,
exportModel,
switchModelTab,
renderTablePage,
getMergeButtonHtml,
startMerge,
deleteTrainedWeight,
getCompareButtonHtml,
loadCompare,
unloadCompare
};

View File

@@ -0,0 +1,66 @@
/**
* 常量定义
* 集中管理所有硬编码的配置值
*/
// API 配置
window.API_CONFIG = {
PORT: 7861,
TIMEOUT: 30000, // 请求超时时间
// 系统监控
METRICS_INTERVAL: 30000, // 系统指标刷新间隔(ms)
// 训练进度
TRAINING_REFRESH_INTERVAL: 5000, // 训练进度刷新间隔(ms)
PROGRESS_BAR_WIDTH: 200, // 进度条宽度
// 合并操作
MERGE_TIMEOUT: 5 * 60 * 1000, // 合并状态超时时间(ms)
// 分页
PAGE_SIZE: 10,
// 消息提示
TOAST_DURATION: 3000, // toast提示显示时间(ms)
// 模型类型
MODEL_TYPES: {
'LLM': '大语言模型',
'CV': '计算机视觉',
'NLP': '自然语言处理',
'Embedding': '向量模型',
'Other': '其他'
},
// 用途映射
PURPOSE_MAP: {
'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' }
},
// 模型来源
SOURCE_MAP: {
'local': '本地模型',
'api': '在线模型',
'online': '在线模型'
},
// 训练方法显示名称
TRAIN_METHOD_MAP: {
'lora': 'LoRA',
'qlora': 'QLoRA',
'full': '全量微调',
'prefix': 'Prefix Tuning',
'adapter': 'Adapter',
'peft': 'PEFT',
'adalora': 'AdaLoRA',
'longlora': 'LongLoRA',
'dpo': 'DPO',
'cpt': 'CPT'
}
};
// 导出为常量引用方便使用
window.CONSTANTS = window.API_CONFIG;

597
web/js/main.js Normal file
View File

@@ -0,0 +1,597 @@
/**
* 主入口模块
* 页面初始化和导航控制
*/
// 各功能模块的表格配置
window.tableConfigs = {
'fine-tune': {
title: '模型调优',
api: 'fine-tune',
hasCreate: true,
createText: '创建训练任务',
columns: [
{ title: '任务名称', key: 'name' },
{ title: '任务状态', key: 'status', render: (val) => `<span class="px-2 py-1 rounded text-xs ${val === 'running' ? 'bg-green-100 text-green-700' : val === 'failed' ? 'bg-red-100 text-red-700' : 'bg-gray-100 text-gray-700'}">${val}</span>` },
{ title: '训练方式', key: 'train_type', render: (val) => val === 'SFT' ? 'SFT 微调训练' : (val === 'DPO' ? 'DPO 偏好训练' : (val === 'CPT' ? 'CPT 继续预训练' : '-')) },
{ title: '训练模板', key: 'template', render: (val) => val || '-' },
{ title: '基座模型', key: 'base_model', render: (val, row) => `<span class="model-name-cell" data-model-id="${val}">加载中...</span>` }
],
actions: ['stop', 'logs', 'delete']
},
'my-models': {
title: '我的模型',
api: 'model-manage/trained-models',
dataPath: 'models',
hasCreate: false,
columns: [
{ title: '模型名称', key: 'name' },
{ title: '训练方法', key: 'train_methods', render: (val) => val && val[0] ? val[0].name : '-' },
{ title: '基座模型', key: 'base_model_path', render: (val) => `<span class="text-xs text-gray-500 truncate block" title="${val}">${val || '-'}</span>` },
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
],
actions: ['view', 'delete']
},
'model-eval': {
title: '模型评测',
isExternalPage: true,
createConfig: {
page: 'model-eval-create',
hasCreate: true,
createText: '新建评测'
}
},
'model-compare': {
title: '模型对比',
api: 'model-compare',
hasCreate: true,
createText: '新建对比',
columns: [
{ title: '对比名称', key: 'model_name' },
{ title: '描述', key: 'description', render: (val) => val || '-' },
{ title: '相关模型', key: 'models', render: (val) => {
if (!val) return '-';
try {
// 如果是字符串,尝试解析 JSON
let models = val;
if (typeof val === 'string') {
try {
models = JSON.parse(val);
} catch {
models = val.split(',').map(id => ({ model_name: id.trim() }));
}
}
// 如果是数组,提取模型名称
if (Array.isArray(models) && models.length > 0) {
return models.map(function(m) {
if (typeof m === 'object' && m !== null) {
return m.model_name || m.name || '未知模型';
}
return String(m);
}).join(', ');
}
// 如果是单个对象
if (typeof models === 'object' && models !== null) {
return models.model_name || models.name || '未知模型';
}
return String(models);
} catch (e) {
return '解析错误';
}
}},
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
],
actions: ['compare', 'delete']
},
'dataset-manage': {
title: '数据集管理',
api: 'dataset-manage',
hasCreate: true,
createText: '上传数据集',
columns: [
{ title: '数据集名称', key: 'name' },
{ title: '数据类型', key: 'type', render: (val) => {
const textMap = {
'train': '训练数据',
'test': '测试数据',
'eval': '评测数据',
'val': '验证数据',
'other': '其他'
};
const displayText = textMap[val?.toLowerCase()] || val || '-';
return '<span class="px-2 py-1 rounded text-xs bg-blue-100 text-blue-700">' + displayText + '</span>';
}},
{ title: '存储位置', key: 'storage_type', render: (val) => {
const textMap = {
'local': '本地存储',
'minio': 'MinIO',
'cloud': '云存储'
};
const displayText = textMap[val] || val || '-';
return '<span class="px-2 py-1 rounded text-xs bg-green-100 text-green-700">' + displayText + '</span>';
}},
{ title: '大小', key: 'size', render: (val) => (val && val !== '0 B' && val !== '0') ? val : '-' },
{ title: '数据条数', key: 'count', render: (val) => val || 0 },
{ title: '描述', key: 'description', render: (val) => val || '-' },
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
],
actions: ['preview', 'download', 'delete']
},
'data-generate': {
title: '其他工具',
isTools: true,
defaultTools: [
{ id: 'data-generate', name: '数据生成', icon: 'fa-database', description: '基于LLM生成微调数据集' },
{ id: 'json2jsonl', name: 'JSON转JSONL', icon: 'fa-code', description: '将JSON文件转换为JSONL格式' },
{ id: 'md-convert', name: '转换Markdown', icon: 'fa-file-text', description: '将Markdown文件转换为训练数据' }
],
customTools: []
},
'model-manage': {
title: '模型管理',
api: 'model-manage',
hasCreate: true,
hasModelTabs: true,
createText: '添加模型',
columns: [
{ title: '模型名称', key: 'name' },
{ title: '模型类型', key: 'type', render: (val) => {
const textMap = {
'LLM': '大语言模型',
'CV': '计算机视觉',
'NLP': '自然语言处理',
'Embedding': '向量模型',
'Other': '其他'
};
const displayText = textMap[val] || val || '-';
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) => {
const textMap = {
'local': '本地模型',
'api': '在线模型',
'online': '在线模型'
};
const displayText = textMap[val] || val || '-';
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: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
],
actions: ['edit', 'delete']
},
'config': {
title: '平台性能',
skipFetch: true,
hasCreate: false,
isHardwareMonitor: true
},
'logs': {
title: '查看日志',
skipFetch: true,
hasCreate: false,
isLogViewer: true
},
'model-compare-chat': {
title: '模型对比',
skipFetch: true,
hasCreate: false,
isExternalPage: true
},
'model-compare-result': {
title: '对比结果',
skipFetch: true,
hasCreate: false,
isExternalPage: true
},
'training-log': {
title: '训练日志',
skipFetch: true,
hasCreate: false,
isExternalPage: true
}
};
// 操作按钮映射
window.actionLabels = {
'stop': '停止',
'logs': '查看日志',
'delete': '删除',
'deploy': '部署',
'eval': '评测',
'report': '查看报告',
'scale': '扩容',
'preview': '预览',
'download': '下载',
'detail': '详情',
'edit': '编辑',
'compare': '开始对话',
'chat': '对话',
'view': '合并权重'
};
// 加载模型列表缓存
async function loadModelListCache() {
try {
const response = await fetch(`${window.API_BASE}/model-manage`);
const result = await response.json();
if (result.code === 0) {
window.modelListCache = result.data || [];
}
} catch (e) {
console.error('加载模型列表失败:', e);
window.modelListCache = [];
}
}
// 根据模型ID获取模型名称同步版本
function getModelName(modelId) {
if (!modelId) return '-';
const model = window.modelListCache.find(m =>
m.id == modelId ||
m.id === String(modelId) ||
m.id === Number(modelId)
);
if (model) {
return model.name;
}
return `模型${modelId}`;
}
// 异步获取模型名称并更新 DOM
async function fetchAndUpdateModelName(modelId, cellElement) {
if (!modelId) {
cellElement.textContent = '-';
return;
}
let model = window.modelListCache.find(m =>
m.id == modelId ||
m.id === String(modelId) ||
m.id === Number(modelId)
);
if (!model) {
try {
const response = await fetch(`${window.API_BASE}/model-manage`);
const result = await response.json();
if (result.code === 0) {
window.modelListCache = result.data || [];
model = window.modelListCache.find(m =>
m.id == modelId ||
m.id === String(modelId) ||
m.id === Number(modelId)
);
}
} catch (e) {
console.error('获取模型列表失败:', e);
}
}
if (model) {
cellElement.textContent = model.name;
} else {
cellElement.textContent = `模型${modelId}`;
}
}
// 根据模型ID列表获取模型名称列表
function getModelNames(modelIds) {
if (!modelIds || !Array.isArray(modelIds)) return '-';
return modelIds.map(id => getModelName(id)).join(', ');
}
// 显示创建表单页面
window.showCreateModal = function(apiType) {
if (apiType === 'fine-tune') {
window.location.href = 'fine-tune-create.html';
} else if (apiType === 'model-manage') {
window.location.href = 'model-manage-create.html';
} else if (apiType === 'model-eval') {
window.location.href = 'model-eval-create.html';
} else if (apiType === 'dataset-manage') {
window.location.href = 'dataset-create.html';
} else if (apiType === 'model-compare') {
window.location.href = 'model-compare-create.html';
} else {
window.showMessage('提示', '该功能开发中...', 'info');
}
};
// 返回列表页
window.goBack = function() {
if (window.currentParentPage) {
window.currentPage = window.currentParentPage;
window.currentParentPage = null;
loadPage(window.currentPage);
} else {
loadPage('fine-tune');
}
};
// 跳转到页面
window.navigateToPage = function(pageName) {
if (pageName.endsWith('-create')) {
window.location.href = `${pageName}.html`;
} else {
window.location.href = `main.html?page=${pageName}`;
}
};
// 返回到列表页
window.goBackToList = function() {
navigateToPage('fine-tune');
};
// 加载页面内容
async function loadPage(pageName) {
// 切换页面时清除选中状态
TableComponent.clearSelection();
// 离开日志页面时停止自动刷新
SystemService.stopLogAutoRefresh();
// 离开模型调优页面时停止进度刷新
if (window.currentPage === 'fine-tune' && pageName !== 'fine-tune') {
TrainingService.stopProgressRefresh();
}
const container = document.getElementById('page-content');
const config = window.tableConfigs[pageName];
if (!config) return;
// 更新当前页面
window.currentPage = pageName;
// 显示加载中
container.innerHTML = `
<div class="bg-white rounded-lg shadow-sm mb-6 p-8 text-center">
<i class="fa fa-spinner fa-spin text-3xl text-primary"></i>
<p class="mt-2 text-gray-500">加载中...</p>
</div>
`;
// 显示/隐藏返回按钮
const backBtn = document.getElementById('pageBackBtn');
if (config.isExternalPage) {
backBtn.classList.remove('hidden');
} else {
backBtn.classList.add('hidden');
}
try {
if (config.isExternalPage) {
// 外部页面
const response = await fetch(`${pageName}.html?t=${Date.now()}`);
if (response.ok) {
const html = await response.text();
const scriptRegex = /<script\b(?![^>]*\bsrc)[^>]*>([\s\S]*?)<\/script>/g;
const scriptContents = [];
let match;
while ((match = scriptRegex.exec(html)) !== null) {
scriptContents.push(match[1]);
}
const scriptContent = scriptContents.join('\n');
const htmlWithoutScript = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, '');
let headerHtml = '';
if (config.createConfig && config.createConfig.hasCreate) {
headerHtml = `
<div class="bg-white rounded-lg shadow-sm mb-6 p-4 border-b border-gray-100 flex items-center justify-between">
<div class="flex items-center space-x-8">
<button class="tab-btn active pb-3 text-sm font-medium flex items-center text-primary" data-tab="tasks" onclick="switchTab(this, 'tasks')">
<i class="fa fa-tasks mr-2"></i>评测任务
</button>
<button class="tab-btn pb-3 text-sm font-medium flex items-center text-gray-500" data-tab="leaderboard" onclick="switchTab(this, 'leaderboard')">
<i class="fa fa-trophy mr-2"></i>排行榜
</button>
<button class="tab-btn pb-3 text-sm font-medium flex items-center text-gray-500" data-tab="dimensions" onclick="switchTab(this, 'dimensions')">
<i class="fa fa-sliders mr-2"></i>评测维度
</button>
</div>
<div id="headerActionButtons" style="min-height: 36px;">
<button onclick="navigateToPage('${config.createConfig.page}')" class="bg-primary text-white px-4 py-2 rounded-lg text-sm hover:bg-primary/90 transition-colors flex items-center">
<i class="fa fa-plus mr-2"></i>${config.createConfig.createText}
</button>
</div>
</div>
<style>
.tab-btn { position: relative; transition: all 0.2s; }
.tab-btn.active { color: #1890ff; }
.tab-btn.active::after { content: ''; position: absolute; bottom: -16px; left: 0; right: 0; height: 2px; background-color: #1890ff; }
.tab-btn:hover:not(.active) { color: #1890ff; }
</style>
`;
}
container.innerHTML = headerHtml + htmlWithoutScript;
if (scriptContent && scriptContent.trim()) {
try {
const oldScript = document.getElementById('externalPageScript');
if (oldScript) oldScript.remove();
const scriptEl = document.createElement('script');
scriptEl.id = 'externalPageScript';
scriptEl.textContent = scriptContent;
document.body.appendChild(scriptEl);
} catch (e) {
console.error('执行脚本失败:', e);
}
}
} else {
throw new Error('页面加载失败');
}
} else if (config.isHardwareMonitor) {
container.innerHTML = PageRenderer.renderConfigPage(config, null);
PageRenderer.initGPUList();
PageRenderer.startRefreshTimer();
} else if (config.isLogViewer) {
container.innerHTML = PageRenderer.renderLogViewerPage(config);
SystemService.initLogViewer();
} else if (config.isForm) {
const data = await TableComponent.fetchData(`${window.API_BASE}/${config.api}`);
container.innerHTML = PageRenderer.renderConfigPage(config, data);
} else if (config.isTools) {
container.innerHTML = PageRenderer.renderToolsPage(config);
} else {
// 模型管理页面根据tab选择不同的API
let apiUrl = `${window.API_BASE}/${config.api}`;
if (config.hasModelTabs) {
if (window.currentModelTab === 'trained') {
apiUrl = `${window.API_BASE}/model-manage/trained-models`;
}
}
let data = await TableComponent.fetchData(apiUrl);
let dataPath = config.dataPath || null;
if (config.hasModelTabs && window.currentModelTab === 'trained') {
dataPath = 'models';
}
if (dataPath && typeof data === 'object' && data !== null) {
data = data[dataPath] || [];
}
window.currentPageData = data;
container.innerHTML = TableComponent.renderTablePage(config, data);
setTimeout(() => {
const modelCells = container.querySelectorAll('.model-name-cell');
modelCells.forEach(cell => {
const modelId = cell.getAttribute('data-model-id');
if (modelId) {
fetchAndUpdateModelName(modelId, cell);
}
});
}, 0);
}
} catch (error) {
console.error('加载数据失败:', error);
container.innerHTML = `
<div class="bg-white rounded-lg shadow-sm mb-6 p-8 text-center">
<i class="fa fa-exclamation-circle text-3xl text-danger"></i>
<p class="mt-2 text-gray-500">加载数据失败,请检查后端服务是否启动</p>
<p class="text-sm text-gray-400 mt-1">${error.message}</p>
</div>
`;
}
}
// 添加评测维度
window.addDimension = function() {
window.location.href = 'model-dimension-create.html';
};
// 删除评测维度
window.deleteDimension = async function(id) {
window.showConfirm('确认删除', '确定要删除此评测维度吗?', async () => {
try {
const response = await fetch(`${window.API_BASE}/dimension/${id}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.code === 0) {
window.showMessage('成功', '删除成功', 'success', () => {
switchTab(document.querySelector('[data-tab="dimensions"]'), 'dimensions');
});
} else {
window.showMessage('错误', result.message || '删除失败', 'error');
}
} catch (error) {
console.error('删除维度失败:', error);
window.showMessage('错误', '删除失败: ' + error.message, 'error');
}
});
};
// 切换 Tab
window.switchTab = function(btn, tabId) {
const parent = btn.parentElement;
parent.querySelectorAll('.tab-btn').forEach(b => {
b.classList.remove('active', 'text-primary');
b.classList.add('text-gray-500');
});
btn.classList.add('active');
btn.classList.remove('text-gray-500');
if (typeof window.switchTabContent === 'function') {
window.switchTabContent(tabId);
}
const btnContainer = document.getElementById('headerActionButtons');
const currentConfig = window.tableConfigs[window.currentPage];
if (btnContainer) {
if (tabId === 'tasks') {
const page = currentConfig?.createConfig?.page || 'model-eval-create';
const text = currentConfig?.createConfig?.createText || '新建评测';
btnContainer.innerHTML = `
<button onclick="navigateToPage('${page}')" class="bg-primary text-white px-4 py-2 rounded-lg text-sm hover:bg-primary/90 transition-colors flex items-center">
<i class="fa fa-plus mr-2"></i>${text}
</button>
`;
} else if (tabId === 'leaderboard') {
btnContainer.innerHTML = '<span class="invisible px-4 py-2 rounded-lg">占位</span>';
} else if (tabId === 'dimensions') {
btnContainer.innerHTML = `
<button onclick="addDimension()" class="bg-primary text-white px-4 py-2 rounded-lg text-sm hover:bg-primary/90 transition-colors flex items-center">
<i class="fa fa-plus mr-2"></i>添加维度
</button>
`;
}
}
};
// ============ 初始化 ============
document.addEventListener('DOMContentLoaded', function() {
// 从 localStorage 加载自定义工具
const savedCustomTools = localStorage.getItem('customTools');
if (savedCustomTools) {
window.tableConfigs['data-generate'].customTools = JSON.parse(savedCustomTools);
}
// 加载模型列表缓存
loadModelListCache();
// 检查URL参数
const urlParams = new URLSearchParams(window.location.search);
const pageParam = urlParams.get('page');
let defaultPage = 'fine-tune';
if (pageParam) {
defaultPage = pageParam;
} else {
const sessionPage = sessionStorage.getItem('lastPage');
const localPage = localStorage.getItem('lastPage');
const savedPage = sessionPage || localPage;
if (savedPage && window.tableConfigs[savedPage]) {
defaultPage = savedPage;
}
}
sessionStorage.setItem('lastPage', defaultPage);
// 加载页面
loadPage(defaultPage);
// 启动系统监控定时器
SystemService.fetchSystemMetrics();
setInterval(SystemService.fetchSystemMetrics, 30000);
// 启动训练进度自动刷新
TrainingService.startProgressRefresh();
// 初始化日志
const path = window.location.pathname;
const pageName = path.split('/').pop().replace('.html', '') || 'main';
webLogger.init(pageName);
webLogger.info('页面加载完成');
});

723
web/js/pages/render.js Normal file
View File

@@ -0,0 +1,723 @@
/**
* 页面渲染模块
* 包含各类型页面的渲染函数
*/
// 渲染日志查看页面
function renderLogViewerPage(config) {
return `
<div class="bg-white rounded-lg shadow-sm mb-6">
<div class="flex items-center justify-between p-4 border-b border-gray-100">
<h2 class="text-lg font-medium">${config.title}</h2>
<div class="flex items-center space-x-2">
<button onclick="SystemService.refreshLogs()" class="px-3 py-1.5 text-sm bg-primary text-white rounded hover:bg-primary/90 transition-colors">
<i class="fa fa-refresh mr-1"></i>刷新
</button>
</div>
</div>
<div class="p-4">
<!-- 日志类型切换 -->
<div class="flex items-center mb-4">
<div class="flex bg-gray-100 rounded-lg p-1">
<button id="logTabSystem" onclick="SystemService.switchLogTab('system')" class="px-4 py-1.5 text-sm rounded-md transition-colors bg-white shadow-sm text-primary">
系统日志
</button>
<button id="logTabTraining" onclick="SystemService.switchLogTab('training')" class="px-4 py-1.5 text-sm rounded-md transition-colors text-gray-600 hover:text-gray-800">
训练日志
</button>
</div>
</div>
<!-- 系统日志选项 -->
<div id="systemLogOptions">
<!-- 日期选择 -->
<div class="flex items-center flex-wrap gap-4 mb-4">
<div class="flex items-center">
<label class="text-sm text-gray-600 mr-3">选择日期:</label>
<input type="date" id="logDatePicker" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none" onchange="SystemService.loadLogFiles()">
</div>
<div class="flex items-center">
<label class="text-sm text-gray-600 mr-3">自动刷新:</label>
<select id="logRefreshInterval" onchange="SystemService.setRefreshInterval()" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<option value="0">关闭</option>
<option value="5">5秒</option>
<option value="10" selected>10秒</option>
<option value="30">30秒</option>
<option value="60">60秒</option>
</select>
</div>
<div id="logRefreshCountdown" class="text-sm text-gray-500 hidden">
<i class="fa fa-clock-o mr-1"></i><span>下次刷新: <span id="countdownNumber">10</span>秒</span>
</div>
</div>
<!-- 日志类型选择 -->
<div class="flex items-center mb-4">
<label class="text-sm text-gray-600 mr-3">日志类型:</label>
<select id="logTypeSelect" onchange="SystemService.loadSelectedLog()" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<option value="">请选择日志文件</option>
</select>
</div>
</div>
<!-- 训练日志选项(初始隐藏) -->
<div id="trainingLogOptions" class="hidden">
<div class="flex items-center mb-4">
<label class="text-sm text-gray-600 mr-3">训练日志:</label>
<select id="trainingLogSelect" onchange="SystemService.loadSelectedTrainingLog()" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none flex-1">
<option value="">请选择训练日志</option>
</select>
</div>
</div>
<!-- 日志内容显示 -->
<div class="border border-gray-200 rounded-lg">
<div class="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-200">
<span class="text-sm text-gray-600" id="logFileInfo">请选择日志文件</span>
<div class="flex items-center space-x-3">
<input type="text" id="logSearchInput" placeholder="搜索日志..." oninput="SystemService.filterLogContent()" class="px-2 py-1 text-xs border border-gray-300 rounded focus:border-primary focus:outline-none">
<span id="logMatchCount" class="text-xs text-gray-500"></span>
<button onclick="SystemService.clearLogContent()" class="text-xs text-gray-500 hover:text-primary">清空</button>
</div>
</div>
<pre id="logContent" class="p-4 text-xs font-mono bg-gray-900 text-gray-100 overflow-auto max-h-[600px]" style="white-space: pre-wrap; word-wrap: break-word;">日志内容将在这里显示...</pre>
</div>
</div>
</div>
`;
}
// 渲染工具卡片页面
function renderToolsPage(config) {
const renderToolCard = (tool, canDelete = false, isCustom = false) => `
<div class="relative group border border-gray-200 rounded-lg p-6 cursor-pointer hover:border-primary hover:shadow-md transition-all" onclick="navigateToTool('${tool.id}', '${tool.url || ''}', ${isCustom})">
<div class="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
<i class="fa ${tool.icon} text-xl text-primary"></i>
</div>
<h3 class="text-base font-medium text-gray-800 mb-2">${tool.name}</h3>
<p class="text-sm text-gray-500">${tool.description}</p>
${canDelete ? `
<button onclick="event.stopPropagation(); editCustomTool('${tool.id}')" class="absolute top-2 right-10 w-6 h-6 rounded-full bg-gray-100 hover:bg-blue-100 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity z-10" title="修改">
<i class="fa fa-pencil text-gray-400 hover:text-blue-500 text-xs"></i>
</button>
<button onclick="event.stopPropagation(); deleteCustomTool('${tool.id}')" class="absolute top-2 right-2 w-6 h-6 rounded-full bg-gray-100 hover:bg-red-100 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity z-10" title="删除">
<i class="fa fa-times text-gray-400 hover:text-red-500 text-xs"></i>
</button>
` : ''}
</div>
`;
const defaultCards = config.defaultTools.map(t => renderToolCard(t, false, false)).join('');
const customCards = config.customTools.map(t => renderToolCard(t, true, true)).join('');
return `
<div class="bg-white rounded-lg shadow-sm mb-6">
<div class="flex items-center justify-between p-4 border-b border-gray-100">
<h2 class="text-lg font-medium">${config.title}</h2>
<button onclick="showCreateToolModal()" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
<i class="fa fa-plus mr-1"></i>添加自定义工具
</button>
</div>
<div class="p-6">
<!-- 默认工具 -->
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">默认工具</h3>
<div class="grid grid-cols-3 gap-6 mb-8">
${defaultCards || '<p class="text-gray-400 text-sm col-span-3">暂无默认工具</p>'}
</div>
<!-- 自定义工具 -->
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">自定义工具</h3>
<div class="grid grid-cols-3 gap-6">
${customCards || '<p class="text-gray-400 text-sm col-span-3">暂无自定义工具,点击右上角添加</p>'}
</div>
</div>
</div>
`;
}
// 删除自定义工具
function deleteCustomTool(toolId) {
window.showConfirm('确认删除', '确定要删除这个自定义工具吗?', () => {
const config = window.tableConfigs['data-generate'];
config.customTools = config.customTools.filter(t => t.id !== toolId);
localStorage.setItem('customTools', JSON.stringify(config.customTools));
document.getElementById('page-content').innerHTML = renderToolsPage(config);
});
}
// 修改自定义工具
function editCustomTool(toolId) {
const config = window.tableConfigs['data-generate'];
const tool = config.customTools.find(t => t.id === toolId);
if (tool) {
localStorage.setItem('editTool', JSON.stringify(tool));
window.location.href = 'custom-tool-create.html?edit=true';
}
}
// 显示创建工具弹窗
function showCreateToolModal() {
window.location.href = 'custom-tool-create.html';
}
// 跳转到工具页面
function navigateToTool(toolId, url, isCustom = false) {
if (isCustom && url) {
if (url.startsWith('http')) {
window.open(url, '_blank');
} else {
window.location.href = url;
}
} else if (toolId === 'data-generate') {
window.location.href = 'data-generate.html';
} else if (toolId === 'json2jsonl') {
window.showMessage('提示', 'JSON转JSONL 工具开发中...', 'info');
} else if (toolId === 'md-convert') {
window.showMessage('提示', '转换Markdown 工具开发中...', 'info');
} else {
window.showMessage('提示', `${toolId} 功能开发中...`, 'info');
}
}
// 渲染配置页面(硬件监控)
function renderConfigPage(config, data) {
return `
<div class="bg-white rounded-lg shadow-sm mb-6">
<div class="flex items-center justify-between p-4 border-b border-gray-100">
<h2 class="text-lg font-medium">${config.title}</h2>
<div class="flex items-center text-sm text-gray-500">
<i class="fa fa-refresh mr-2"></i>
<span class="mr-2">刷新频率:</span>
<select id="refreshInterval" onchange="changeRefreshRate()" class="px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<option value="1000">1秒</option>
<option value="3000">3秒</option>
<option value="5000" selected>5秒</option>
</select>
</div>
</div>
<div class="p-6">
<!-- 第一行CPU、内存、磁盘 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<!-- CPU监控 -->
<div class="border border-gray-200 rounded-lg p-5">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
<div class="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center mr-3">
<i class="fa fa-microchip text-blue-600 text-lg"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-800">CPU 使用率</h3>
<p class="text-xs text-gray-500" id="cpuCores">4 核心</p>
</div>
</div>
<div class="text-right">
<span class="text-2xl font-medium text-gray-800" id="cpuPercent">0%</span>
</div>
</div>
<div class="relative h-4 bg-gray-100 rounded-full overflow-hidden">
<div id="cpuBar" class="absolute left-0 top-0 h-full bg-gradient-to-r from-green-400 to-blue-500 transition-all duration-500" style="width: 0%"></div>
</div>
<div class="mt-4 grid grid-cols-4 gap-2" id="cpuCoresList">
<div class="text-center p-2 bg-gray-50 rounded">
<div class="text-xs text-gray-500">核心1</div>
<div class="text-sm font-medium text-gray-800" id="core1">0%</div>
</div>
<div class="text-center p-2 bg-gray-50 rounded">
<div class="text-xs text-gray-500">核心2</div>
<div class="text-sm font-medium text-gray-800" id="core2">0%</div>
</div>
<div class="text-center p-2 bg-gray-50 rounded">
<div class="text-xs text-gray-500">核心3</div>
<div class="text-sm font-medium text-gray-800" id="core3">0%</div>
</div>
<div class="text-center p-2 bg-gray-50 rounded">
<div class="text-xs text-gray-500">核心4</div>
<div class="text-sm font-medium text-gray-800" id="core4">0%</div>
</div>
</div>
</div>
<!-- 内存监控 -->
<div class="border border-gray-200 rounded-lg p-5">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
<div class="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center mr-3">
<i class="fa fa-database text-purple-600 text-lg"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-800">内存使用</h3>
<p class="text-xs text-gray-500" id="memoryTotal">总计: 16 GB</p>
</div>
</div>
<div class="text-right">
<span class="text-2xl font-medium text-gray-800" id="memoryPercent">0%</span>
</div>
</div>
<div class="relative h-4 bg-gray-100 rounded-full overflow-hidden">
<div id="memoryBar" class="absolute left-0 top-0 h-full bg-gradient-to-r from-yellow-400 to-orange-500 transition-all duration-500" style="width: 0%"></div>
</div>
<div class="mt-4 flex justify-between text-sm">
<div class="text-center">
<div class="text-xs text-gray-500">已用</div>
<div class="text-sm font-medium text-gray-800" id="memoryUsed">0 GB</div>
</div>
<div class="text-center">
<div class="text-xs text-gray-500">可用</div>
<div class="text-sm font-medium text-gray-800" id="memoryAvailable">0 GB</div>
</div>
<div class="text-center">
<div class="text-xs text-gray-500">缓存</div>
<div class="text-sm font-medium text-gray-800" id="memoryCached">0 GB</div>
</div>
</div>
</div>
<!-- 磁盘监控 -->
<div class="border border-gray-200 rounded-lg p-5">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
<div class="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center mr-3">
<i class="fa fa-hdd-o text-green-600 text-lg"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-800">磁盘使用</h3>
<p class="text-xs text-gray-500" id="diskTotal">SSD 512 GB</p>
</div>
</div>
<div class="text-right">
<span class="text-2xl font-medium text-gray-800" id="diskPercent">0%</span>
</div>
</div>
<div class="relative h-4 bg-gray-100 rounded-full overflow-hidden">
<div id="diskBar" class="absolute left-0 top-0 h-full bg-gradient-to-r from-emerald-400 to-teal-500 transition-all duration-500" style="width: 0%"></div>
</div>
<div class="mt-4 grid grid-cols-3 gap-2 text-center">
<div class="p-2 bg-gray-50 rounded">
<div class="text-xs text-gray-500">已用空间</div>
<div class="text-sm font-medium text-gray-800" id="diskUsed">0 GB</div>
</div>
<div class="p-2 bg-gray-50 rounded">
<div class="text-xs text-gray-500">可用空间</div>
<div class="text-sm font-medium text-gray-800" id="diskAvailable">0 GB</div>
</div>
<div class="p-2 bg-gray-50 rounded">
<div class="text-xs text-gray-500">读写速度</div>
<div class="text-sm font-medium text-gray-800" id="diskSpeed">0 MB/s</div>
</div>
</div>
</div>
</div>
<!-- 第二行GPU监控 -->
<div class="mb-6">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-red-100 flex items-center justify-center mr-3">
<i class="fa fa-microchip text-red-600 text-lg"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-800">GPU监控</h3>
<p class="text-xs text-gray-500" id="gpuCount">多GPU并行监控</p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4" id="gpuList" style="max-height: 400px; overflow-y: auto;">
<!-- GPU卡片由initGPUList()动态生成 -->
</div>
</div>
<!-- 第三行:网络和系统 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 网络流量 -->
<div class="border border-gray-200 rounded-lg p-5">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-cyan-100 flex items-center justify-center mr-3">
<i class="fa fa-globe text-cyan-600 text-lg"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-800">网络流量</h3>
<p class="text-xs text-gray-500">实时带宽使用</p>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="p-3 bg-blue-50 rounded-lg">
<div class="flex items-center mb-2">
<i class="fa fa-arrow-down text-blue-600 mr-2"></i>
<span class="text-xs text-gray-600">下载速度</span>
</div>
<div class="text-lg font-medium text-gray-800" id="downloadSpeed">0 MB/s</div>
</div>
<div class="p-3 bg-green-50 rounded-lg">
<div class="flex items-center mb-2">
<i class="fa fa-arrow-up text-green-600 mr-2"></i>
<span class="text-xs text-gray-600">上传速度</span>
</div>
<div class="text-lg font-medium text-gray-800" id="uploadSpeed">0 MB/s</div>
</div>
</div>
<div class="mt-4 flex justify-between text-xs text-gray-500">
<span>总流入: <span id="totalDownload">0 GB</span></span>
<span>总流出: <span id="totalUpload">0 GB</span></span>
</div>
</div>
<!-- 系统信息 -->
<div class="border border-gray-200 rounded-lg p-5">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-indigo-100 flex items-center justify-center mr-3">
<i class="fa fa-info-circle text-indigo-600 text-lg"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-800">系统信息</h3>
<p class="text-xs text-gray-500">服务器状态</p>
</div>
</div>
<div class="space-y-3 text-sm">
<div class="flex justify-between py-2 border-b border-gray-100">
<span class="text-gray-500">操作系统</span>
<span class="text-gray-800 font-medium" id="osInfo">Ubuntu 22.04 LTS</span>
</div>
<div class="flex justify-between py-2 border-b border-gray-100">
<span class="text-gray-500">运行时间</span>
<span class="text-gray-800 font-medium" id="uptime">0 天 0 时 0 分</span>
</div>
<div class="flex justify-between py-2 border-b border-gray-100">
<span class="text-gray-500">进程数</span>
<span class="text-gray-800 font-medium" id="processCount">0</span>
</div>
<div class="flex justify-between py-2">
<span class="text-gray-500">负载均值</span>
<span class="text-gray-800 font-medium" id="loadAvg">0.00, 0.00, 0.00</span>
</div>
</div>
</div>
</div>
</div>
</div>
`;
}
// 刷新间隔定时器
let refreshTimer = null;
let currentRefreshInterval = 5000;
// 刷新硬件信息
async function refreshHardwareInfo() {
try {
const response = await fetch(`${window.API_BASE}/system-info`);
const result = await response.json();
if (result.code === 0 && result.data) {
const data = result.data;
// 更新CPU
const cpu = data.cpu || {};
const cpuPercent = cpu.percent || 0;
const cpuEl = document.getElementById('cpuPercent');
if (cpuEl) {
cpuEl.textContent = cpuPercent + '%';
document.getElementById('cpuBar').style.width = cpuPercent + '%';
document.getElementById('cpuCores').textContent = (cpu.cores || 0) + ' 核心';
}
// 更新内存
const mem = data.memory || {};
const memUsed = mem.used_gb || 0;
const memTotal = mem.total_gb || 0;
const memPercent = mem.percent || 0;
document.getElementById('memoryPercent').textContent = memPercent + '%';
document.getElementById('memoryBar').style.width = memPercent + '%';
document.getElementById('memoryUsed').textContent = memUsed + ' GB';
document.getElementById('memoryAvailable').textContent = (mem.available_gb || 0) + ' GB';
document.getElementById('memoryCached').textContent = (mem.cached_gb || 0) + ' GB';
// 更新磁盘
const disk = data.disk || {};
const diskUsed = disk.used_gb || 0;
const diskTotal = disk.total_gb || 0;
const diskPercent = disk.percent || 0;
document.getElementById('diskPercent').textContent = diskPercent + '%';
document.getElementById('diskBar').style.width = diskPercent + '%';
document.getElementById('diskUsed').textContent = diskUsed + ' GB';
document.getElementById('diskAvailable').textContent = (diskTotal - diskUsed) + ' GB';
// 更新网络
const net = data.network || {};
document.getElementById('totalDownload').textContent = (net.download_mb || 0) + ' GB';
document.getElementById('totalUpload').textContent = (net.upload_mb || 0) + ' GB';
// 更新系统信息
const sys = data.system || {};
const uptime = sys.uptime_seconds || 0;
const days = Math.floor(uptime / 86400);
const hours = Math.floor((uptime % 86400) / 3600);
const mins = Math.floor((uptime % 3600) / 60);
document.getElementById('uptime').textContent = days + ' 天 ' + hours + ' 时 ' + mins + ' 分';
document.getElementById('processCount').textContent = sys.process_count || 0;
// 更新GPU信息
updateGPUInfo(data.gpu || []);
}
} catch (error) {
console.error('获取系统信息失败:', error);
useMockData();
}
}
// 使用模拟数据
function useMockData() {
const cpuUsage = Math.floor(Math.random() * 30) + 20;
document.getElementById('cpuPercent').textContent = cpuUsage + '%';
document.getElementById('cpuBar').style.width = cpuUsage + '%';
document.getElementById('core1').textContent = Math.floor(Math.random() * 40 + 20) + '%';
document.getElementById('core2').textContent = Math.floor(Math.random() * 40 + 15) + '%';
document.getElementById('core3').textContent = Math.floor(Math.random() * 40 + 25) + '%';
document.getElementById('core4').textContent = Math.floor(Math.random() * 40 + 10) + '%';
const memUsed = (Math.random() * 4 + 6).toFixed(1);
const memTotal = 16;
const memPercent = Math.floor((memUsed / memTotal) * 100);
document.getElementById('memoryPercent').textContent = memPercent + '%';
document.getElementById('memoryBar').style.width = memPercent + '%';
document.getElementById('memoryUsed').textContent = memUsed + ' GB';
document.getElementById('memoryAvailable').textContent = (memTotal - memUsed).toFixed(1) + ' GB';
document.getElementById('memoryCached').textContent = (Math.random() * 3 + 1).toFixed(1) + ' GB';
const diskUsed = Math.floor(Math.random() * 100 + 150);
const diskTotal = 512;
const diskPercent = Math.floor((diskUsed / diskTotal) * 100);
document.getElementById('diskPercent').textContent = diskPercent + '%';
document.getElementById('diskBar').style.width = diskPercent + '%';
document.getElementById('diskUsed').textContent = diskUsed + ' GB';
document.getElementById('diskAvailable').textContent = (diskTotal - diskUsed) + ' GB';
document.getElementById('diskSpeed').textContent = (Math.random() * 500 + 100).toFixed(0) + ' MB/s';
updateGPUInfo();
document.getElementById('downloadSpeed').textContent = (Math.random() * 100 + 10).toFixed(1) + ' MB/s';
document.getElementById('uploadSpeed').textContent = (Math.random() * 50 + 5).toFixed(1) + ' MB/s';
document.getElementById('totalDownload').textContent = (Math.random() * 500 + 100).toFixed(1) + ' GB';
document.getElementById('totalUpload').textContent = (Math.random() * 200 + 50).toFixed(1) + ' GB';
const days = Math.floor(Math.random() * 30);
const hours = Math.floor(Math.random() * 24);
const mins = Math.floor(Math.random() * 60);
document.getElementById('uptime').textContent = days + ' 天 ' + hours + ' 时 ' + mins + ' 分';
document.getElementById('processCount').textContent = Math.floor(Math.random() * 200 + 100);
document.getElementById('loadAvg').textContent = (Math.random() * 2).toFixed(2) + ', ' + (Math.random() * 1.5).toFixed(2) + ', ' + (Math.random() * 1).toFixed(2);
}
// GPU配置
const GPU_COUNT = 4;
const gpuConfigs = [
{ name: 'NVIDIA RTX 3090', memory: 24 },
{ name: 'NVIDIA RTX 4090', memory: 24 },
{ name: 'NVIDIA A100', memory: 80 },
{ name: 'NVIDIA V100', memory: 32 },
{ name: 'NVIDIA T4', memory: 16 },
{ name: 'NVIDIA L40S', memory: 48 },
{ name: 'NVIDIA H100', memory: 80 },
{ name: 'NVIDIA RTX 4080', memory: 16 }
];
// 初始化GPU列表
async function initGPUList() {
try {
const response = await fetch(`${window.API_BASE}/system-info`);
const result = await response.json();
const gpuData = (result.data && result.data.gpu) || [];
updateGPUInfo(gpuData);
} catch (error) {
console.error('初始化GPU列表失败:', error);
useMockGPUData();
}
}
// 更新GPU信息
function updateGPUInfo(gpuData) {
if (gpuData && gpuData.length > 0) {
const gpuCount = gpuData.length;
document.getElementById('gpuCount').textContent = `检测到 ${gpuCount} 块 GPU`;
let totalUsedMemory = 0;
let totalMemory = 0;
const gpuList = document.getElementById('gpuList');
if (gpuList) {
let gpuCardsHTML = '';
for (let i = 0; i < gpuCount; i++) {
const gpu = gpuData[i];
totalUsedMemory += gpu.memory_used_gb;
totalMemory += gpu.memory_total_gb;
gpuCardsHTML += `
<div class="border border-gray-200 rounded-lg p-2 bg-gradient-to-br from-gray-50 to-gray-100" id="gpuCard${i}">
<div class="flex items-center justify-between mb-1">
<div class="flex items-center min-w-0">
<div class="w-6 h-6 rounded bg-red-100 flex items-center justify-center mr-2 flex-shrink-0">
<i class="fa fa-microchip text-red-600 text-xs"></i>
</div>
<div class="min-w-0">
<div class="text-xs font-medium text-gray-800 truncate" id="gpuName${i}" title="${gpu.name}">${gpu.name}</div>
<div class="text-[10px] text-gray-400">PCIe</div>
</div>
</div>
<div class="text-right flex-shrink-0 ml-2">
<span class="text-sm font-bold text-gray-800" id="gpuPercent${i}">${gpu.gpu_percent}%</span>
</div>
</div>
<div class="relative h-1.5 bg-gray-200 rounded-full overflow-hidden mb-2">
<div id="gpuBar${i}" class="absolute left-0 top-0 h-full bg-gradient-to-r from-green-400 via-yellow-400 to-red-400 transition-all duration-500" style="width: ${gpu.gpu_percent}%"></div>
</div>
<div class="grid grid-cols-4 gap-1 text-center text-[10px]">
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">显存</div>
<div class="font-medium text-gray-700" id="gpuMem${i}">${gpu.memory_used_gb}/${gpu.memory_total_gb} GB</div>
</div>
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">温度</div>
<div class="font-medium ${gpu.temperature >= 80 ? 'text-red-600' : gpu.temperature >= 70 ? 'text-yellow-600' : 'text-gray-800'}" id="gpuTemp${i}">${gpu.temperature}°C</div>
</div>
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">功耗</div>
<div class="font-medium text-gray-700" id="gpuPower${i}">${gpu.power_w} W</div>
</div>
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">Fan</div>
<div class="font-medium text-gray-700" id="gpuFan${i}">${gpu.fan_speed || 0}%</div>
</div>
</div>
<div class="mt-1 grid grid-cols-2 gap-1 text-center text-[9px] text-gray-400">
<div>Clock: <span id="gpuClock${i}">${gpu.clock_mhz || 0} MHz</span></div>
<div>Driver: <span id="gpuDriver${i}">${gpu.driver_version || '-'}</span></div>
</div>
</div>
`;
}
// 如果GPU数量不足4个补充显示总显存
if (gpuCount < 4) {
gpuCardsHTML += `
<div class="border border-gray-200 rounded-lg p-2 bg-gradient-to-br from-gray-50 to-gray-100">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="w-6 h-6 rounded bg-gray-200 flex items-center justify-center mr-2">
<i class="fa fa-server text-gray-600 text-xs"></i>
</div>
<div class="text-xs font-medium text-gray-600">总显存使用</div>
</div>
<div class="text-right">
<span class="text-sm font-bold text-gray-800" id="gpuTotalMemory">${totalUsedMemory}/${totalMemory} GB</span>
</div>
</div>
</div>
`;
}
gpuList.innerHTML = gpuCardsHTML;
}
return;
}
useMockGPUData();
}
// 使用模拟GPU数据
function useMockGPUData() {
const gpuCount = Math.min(GPU_COUNT, 8);
let totalUsedMemory = 0;
let totalMemory = 0;
const gpuList = document.getElementById('gpuList');
if (gpuList) {
let gpuCardsHTML = '';
for (let i = 0; i < gpuCount; i++) {
const config = gpuConfigs[i % gpuConfigs.length];
const gpuUsage = Math.floor(Math.random() * 60 + 20);
const memUsed = (Math.random() * config.memory * 0.7 + config.memory * 0.1).toFixed(1);
const temp = Math.floor(Math.random() * 30 + 40);
const power = Math.floor(Math.random() * 150 + 100);
const fan = Math.floor(gpuUsage + Math.random() * 10);
totalUsedMemory += parseFloat(memUsed);
totalMemory += config.memory;
gpuCardsHTML += `
<div class="border border-gray-200 rounded-lg p-2 bg-gradient-to-br from-gray-50 to-gray-100" id="gpuCard${i}">
<div class="flex items-center justify-between mb-1">
<div class="flex items-center min-w-0">
<div class="w-6 h-6 rounded bg-red-100 flex items-center justify-center mr-2 flex-shrink-0">
<i class="fa fa-microchip text-red-600 text-xs"></i>
</div>
<div class="min-w-0">
<div class="text-xs font-medium text-gray-800 truncate" id="gpuName${i}" title="${config.name}">${config.name}</div>
<div class="text-[10px] text-gray-400">PCIe ${Math.floor(Math.random() * 4 + 1)}:00.0</div>
</div>
</div>
<div class="text-right flex-shrink-0 ml-2">
<span class="text-sm font-bold text-gray-800" id="gpuPercent${i}">${gpuUsage}%</span>
</div>
</div>
<div class="relative h-1.5 bg-gray-200 rounded-full overflow-hidden mb-2">
<div id="gpuBar${i}" class="absolute left-0 top-0 h-full bg-gradient-to-r from-green-400 via-yellow-400 to-red-400 transition-all duration-500" style="width: ${gpuUsage}%"></div>
</div>
<div class="grid grid-cols-4 gap-1 text-center text-[10px]">
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">显存</div>
<div class="font-medium text-gray-700" id="gpuMem${i}">${parseFloat(memUsed).toFixed(1)}/${config.memory} GB</div>
</div>
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">温度</div>
<div class="font-medium ${temp >= 80 ? 'text-red-600' : temp >= 70 ? 'text-yellow-600' : 'text-gray-800'}" id="gpuTemp${i}">${temp}°C</div>
</div>
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">功耗</div>
<div class="font-medium text-gray-700" id="gpuPower${i}">${power} W</div>
</div>
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">Fan</div>
<div class="font-medium text-gray-700" id="gpuFan${i}">${fan}%</div>
</div>
</div>
</div>
`;
}
gpuList.innerHTML = gpuCardsHTML;
document.getElementById('gpuCount').textContent = `检测到 ${gpuCount} 块 GPU`;
}
const gpuTotalMem = document.getElementById('gpuTotalMemory');
if (gpuTotalMem) {
gpuTotalMem.textContent = `${totalUsedMemory.toFixed(1)}/${totalMemory} GB`;
}
}
// 启动硬件监控自动刷新
function startRefreshTimer() {
if (refreshTimer) {
clearInterval(refreshTimer);
}
refreshTimer = setInterval(refreshHardwareInfo, currentRefreshInterval);
}
// 改变刷新频率
function changeRefreshRate() {
const select = document.getElementById('refreshInterval');
currentRefreshInterval = parseInt(select.value);
startRefreshTimer();
}
// 保存配置
function saveConfig() {
window.showMessage('提示', '配置保存功能开发中...', 'info');
}
// 导出页面渲染模块
window.PageRenderer = {
renderLogViewerPage,
renderToolsPage,
renderConfigPage,
refreshHardwareInfo,
useMockData,
initGPUList,
updateGPUInfo,
useMockGPUData,
startRefreshTimer,
changeRefreshRate,
saveConfig
};

392
web/js/services/system.js Normal file
View File

@@ -0,0 +1,392 @@
/**
* 系统监控服务
* 处理系统性能指标获取和展示
*/
// 日志自动刷新相关变量
let logRefreshTimer = null;
let logCountdownTimer = null;
let logCurrentInterval = 10;
let logFullContent = '';
// 设置自动刷新间隔
function setRefreshInterval() {
const select = document.getElementById('logRefreshInterval');
const countdownEl = document.getElementById('logRefreshCountdown');
const secondsEl = document.getElementById('countdownNumber');
if (!select) return;
logCurrentInterval = parseInt(select.value) || 10;
// 清除之前的定时器
if (logRefreshTimer) {
clearInterval(logRefreshTimer);
logRefreshTimer = null;
}
if (logCountdownTimer) {
clearInterval(logCountdownTimer);
logCountdownTimer = null;
}
// 如果选择关闭,不显示倒计时
if (select.value === '0') {
countdownEl.classList.add('hidden');
return;
}
// 显示倒计时
countdownEl.classList.remove('hidden');
secondsEl.textContent = logCurrentInterval;
// 启动倒计时
let countdown = logCurrentInterval;
logCountdownTimer = setInterval(() => {
countdown--;
if (countdown <= 0) {
countdown = logCurrentInterval;
}
secondsEl.textContent = countdown;
}, 1000);
// 启动自动刷新
logRefreshTimer = setInterval(() => {
if (typeof refreshLogs === 'function') {
refreshLogs();
}
}, logCurrentInterval * 1000);
}
// 获取系统性能监控数据
async function fetchSystemMetrics() {
try {
const response = await fetch(`${window.API_BASE}/health`);
const result = await response.json();
if (result.code === 0 && result.data) {
const data = result.data;
// 更新CPU使用率
const cpuEl = document.getElementById('cpuUsage');
if (cpuEl && data.cpu_percent !== undefined) {
cpuEl.textContent = data.cpu_percent;
cpuEl.className = data.cpu_percent > 80 ? 'text-red-500 font-medium' : '';
}
// 更新内存使用率
const memEl = document.getElementById('memUsage');
if (memEl && data.memory_percent !== undefined) {
memEl.textContent = data.memory_percent;
memEl.className = data.memory_percent > 80 ? 'text-red-500 font-medium' : '';
}
// 更新磁盘使用率
const diskEl = document.getElementById('diskUsage');
if (diskEl && data.disk_percent !== undefined) {
diskEl.textContent = data.disk_percent;
diskEl.className = data.disk_percent > 80 ? 'text-red-500 font-medium' : '';
}
}
} catch (error) {
console.error('获取系统监控数据失败:', error);
}
}
// 停止日志自动刷新(离开页面时调用)
function stopLogAutoRefresh() {
if (logRefreshTimer) {
clearInterval(logRefreshTimer);
logRefreshTimer = null;
}
if (logCountdownTimer) {
clearInterval(logCountdownTimer);
logCountdownTimer = null;
}
}
// 刷新日志
function refreshLogs() {
if (typeof switchLogTab === 'function') {
const currentLogTab = window.currentLogTab || 'system';
if (currentLogTab === 'system') {
if (typeof loadLogFiles === 'function' && document.getElementById('logTypeSelect')?.value) {
loadSelectedLog();
} else {
loadLogFiles();
}
} else {
loadTrainingLogFiles();
}
} else {
loadLogFiles();
}
// 重置倒计时
const select = document.getElementById('logRefreshInterval');
const secondsEl = document.getElementById('countdownNumber');
if (select && select.value !== '0' && secondsEl) {
secondsEl.textContent = logCurrentInterval;
}
}
// 滚动到日志底部
function scrollToLogBottom() {
const logContent = document.getElementById('logContent');
if (logContent) {
logContent.scrollTop = logContent.scrollHeight;
}
}
// 当前日志类型system 或 training
window.currentLogTab = 'system';
// 切换日志类型标签
function switchLogTab(tab) {
window.currentLogTab = tab;
const systemTab = document.getElementById('logTabSystem');
const trainingTab = document.getElementById('logTabTraining');
const systemOptions = document.getElementById('systemLogOptions');
const trainingOptions = document.getElementById('trainingLogOptions');
if (tab === 'system') {
systemTab.className = 'px-4 py-1.5 text-sm rounded-md transition-colors bg-white shadow-sm text-primary';
trainingTab.className = 'px-4 py-1.5 text-sm rounded-md transition-colors text-gray-600 hover:text-gray-800';
systemOptions.classList.remove('hidden');
trainingOptions.classList.add('hidden');
loadLogFiles();
} else {
trainingTab.className = 'px-4 py-1.5 text-sm rounded-md transition-colors bg-white shadow-sm text-primary';
systemTab.className = 'px-4 py-1.5 text-sm rounded-md transition-colors text-gray-600 hover:text-gray-800';
trainingOptions.classList.remove('hidden');
systemOptions.classList.add('hidden');
loadTrainingLogFiles();
}
}
// 初始化日志查看器
function initLogViewer() {
const datePicker = document.getElementById('logDatePicker');
if (datePicker) {
const today = new Date().toISOString().split('T')[0];
datePicker.value = today;
}
// 加载默认日志类型
loadLogFiles();
// 启动自动刷新
setRefreshInterval();
}
// 加载训练日志文件列表
async function loadTrainingLogFiles() {
const logSelect = document.getElementById('trainingLogSelect');
if (!logSelect) return;
logSelect.innerHTML = '<option value="">加载中...</option>';
try {
const response = await fetch(`${window.API_BASE}/training-log-files`);
const result = await response.json();
if (result.code === 0 && result.data) {
logSelect.innerHTML = '<option value="">请选择训练日志</option>';
result.data.forEach(log => {
const option = document.createElement('option');
option.value = log.file;
option.textContent = `${log.name} (PID: ${log.pid}, ${log.date}, ${log.size})`;
logSelect.appendChild(option);
});
// 如果有日志文件,自动加载第一个
if (result.data.length > 0) {
logSelect.value = result.data[0].file;
loadSelectedTrainingLog();
} else {
document.getElementById('logContent').textContent = '暂无训练日志';
document.getElementById('logFileInfo').textContent = '无训练日志';
}
} else {
logSelect.innerHTML = '<option value="">暂无训练日志</option>';
document.getElementById('logContent').textContent = '暂无训练日志';
document.getElementById('logFileInfo').textContent = '无训练日志';
}
} catch (error) {
console.error('加载训练日志列表失败:', error);
logSelect.innerHTML = '<option value="">加载失败</option>';
document.getElementById('logContent').textContent = '加载训练日志列表失败: ' + error.message;
}
}
// 加载选中的训练日志
async function loadSelectedTrainingLog() {
const logSelect = document.getElementById('trainingLogSelect');
const logFile = logSelect.value;
const logContent = document.getElementById('logContent');
const logFileInfo = document.getElementById('logFileInfo');
if (!logFile) {
logContent.textContent = '请选择训练日志';
logFileInfo.textContent = '无训练日志';
return;
}
logContent.textContent = '加载中...';
logFileInfo.textContent = '加载中...';
try {
const response = await fetch(`${window.API_BASE}/training-log-content?file=${encodeURIComponent(logFile)}`);
const result = await response.json();
if (result.code === 0 && result.data) {
logFullContent = result.data.content || '';
logContent.textContent = logFullContent || '(空日志)';
logFileInfo.textContent = result.data.file + ' (' + result.data.size + ')';
// 清空搜索
document.getElementById('logSearchInput').value = '';
document.getElementById('logMatchCount').textContent = '';
// 滚动到底部
scrollToLogBottom();
} else {
logContent.textContent = '加载失败: ' + (result.message || '未知错误');
logFileInfo.textContent = '加载失败';
}
} catch (error) {
console.error('加载训练日志内容失败:', error);
logContent.textContent = '加载失败: ' + error.message;
logFileInfo.textContent = '加载失败';
}
}
// 加载日志文件列表
async function loadLogFiles() {
const datePicker = document.getElementById('logDatePicker');
const logTypeSelect = document.getElementById('logTypeSelect');
const selectedDate = datePicker ? datePicker.value : new Date().toISOString().split('T')[0];
if (!logTypeSelect) return;
logTypeSelect.innerHTML = '<option value="">加载中...</option>';
try {
const response = await fetch(`${window.API_BASE}/log-files?date=${selectedDate}`);
const result = await response.json();
if (result.code === 0 && result.data) {
logTypeSelect.innerHTML = '<option value="">请选择日志文件</option>';
result.data.forEach(log => {
const option = document.createElement('option');
option.value = log.file;
option.textContent = log.name + ' (' + log.size + ')';
logTypeSelect.appendChild(option);
});
// 如果有日志文件,自动加载第一个
if (result.data.length > 0) {
logTypeSelect.value = result.data[0].file;
loadSelectedLog();
} else {
logTypeSelect.innerHTML = '<option value="">暂无日志文件</option>';
document.getElementById('logContent').textContent = '该日期暂无日志文件';
document.getElementById('logFileInfo').textContent = '无日志文件';
}
} else {
logTypeSelect.innerHTML = '<option value="">暂无日志文件</option>';
document.getElementById('logContent').textContent = '该日期暂无日志文件';
document.getElementById('logFileInfo').textContent = '无日志文件';
}
} catch (error) {
console.error('加载日志文件列表失败:', error);
logTypeSelect.innerHTML = '<option value="">加载失败</option>';
document.getElementById('logContent').textContent = '加载日志文件列表失败: ' + error.message;
}
}
// 加载选中的日志
async function loadSelectedLog() {
const logTypeSelect = document.getElementById('logTypeSelect');
const logFile = logTypeSelect.value;
const logContent = document.getElementById('logContent');
const logFileInfo = document.getElementById('logFileInfo');
if (!logFile) {
logContent.textContent = '请选择日志文件';
logFileInfo.textContent = '无日志文件';
return;
}
logContent.textContent = '加载中...';
logFileInfo.textContent = '加载中...';
try {
const response = await fetch(`${window.API_BASE}/log-content?file=${encodeURIComponent(logFile)}`);
const result = await response.json();
if (result.code === 0) {
logFullContent = result.data.content || '';
logContent.textContent = logFullContent || '(空日志)';
logFileInfo.textContent = result.data.file + ' (' + result.data.size + ')';
// 清空搜索框和匹配计数
document.getElementById('logSearchInput').value = '';
document.getElementById('logMatchCount').textContent = '';
// 滚动到最底部
scrollToLogBottom();
} else {
logContent.textContent = '加载失败: ' + (result.message || '未知错误');
logFileInfo.textContent = '加载失败';
}
} catch (error) {
console.error('加载日志内容失败:', error);
logContent.textContent = '加载日志内容失败: ' + error.message;
logFileInfo.textContent = '加载失败';
}
}
// 过滤日志内容
function filterLogContent() {
const searchInput = document.getElementById('logSearchInput');
const matchCount = document.getElementById('logMatchCount');
const logContent = document.getElementById('logContent');
if (!searchInput || !matchCount || !logContent) return;
const keyword = searchInput.value.trim();
if (!keyword) {
logContent.textContent = logFullContent || '(空日志)';
matchCount.textContent = '';
scrollToLogBottom();
return;
}
const lines = logFullContent.split('\n');
const matchingLines = lines.filter(line => line.toLowerCase().includes(keyword.toLowerCase()));
if (matchingLines.length > 0) {
logContent.textContent = matchingLines.join('\n');
matchCount.textContent = `(${matchingLines.length}条匹配)`;
// 滚动到最底部查看最新匹配
scrollToLogBottom();
} else {
logContent.textContent = '未找到匹配的日志';
matchCount.textContent = '(0条匹配)';
}
}
// 清空日志内容显示
function clearLogContent() {
document.getElementById('logContent').textContent = '日志内容将在这里显示...';
document.getElementById('logFileInfo').textContent = '请选择日志文件';
const logTypeSelect = document.getElementById('logTypeSelect');
if (logTypeSelect) logTypeSelect.value = '';
document.getElementById('logSearchInput').value = '';
document.getElementById('logMatchCount').textContent = '';
logFullContent = '';
}
// 导出服务函数
window.SystemService = {
fetchSystemMetrics,
setRefreshInterval,
stopLogAutoRefresh,
refreshLogs,
scrollToLogBottom,
switchLogTab,
initLogViewer,
loadTrainingLogFiles,
loadSelectedTrainingLog,
loadLogFiles,
loadSelectedLog,
filterLogContent,
clearLogContent
};

201
web/js/services/training.js Normal file
View File

@@ -0,0 +1,201 @@
/**
* 训练服务模块
* 处理训练任务相关的操作和进度跟踪
*/
// 训练进度缓存
window.trainingProgressCache = window.trainingProgressCache || {};
// 使用 window 避免重复声明
if (typeof window._progressRefreshTimer === 'undefined') {
window._progressRefreshTimer = null;
}
// 渲染训练进度
function renderTrainingProgress(val, row) {
const progressData = window.trainingProgressCache[row.id];
if (progressData && progressData.status === 'running') {
if (progressData.progress > 0) {
return `
<div class="flex flex-col">
<span class="text-sm font-medium text-primary">${progressData.progress}%</span>
<span class="text-xs text-gray-500">${progressData.step || ''} ${progressData.speed || ''}</span>
<span class="text-xs text-gray-400">ETA: ${progressData.eta || '--:--'}</span>
</div>
`;
}
}
return `${val || 0}%`;
}
// 刷新训练进度
async function refreshTrainingProgress() {
if (typeof window.currentPage !== 'string' || window.currentPage !== 'fine-tune') return;
try {
const response = await fetch(`${window.API_BASE}/fine-tune`);
const result = await response.json();
if (result.code === 0 && result.data) {
// 刷新运行中或已完成的任务(有进度信息)
const activeTasks = result.data.filter(task =>
task.status === 'running' || task.status === 'pending'
);
for (const task of activeTasks) {
try {
// 并行获取进度和PID状态
const [progressResponse, statusResponse] = await Promise.all([
fetch(`${window.API_BASE}/fine-tune/progress/${task.id}`),
fetch(`${window.API_BASE}/fine-tune/${task.id}`)
]);
const progressResult = await progressResponse.json();
const statusResult = await statusResponse.json();
if (progressResult.code === 0 && progressResult.data) {
window.trainingProgressCache[task.id] = progressResult.data;
}
// 如果状态已改变PID已结束更新表格中的状态显示
if (statusResult.code === 0 && statusResult.data) {
const actualStatus = statusResult.data.status;
if (task.status !== actualStatus) {
// 找到对应的行并更新状态
const row = document.querySelector(`tr[data-id="${task.id}"]`);
if (row) {
const statusCell = row.querySelector('td:nth-child(3)');
if (statusCell) {
statusCell.innerHTML = `<span class="px-2 py-1 rounded text-xs ${actualStatus === 'running' ? 'bg-green-100 text-green-700' : actualStatus === 'failed' ? 'bg-red-100 text-red-700' : 'bg-blue-100 text-blue-700'}">${actualStatus}</span>`;
}
}
}
}
} catch (e) {
console.warn(`获取任务 ${task.id} 信息失败:`, e);
}
}
}
} catch (error) {
console.warn('刷新训练进度失败:', error);
}
}
// 检查并更新任务状态(用于 fine-tune 页面)
async function checkAndUpdateTaskStatus() {
if (typeof window.currentPage !== 'string' || window.currentPage !== 'fine-tune') return;
try {
const response = await fetch(`${window.API_BASE}/fine-tune`);
const result = await response.json();
if (result.code === 0 && result.data) {
// 获取所有 running 状态的任务
const runningTasks = result.data.filter(task => task.status === 'running');
for (const task of runningTasks) {
try {
// 调用 status API 获取实际状态(会检查 PID
const statusResponse = await fetch(`${window.API_BASE}/fine-tune/${task.id}`);
const statusResult = await statusResponse.json();
if (statusResult.code === 0 && statusResult.data) {
const actualStatus = statusResult.data.status;
// 如果实际状态不是 running更新表格显示
if (actualStatus !== 'running') {
const row = document.querySelector(`tr[data-id="${task.id}"]`);
if (row) {
const statusCell = row.querySelector('td:nth-child(3)');
if (statusCell) {
const statusClass = actualStatus === 'failed'
? 'bg-red-100 text-red-700'
: 'bg-blue-100 text-blue-700';
statusCell.innerHTML = `<span class="px-2 py-1 rounded text-xs ${statusClass}">${actualStatus}</span>`;
console.log(`[Status] 任务 ${task.id} 状态已更新: running -> ${actualStatus}`);
}
}
}
}
} catch (e) {
console.warn(`检查任务 ${task.id} 状态失败:`, e);
}
}
}
} catch (error) {
console.warn('检查任务状态失败:', error);
}
}
// 启动训练进度自动刷新
function startProgressRefresh() {
stopProgressRefresh();
window._progressRefreshTimer = setInterval(() => {
refreshTrainingProgress();
checkAndUpdateTaskStatus();
}, 5000);
}
// 停止训练进度刷新
function stopProgressRefresh() {
if (window._progressRefreshTimer) {
clearInterval(window._progressRefreshTimer);
window._progressRefreshTimer = null;
}
}
// 停止训练任务
async function stopItem(taskId) {
window.showConfirm('确认停止', '确定要停止这个训练任务吗?进程将被终止。', async () => {
try {
const response = await fetch(`${window.API_BASE}/fine-tune/stop/${taskId}`, {
method: 'POST'
});
const result = await response.json();
if (result.code === 0) {
window.showMessage('成功', '训练任务已停止', 'success');
// 刷新当前页面
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
if (activeLink && typeof window.loadPage === 'function') {
window.loadPage(activeLink.dataset.page);
}
} else {
window.showMessage('错误', result.message || '停止失败', 'error');
}
} catch (error) {
window.showMessage('错误', '停止失败: ' + error.message, 'error');
}
});
}
// 查看训练日志 - 跳转到日志页面
async function viewTrainingLog(taskId, taskName) {
window.loadPage('logs');
}
// 查看调优任务日志 - 跳转到training-log.html页面
function viewFineTuneLogs(taskId, taskName) {
// 保存 taskId 到 sessionStorage
sessionStorage.setItem('trainingLogTaskId', taskId.toString());
sessionStorage.setItem('trainingLogTaskName', taskName);
// 跳转到日志页面
window.navigateToPage('training-log');
}
// 跳转到训练日志二级页面
function navigateToTrainingLog(taskId) {
// 传递 taskId 到 sessionStorage
sessionStorage.setItem('trainingLogTaskId', taskId.toString());
// 跳转到日志页面
window.navigateToPage('training-log');
}
// 导出训练服务
window.TrainingService = {
renderTrainingProgress,
refreshTrainingProgress,
checkAndUpdateTaskStatus,
startProgressRefresh,
stopProgressRefresh,
stopItem,
viewTrainingLog,
viewFineTuneLogs,
navigateToTrainingLog
};

181
web/js/utils.js Normal file
View File

@@ -0,0 +1,181 @@
/**
* 工具函数模块
* 包含弹窗、消息提示等通用功能
*/
// ============ 自定义消息弹窗 ============
window.showMessage = function(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';
// 设置消息
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>';
}
// 单按钮模式
modalBtnGroup.classList.add('hidden');
modalSingleBtnGroup.classList.remove('hidden');
const confirmBtn = modalConfirmBtn2;
if (type === 'error') {
confirmBtn.className = 'px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors';
} else {
confirmBtn.className = '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';
// 保存回调到按钮属性
confirmBtn._onConfirm = onConfirm;
// 使用 function 而不是箭头函数
confirmBtn.onclick = function() {
closeModal();
const callback = this._onConfirm;
if (typeof callback === 'function') {
callback();
}
this._onConfirm = null;
};
};
// 关闭消息弹窗
function closeModal() {
const modal = document.getElementById('customModal');
modal.classList.add('hidden');
document.body.style.overflow = '';
}
// 确认弹窗(两个按钮)
window.showConfirm = function(title, message, onConfirm, onCancel, type = 'info') {
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 modalCancelBtn = document.getElementById('modalCancelBtn');
const modalBtnGroup = document.getElementById('modalBtnGroup');
const modalSingleBtnGroup = document.getElementById('modalSingleBtnGroup');
if (!modalConfirmBtn) {
console.error('modalConfirmBtn not found');
return;
}
modalTitle.textContent = title;
modalMessage.innerHTML = message;
// 根据类型设置图标
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');
modalSingleBtnGroup.classList.add('hidden');
modalConfirmBtn.textContent = '确定';
modalConfirmBtn.className = '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;
modalConfirmBtn._onCancel = onCancel;
// 使用 function 而不是箭头函数,确保 this 指向正确
modalConfirmBtn.onclick = function() {
closeModal();
const callback = this._onConfirm;
if (typeof callback === 'function') {
callback();
}
this._onConfirm = null;
this._onCancel = null;
};
modalCancelBtn.onclick = function() {
closeModal();
const callback = this._onCancel || modalConfirmBtn._onCancel;
if (typeof callback === 'function') {
callback();
}
modalConfirmBtn._onConfirm = null;
modalConfirmBtn._onCancel = null;
};
};
// ============ Web日志系统 ============
const webLogger = {
_currentPage: 'main',
// 初始化当前页面名称
init: function(pageName) {
this._currentPage = pageName || 'unknown';
},
// 发送日志到服务器
_sendLog: async function(level, message) {
try {
await fetch(`${window.API_BASE}/web-log`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
level: level,
message: message,
page: this._currentPage,
timestamp: new Date().toISOString()
})
});
} catch (e) {
// 发送失败时只记录到控制台
console.warn('日志发送失败:', e);
}
},
info: function(message) {
console.log(`[INFO] ${message}`);
this._sendLog('info', message);
},
error: function(message) {
console.error(`[ERROR] ${message}`);
this._sendLog('error', message);
},
warning: function(message) {
console.warn(`[WARNING] ${message}`);
this._sendLog('warning', message);
},
debug: function(message) {
console.debug(`[DEBUG] ${message}`);
this._sendLog('debug', message);
}
};
// 导出 webLogger 到全局
window.webLogger = webLogger;

View File

@@ -1,11 +1,11 @@
<!-- 共享侧边栏组件 -->
<!-- 使用方法:在页面中通过 JavaScript 动态加载此组件 -->
<!-- 使用方法:在页面中添加 <div id="sidebar-container"></div> 并引入 sidebar-loader.js -->
<!-- 侧边导航 -->
<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">
<img id="sidebar-logo" 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>
@@ -17,17 +17,11 @@
<!-- 第一分区:模型服务 -->
<div class="sidebar-section-title">模型服务</div>
<div class="nav-item-wrapper">
<a href="main.html" data-page="fine-tune" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<a href="main.html?page=fine-tune" 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="main.html?page=model-eval" 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>
@@ -56,8 +50,8 @@
</a>
</div>
<div class="nav-item-wrapper">
<a href="main.html?page=data-generate" 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>
<a href="tools.html" data-page="tools" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-wrench w-5 text-center"></i>
<span class="ml-2">其他工具</span>
</a>
</div>
@@ -65,13 +59,13 @@
<!-- 第三分区:系统设置 -->
<div class="sidebar-section-title mt-6">系统设置</div>
<div class="nav-item-wrapper">
<a href="main.html?page=config" data-page="config" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<a href="hardware.html" data-page="hardware" 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="main.html?page=logs" data-page="logs" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<a href="logs.html" 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>
@@ -87,84 +81,3 @@
</div>
</div>
</aside>
<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-slider {
position: absolute;
width: 4px;
height: 0;
background-color: #1890ff;
border-radius: 0 2px 2px 0;
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
z-index: 10;
}
.nav-item-wrapper {
position: relative;
}
.nav-link {
position: relative;
z-index: 1;
}
</style>
<script>
// 更新侧边栏滑块位置和高亮当前页
function initSidebar(currentPage) {
// 设置当前页高亮
if (currentPage) {
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');
}
});
}
// 更新滑块位置
const slider = document.getElementById('sidebar-slider');
if (slider) {
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';
}
}
}
// 绑定导航点击事件
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', function(e) {
// 如果是当前页的链接,不阻止
if (this.dataset.page === currentPage) return;
// 外部链接不阻止
if (!this.href.includes('main.html')) return;
e.preventDefault();
window.location.href = this.href;
});
});
}
// 页面加载后初始化
document.addEventListener('DOMContentLoaded', function() {
// 延迟执行以确保DOM完全就绪
setTimeout(() => {
const currentPage = window.currentPageName || 'fine-tune';
initSidebar(currentPage);
}, 100);
});
</script>

View File

@@ -15,21 +15,15 @@
originalWarn.apply(console, args);
};
}
})();
</script>
<script>
// 设置当前页面,供侧边栏高亮使用
window.sidebarCurrentPage = 'tools';
</script>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<!-- 侧边栏加载器 -->
<script src="../js/components/sidebar-loader.js"></script>
<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);
}
.form-input {
width: 100%;
padding: 0.5rem 0.75rem;
@@ -72,117 +66,11 @@
.text-primary { color: #1890ff; }
.border-primary { border-color: #1890ff; }
:root { --primary: #1890ff; --danger: #f5222d; --success: #52c41a; }
/* 侧边栏滑块动画 */
.sidebar-slider {
position: absolute;
width: 4px;
height: 0;
background-color: #1890ff;
border-radius: 0 2px 2px 0;
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
z-index: 10;
}
.nav-item-wrapper {
position: relative;
}
.nav-link {
position: relative;
z-index: 1;
}
</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-slider" id="sidebar-slider"></div>
<!-- 第一分区:模型服务 -->
<div class="sidebar-section-title">模型服务</div>
<div class="nav-item-wrapper">
<a href="main.html" 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="main.html?page=model-eval" 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="main.html?page=model-compare" 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="main.html?page=model-manage" data-page="model-manage" class="nav-link 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="main.html?page=dataset-manage" 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="main.html?page=data-generate" 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="main.html?page=config" 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="main.html?page=logs" 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 id="sidebar-container"></div>
<div class="flex-1 flex flex-col overflow-hidden">
<!-- 顶部导航 -->
<header class="bg-white border-b border-gray-200 shadow-sm">

View File

@@ -16,38 +16,14 @@
};
}
</script>
<script>
// 设置当前页面,供侧边栏高亮使用
window.sidebarCurrentPage = 'dataset-manage';
</script>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<!-- 侧边栏加载器 -->
<script src="../js/components/sidebar-loader.js"></script>
<style>
/* 侧边栏滑块动画 */
.sidebar-slider {
position: absolute;
width: 4px;
height: 0;
background-color: #1890ff;
border-radius: 0 2px 2px 0;
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
z-index: 10;
}
.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);
}
.nav-item-wrapper {
position: relative;
}
.nav-link {
position: relative;
z-index: 1;
}
.form-input {
width: 100%;
padding: 0.5rem 0.75rem;
@@ -98,94 +74,8 @@
</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-slider" id="sidebar-slider"></div>
<!-- 第一分区:模型服务 -->
<div class="sidebar-section-title">模型服务</div>
<div class="nav-item-wrapper">
<a href="main.html" 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="main.html?page=model-eval" 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="main.html?page=model-compare" 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="main.html?page=model-manage" data-page="model-manage" class="nav-link 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="main.html?page=dataset-manage" 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="main.html?page=data-generate" 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="main.html?page=config" 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="main.html?page=logs" 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 id="sidebar-container"></div>
<div class="flex-1 flex flex-col overflow-hidden">
<!-- 顶部导航 -->
<header class="bg-white border-b border-gray-200 shadow-sm">
@@ -197,6 +87,18 @@
</a>
</div>
<div class="flex items-center space-x-4">
<!-- 简化的性能监控 -->
<a href="?page=config" class="flex items-center space-x-3 text-xs text-gray-500 hover:text-primary transition-colors mr-4">
<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>
</a>
<div class="h-4 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 mt-2 bg-white rounded shadow-lg py-1 hidden group-hover:block border border-gray-100 min-w-[140px]">

View File

@@ -16,138 +16,22 @@
};
}
</script>
<script>
// 设置当前页面,供侧边栏高亮使用
window.sidebarCurrentPage = 'dataset-manage';
</script>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<!-- 侧边栏加载器 -->
<script src="../js/components/sidebar-loader.js"></script>
<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;
}
.text-primary { color: #1890ff; }
.bg-primary { background-color: #1890ff; }
.text-danger { color: #f5222d; }
/* 侧边栏滑块动画 */
.sidebar-slider {
position: absolute;
width: 4px;
height: 0;
background-color: #1890ff;
border-radius: 0 2px 2px 0;
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
z-index: 10;
}
.nav-item-wrapper {
position: relative;
}
.nav-link {
position: relative;
z-index: 1;
}
</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-slider" id="sidebar-slider"></div>
<!-- 第一分区:模型服务 -->
<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="#" 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 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="main.html?page=logs" 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 id="sidebar-container"></div>
<div class="flex-1 flex flex-col overflow-hidden">
<!-- 顶部导航 -->
<header class="bg-white border-b border-gray-200 shadow-sm">

View File

@@ -16,38 +16,12 @@
};
}
</script>
<script>
// 设置当前页面,供侧边栏高亮使用
window.sidebarCurrentPage = 'fine-tune';
</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-slider {
position: absolute;
width: 4px;
height: 0;
background-color: #1890ff;
border-radius: 0 2px 2px 0;
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
z-index: 10;
}
.nav-item-wrapper {
position: relative;
}
.nav-link {
position: relative;
z-index: 1;
}
.form-input {
width: 100%;
padding: 0.5rem 0.75rem;
@@ -98,94 +72,12 @@
.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">
<!-- 侧边导航 -->
<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-slider" id="sidebar-slider"></div>
<!-- 第一分区:模型服务 -->
<div class="sidebar-section-title">模型服务</div>
<div class="nav-item-wrapper">
<a href="main.html" 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="main.html?page=model-eval" 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="main.html?page=model-compare" 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="main.html?page=model-manage" data-page="model-manage" class="nav-link 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="main.html?page=dataset-manage" 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="main.html?page=data-generate" 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="main.html?page=config" 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="main.html?page=logs" 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 id="sidebar-container"></div>
<!-- 主内容区 -->
<div class="flex-1 flex flex-col overflow-hidden">

View File

@@ -5,120 +5,40 @@
<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>
<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;
}
:root { --primary: #1890ff; --danger: #f5222d; --success: #52c41a; }
.bg-primary { background-color: var(--primary); }
.text-primary { color: var(--primary); }
.border-primary { border-color: var(--primary); }
.hover\:bg-primary\/90:hover { background-color: rgba(24, 144, 255, 0.9); }
.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; }
.sidebar-item-active { background-color: rgba(24, 144, 255, 0.1); color: #1890ff; border-left: 4px solid #1890ff; }
</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="main.html?page=fine-tune" 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 hidden">
<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="main.html?page=model-eval" 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="main.html?page=model-compare" 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><a href="main.html?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><a href="main.html?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><a href="main.html?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="main.html?page=model-manage" data-page="model-manage" class="nav-link 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="main.html?page=dataset-manage" 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="tools.html" 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><a href="main.html?page=model-manage" class="nav-link 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><a href="main.html?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><a href="tools.html" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors"><i class="fa fa-wrench 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="config.html" class="nav-link sidebar-item-active 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="logs.html" 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><a href="hardware.html" class="nav-link sidebar-item-active flex items-center px-4 py-2.5"><i class="fa fa-bar-chart w-5 text-center"></i><span class="ml-2">平台性能</span></a></div>
<div><a href="logs.html" 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 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>

View File

@@ -5,120 +5,40 @@
<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>
<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;
}
:root { --primary: #1890ff; --danger: #f5222d; --success: #52c41a; }
.bg-primary { background-color: var(--primary); }
.text-primary { color: var(--primary); }
.border-primary { border-color: var(--primary); }
.hover\:bg-primary\/90:hover { background-color: rgba(24, 144, 255, 0.9); }
.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; }
.sidebar-item-active { background-color: rgba(24, 144, 255, 0.1); color: #1890ff; border-left: 4px solid #1890ff; }
</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="main.html?page=fine-tune" 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 hidden">
<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="main.html?page=model-eval" 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="main.html?page=model-compare" 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><a href="main.html?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><a href="main.html?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><a href="main.html?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="main.html?page=model-manage" data-page="model-manage" class="nav-link 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="main.html?page=dataset-manage" 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="tools.html" 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><a href="main.html?page=model-manage" class="nav-link 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><a href="main.html?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><a href="tools.html" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors"><i class="fa fa-wrench 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="hardware.html" 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="logs.html" class="nav-link sidebar-item-active 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><a href="hardware.html" 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><a href="logs.html" class="nav-link sidebar-item-active flex items-center px-4 py-2.5"><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 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>

View File

@@ -25,6 +25,8 @@
<script src="../js/components/table.js"></script>
<script src="../js/pages/render.js"></script>
<script src="../js/main.js"></script>
<!-- 常量配置 -->
<script src="../js/config/constants.js"></script>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<link href="../css/main.css" rel="stylesheet">
</head>
@@ -50,12 +52,6 @@
<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>
@@ -85,7 +81,7 @@
</div>
<div class="nav-item-wrapper">
<a href="tools.html" 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>
<i class="fa fa-wrench w-5 text-center"></i>
<span class="ml-2">其他工具</span>
</a>
</div>
@@ -171,12 +167,19 @@
</div>
<script>
// API 基础地址 - 使用 config.yaml 中的 app.port (7861)
// 加载常量配置
if (typeof window.CONSTANTS === 'undefined') {
// 如果constants.js还未加载使用默认值
window.CONSTANTS = { API_CONFIG: { PORT: 7861, METRICS_INTERVAL: 30000 } };
}
const CONFIG = window.CONSTANTS.API_CONFIG;
// API 基础地址
if (typeof window.getApiBase !== 'function') {
window.getApiBase = () => {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
return `${protocol}//${hostname}:7861/api`;
return `${protocol}//${hostname}:${CONFIG.PORT}/api`;
};
}
if (typeof window.API_BASE === 'undefined') {
@@ -215,9 +218,9 @@
}
}
// 页面加载时获取监控数据,并每30秒刷新
// 页面加载时获取监控数据,并定期刷新
fetchSystemMetrics();
setInterval(fetchSystemMetrics, 30000);
setInterval(fetchSystemMetrics, CONFIG.METRICS_INTERVAL);
// 各功能模块的表格配置
const tableConfigs = {
@@ -274,7 +277,7 @@
}},
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
],
actions: ['compare', 'delete']
actions: ['startCompare', 'delete']
},
'dataset-manage': {
title: '数据集管理',
@@ -356,7 +359,15 @@
{ title: '描述', key: 'description', render: (val) => val || '-' },
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
],
actions: ['edit', 'delete']
actions: ['edit', 'delete'],
// 训练模型 tab 的列配置
trainedColumns: [
{ title: '模型名称', key: 'name' },
{ title: '训练方法', key: 'train_methods', render: (val) => val && val[0] ? val[0].name : '-' },
{ title: '基座模型', key: 'base_model_path', render: (val) => `<span class="text-xs text-gray-500 truncate block" title="${val}">${val || '-'}</span>` },
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
],
trainedActions: ['view', 'delete']
},
'logs': {
title: '查看日志',
@@ -563,7 +574,7 @@
progressRefreshTimer = setInterval(() => {
refreshTrainingProgress();
checkAndUpdateTaskStatus();
}, 5000);
}, CONFIG.TRAINING_REFRESH_INTERVAL);
// 更新侧边栏高亮状态
document.querySelectorAll('.nav-link').forEach(link => {
@@ -761,7 +772,10 @@
}
let data = await fetchData(apiUrl);
// 如果配置了 dataPath从返回数据中提取指定字段
if (config.dataPath && typeof data === 'object' && data !== null) {
// 训练模型 tab 需要从 {models: [...]} 中提取数据
if (config.hasModelTabs && currentModelTab === 'trained' && typeof data === 'object' && data !== null) {
data = data.models || [];
} else if (config.dataPath && typeof data === 'object' && data !== null) {
data = data[config.dataPath] || [];
}
currentPageData = data; // 保存当前页面数据
@@ -1075,15 +1089,13 @@
// 下载数据集(打包下载)
function downloadDataset(datasetId) {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
window.open(`${protocol}//${hostname}:7861/api/dataset-manage/download/${datasetId}`, '_blank');
window.open(`${API_BASE}/dataset-manage/download/${datasetId}`, '_blank');
}
// 开始模型对比
async function startCompare(id) {
// 跳转到模型对比聊天页面(通过主框架加载
window.location.href = `main.html?page=model-compare-chat&id=${id}`;
// 跳转到模型对比聊天页面(独立页面
window.location.href = `model-compare-chat.html?id=${id}`;
}
// 筛选表格
@@ -1108,16 +1120,23 @@
// 渲染表格页面
function renderTablePage(config, data) {
// 使用配置中的列定义,如果没有则使用默认列
const columns = config.columns || [
{ title: '模型名称', key: 'name' },
{ title: '训练方法', key: 'train_methods', render: (val) => val && val[0] ? val[0].name : '-' },
{ title: '基座模型', key: 'base_model_path', render: (val) => `<span class="text-xs text-gray-500 truncate block" title="${val}">${val || '-'}</span>` },
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
];
// 根据 tab 选择列配置和当前 API
let columns;
let currentApi = config.api;
if (config.hasModelTabs && currentModelTab === 'trained') {
columns = config.trainedColumns || config.columns;
currentApi = 'model-manage/trained-models';
} else {
columns = config.columns || [
{ title: '模型名称', key: 'name' },
{ title: '训练方法', key: 'train_methods', render: (val) => val && val[0] ? val[0].name : '-' },
{ title: '基座模型', key: 'base_model_path', render: (val) => `<span class="text-xs text-gray-500 truncate block" title="${val}">${val || '-'}</span>` },
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
];
}
// 搜索框
const searchBox = (config.api === 'model-manage' || config.api === 'model-manage/trained-models' || config.api === 'dataset-manage' || config.api === 'fine-tune') ? `
const searchBox = (currentApi === 'model-manage' || currentApi === 'model-manage/trained-models' || config.api === 'dataset-manage' || config.api === 'fine-tune') ? `
<div class="relative">
<input type="text" id="tableSearchInput" placeholder="搜索${config.title}..."
class="w-72 pl-9 pr-3 py-1.5 rounded border border-gray-300 text-sm focus:outline-none focus:border-primary focus:ring-1 focus:border-primary"
@@ -1126,27 +1145,50 @@
</div>
` : '';
// 是否支持多选(模型管理和数据集管理)
const supportsMultiSelect = config.api === 'model-manage' || config.api === 'model-manage/trained-models' || config.api === 'dataset-manage' || config.api === 'fine-tune';
// 是否支持多选
const supportsMultiSelect = currentApi === 'model-manage' || currentApi === 'model-manage/trained-models' || config.api === 'dataset-manage' || config.api === 'fine-tune';
// 创建按钮(根据API类型决定是否显示)
const createButton = config.api === 'model-manage' ? `
// 创建按钮(根据 tab 类型决定是否显示)
let createButton = '';
if (config.hasModelTabs && currentModelTab === 'trained') {
// 训练模型 tab 不显示创建按钮
createButton = '';
} else if (config.hasModelTabs && currentModelTab === 'config') {
// 配置模型 tab 显示创建按钮
createButton = `
<button onclick="showCreateModal('${config.api}')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
<i class="fa fa-plus mr-1"></i>新建模型
</button>
` : (config.api === 'dataset-manage' ? `
`;
} else if (config.api === 'model-manage') {
createButton = `
<button onclick="showCreateModal('${config.api}')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
<i class="fa fa-plus mr-1"></i>新建模型
</button>
`;
} else if (config.api === 'dataset-manage') {
createButton = `
<button onclick="showCreateModal('${config.api}')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
<i class="fa fa-plus mr-1"></i>创建数据集
</button>
` : (config.api === 'fine-tune' ? `
`;
} else if (config.api === 'fine-tune') {
createButton = `
<button onclick="navigateToPage('fine-tune-create')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
<i class="fa fa-plus mr-1"></i>新建调优任务
</button>
` : ''));
`;
} else if (config.api === 'model-compare') {
createButton = `
<button onclick="showCreateModal('${config.api}')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
<i class="fa fa-plus mr-1"></i>新建对比
</button>
`;
}
// 批量删除按钮(仅当有选中项时显示)
const batchDeleteButton = supportsMultiSelect && selectedItems.size > 0 ? `
<button onclick="batchDeleteItems('${config.api}')" class="bg-red-500 text-white px-4 py-2 rounded text-sm hover:bg-red-600 transition-colors font-medium shadow-sm">
<button onclick="batchDeleteItems('${currentApi}')" class="bg-red-500 text-white px-4 py-2 rounded text-sm hover:bg-red-600 transition-colors font-medium shadow-sm">
<i class="fa fa-trash mr-1"></i>批量删除 (${selectedItems.size})
</button>
` : '';
@@ -1157,7 +1199,7 @@
const selectAllHeader = supportsMultiSelect ? `
<th class="px-4 py-3 text-center font-medium w-10">
<input type="checkbox" class="w-4 h-4 text-primary rounded border-gray-300 cursor-pointer"
onchange="toggleSelectAll(this, '${config.api}')">
onchange="toggleSelectAll(this, '${currentApi}')">
</th>
` : '';
@@ -1212,7 +1254,7 @@
<td class="px-4 py-4 text-sm text-center">
<input type="checkbox" class="w-4 h-4 text-primary rounded border-gray-300 cursor-pointer"
${selectedItems.has(item.name || item.id) ? 'checked' : ''}
onchange="toggleItemSelection('${item.name || item.id}', '${config.api}')">
onchange="toggleItemSelection('${item.name || item.id}', '${currentApi}')">
</td>
` : ''}
${columns.map(col => `
@@ -1222,23 +1264,27 @@
`).join('')}
<td class="px-4 py-4 text-sm text-center">
<div class="flex justify-center space-x-2">
${config.api === 'fine-tune' ? `
${currentApi === 'fine-tune' ? `
<button onclick="viewFineTuneLogs('${item.id}', '${item.name}')" class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600">查看日志</button>
<button onclick="deleteItem('${config.api}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除任务</button>
` : (config.api === 'model-manage/trained-models' ? `
<button onclick="deleteItem('${currentApi}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除任务</button>
` : (currentApi === 'model-manage/trained-models' ? `
${getMergeButtonHtml(item.name, item.train_methods?.[0]?.name || 'lora', item.base_model_path || '', item.merged, item.merging)}
<button onclick="deleteTrainedWeight('${item.name}')" class="bg-orange-500 text-white px-3 py-1 rounded text-xs hover:bg-orange-600">删除权重</button>
${(item.merged && !item.merging) ? `
<button onclick="exportModel('${item.name}')" class="bg-green-500 text-white px-3 py-1 rounded text-xs hover:bg-green-600">导出权重</button>
<button onclick="deleteItem('${config.api}', '${item.name || item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
<button onclick="deleteItem('${currentApi}', '${item.name || item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除模型</button>
` : ''}
` : (config.api === 'model-manage' ? `
` : (currentApi === 'model-manage' ? `
<button onclick="editModel('${item.id}')" class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600">编辑</button>
<button onclick="deleteItem('${config.api}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
` : (config.api === 'dataset-manage' ? `
<button onclick="deleteItem('${currentApi}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
` : (currentApi === 'dataset-manage' ? `
<button onclick="previewDataset('${item.id}')" class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600">预览</button>
<button onclick="downloadDataset('${item.id}')" class="bg-green-500 text-white px-3 py-1 rounded text-xs hover:bg-green-600">下载</button>
<button onclick="deleteItem('${config.api}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
` : '')))}
<button onclick="deleteItem('${currentApi}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
` : (currentApi === 'model-compare' ? `
<button onclick="startCompare('${item.id}')" class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600">开始对比</button>
<button onclick="deleteItem('${currentApi}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
` : ''))))}
</div>
</td>
</tr>
@@ -1463,14 +1509,30 @@
// 获取合并按钮HTML根据合并状态显示不同按钮
function getMergeButtonHtml(name, method, path, merged, merging) {
// 优先检查 sessionStorage 中的临时状态(用于前端实时显示)
const tempStatus = sessionStorage.getItem('merge_status_' + name);
const storageKey = 'merge_status_' + name;
const tempStatus = sessionStorage.getItem(storageKey);
const tempStatusTime = sessionStorage.getItem(storageKey + '_time');
console.log('[DEBUG] getMergeButtonHtml:', name, 'tempStatus:', tempStatus, 'merged:', merged, 'merging:', merging);
// 如果前端正在合并中,显示合并中
if (tempStatus === 'merging') {
return `<button class="bg-gray-300 text-gray-500 px-3 py-1 rounded text-xs cursor-not-allowed flex items-center" disabled>
<i class="fa fa-spinner fa-spin mr-1"></i>合并中...
</button>`;
// 检查临时状态是否过期超过5分钟视为过期
const now = Date.now();
const statusExpired = tempStatusTime && (now - parseInt(tempStatusTime)) > 5 * 60 * 1000;
// 如果状态过期或无效,清除并视为无状态
if (statusExpired || (tempStatus && !tempStatusTime)) {
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey + '_time');
// 继续检查后端状态
} else if (tempStatus === 'merging') {
// 如果后端已经完成合并但前端状态未更新,清除临时状态
if (merged) {
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey + '_time');
} else {
return `<button class="bg-gray-300 text-gray-500 px-3 py-1 rounded text-xs cursor-not-allowed flex items-center" disabled>
<i class="fa fa-spinner fa-spin mr-1"></i>合并中...
</button>`;
}
}
// 如果后端返回正在合并中(锁文件存在)
if (merging) {
@@ -1481,12 +1543,14 @@
// 如果前端成功状态且后端也返回已合并,显示成功
if (tempStatus === 'success' && merged) {
// 清除临时成功状态,让后端状态接管
sessionStorage.removeItem('merge_status_' + name);
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey + '_time');
return `<button class="bg-gray-300 text-gray-500 px-3 py-1 rounded text-xs cursor-not-allowed" disabled>合并成功</button>`;
}
// 如果前端成功状态但后端返回未合并,说明目录被删除,重置状态
if (tempStatus === 'success' && !merged) {
sessionStorage.removeItem('merge_status_' + name);
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey + '_time');
}
// 如果后端返回已合并,显示成功
if (merged) {
@@ -1498,7 +1562,9 @@
// 启动合并任务
async function startMerge(name, method, path) {
// 先设置状态为"合并中"(存储到 sessionStorage
sessionStorage.setItem('merge_status_' + name, 'merging');
const storageKey = 'merge_status_' + name;
sessionStorage.setItem(storageKey, 'merging');
sessionStorage.setItem(storageKey + '_time', Date.now().toString());
// 刷新表格显示合并中状态
loadTableData();
@@ -1517,19 +1583,21 @@
if (result.code === 0) {
// 设置为成功状态,确保即使后端还没更新也能显示成功
sessionStorage.setItem('merge_status_' + name, 'success');
sessionStorage.setItem(storageKey, 'success');
// 延迟刷新表格,用后端真实状态替换前端状态
setTimeout(() => loadTableData(), 1500);
} else {
// 清除合并状态
sessionStorage.removeItem('merge_status_' + name);
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey + '_time');
showMessage('失败', result.message || '合并失败', 'error');
loadTableData();
}
} catch (error) {
console.error('[DEBUG] 合并失败:', error);
// 清除合并状态
sessionStorage.removeItem('merge_status_' + name);
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey + '_time');
showMessage('错误', '合并失败: ' + error.message, 'error');
loadTableData();
}
@@ -1546,6 +1614,29 @@
window.open(`${API_BASE}/model-manage/trained-models/${encodeURIComponent(modelName)}/export`, '_blank');
}
// 删除已训练模型的权重
window.deleteTrainedWeight = function(modelName) {
showConfirm('确认删除', `确定要删除模型 "${modelName}" 的权重文件吗?合并模型不受影响。`, async () => {
try {
const response = await fetch(`${API_BASE}/model-manage/trained-models/${encodeURIComponent(modelName)}?type=lora`, {
method: 'DELETE'
});
const result = await response.json();
if (result.code === 0) {
showMessage('成功', '权重已删除', 'success');
// 清除该模型的合并状态缓存,让前端重新从后端获取状态
sessionStorage.removeItem('merge_status_' + modelName);
sessionStorage.removeItem('merge_status_' + modelName + '_time');
} else {
showMessage('错误', result.message || '删除失败', 'error');
}
} catch (error) {
console.error('删除权重失败:', error);
showMessage('错误', '删除失败: ' + error.message, 'error');
}
});
};
// 编辑模型 - 全局
window.editModel = function(modelId) {
window.location.href = `model-manage-create.html?id=${modelId}`;
@@ -1754,64 +1845,6 @@
}
}
}
// ============ Web日志系统 ============
const webLogger = {
_currentPage: 'main',
// 初始化当前页面名称
init: function(pageName) {
this._currentPage = pageName || 'unknown';
},
// 发送日志到服务器
_sendLog: async function(level, message) {
try {
await fetch(`${API_BASE}/web-log`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
level: level,
message: message,
page: this._currentPage,
timestamp: new Date().toISOString()
})
});
} catch (e) {
// 发送失败时只记录到控制台
console.warn('日志发送失败:', e);
}
},
info: function(message) {
console.log(`[INFO] ${message}`);
this._sendLog('info', message);
},
error: function(message) {
console.error(`[ERROR] ${message}`);
this._sendLog('error', message);
},
warning: function(message) {
console.warn(`[WARNING] ${message}`);
this._sendLog('warning', message);
},
debug: function(message) {
console.debug(`[DEBUG] ${message}`);
this._sendLog('debug', message);
}
};
// 页面加载完成后初始化日志
document.addEventListener('DOMContentLoaded', function() {
// 获取当前页面名称
const path = window.location.pathname;
const pageName = path.split('/').pop().replace('.html', '') || 'main';
webLogger.init(pageName);
webLogger.info('页面加载完成');
});
</script>
<!-- 自定义消息弹窗 -->

View File

@@ -16,24 +16,14 @@
};
}
</script>
<script>
// 设置当前页面,供侧边栏高亮使用
window.sidebarCurrentPage = 'model-compare';
</script>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<!-- 侧边栏加载器 -->
<script src="../js/components/sidebar-loader.js"></script>
<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;
}
.form-input {
width: 100%;
padding: 0.5rem 0.75rem;
@@ -128,125 +118,25 @@
</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="#" 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 sidebar-item-active 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 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 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 class="flex items-center space-x-4">
<a href="main.html?page=model-compare" 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">
<!-- 系统性能监控 -->
<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 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>
@@ -254,23 +144,18 @@
</header>
<!-- 页面内容 -->
<main class="flex-1 overflow-y-auto p-6">
<main class="flex-1 overflow-y-auto p-6 bg-gray-50">
<div class="bg-white rounded-lg shadow-sm">
<!-- 面包屑导航和返回按钮 -->
<div class="px-4 py-3 border-b border-gray-100 flex items-center justify-between">
<div class="flex items-center text-sm">
<a href="main.html?page=model-compare" class="text-primary hover:underline">模型对比</a>
<span class="mx-2 text-gray-300">/</span>
<span class="text-gray-800 font-medium" id="breadcrumbTask">对话</span>
</div>
<a href="main.html?page=model-compare" class="text-gray-500 hover:text-gray-700 text-sm flex items-center">
<i class="fa fa-arrow-left mr-1"></i>返回列表
</a>
</div>
<!-- 页面内容 -->
<div class="p-6">
<form id="chatForm">
<!-- 面包屑导航 -->
<div class="px-4 py-3 border-b border-gray-100 flex items-center justify-between">
<div class="flex items-center text-sm">
<span class="text-primary cursor-pointer hover:underline" onclick="window.location.href='main.html?page=model-compare'">模型对比</span>
<span class="mx-2 text-gray-300">/</span>
<span class="text-gray-800 font-medium" id="breadcrumbTask">对话</span>
</div>
</div>
<div class="p-6">
<form id="chatForm">
<!-- 已配置模型展示 -->
<div class="mb-6">
<label class="form-label flex items-center mb-3">
@@ -584,11 +469,33 @@
return;
}
// 获取推理参数
const temperature = document.getElementById('temperature')?.value || 0.7;
const topP = document.getElementById('topP')?.value || 0.9;
const topK = document.getElementById('topK')?.value || 50;
const maxTokens = document.getElementById('maxTokens')?.value || 2048;
const systemPrompt = document.getElementById('systemPrompt')?.value || '';
// 直接跳转到结果页面不在这里等待API结果
// 结果页面会自动调用API并显示加载状态
const taskName = compareTaskData?.model_name || '对比任务';
const encodedQuestion = encodeURIComponent(userQuestion);
window.location.href = `main.html?page=model-compare-result&taskId=${compareTaskId}&taskName=${encodeURIComponent(taskName)}&question=${encodedQuestion}&real=1`;
const encodedSystemPrompt = encodeURIComponent(systemPrompt);
// 构建 URL传递推理参数
const params = new URLSearchParams({
taskId: compareTaskId,
taskName: taskName,
question: userQuestion,
real: '1',
temperature: temperature,
topP: topP,
topK: topK,
maxTokens: maxTokens,
systemPrompt: systemPrompt
});
window.location.href = `main.html?page=model-compare-result&${params.toString()}`;
}
// 返回

File diff suppressed because it is too large Load Diff

View File

@@ -16,24 +16,14 @@
};
}
</script>
<script>
// 设置当前页面,供侧边栏高亮使用
window.sidebarCurrentPage = 'model-compare';
</script>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<!-- 侧边栏加载器 -->
<script src="../js/components/sidebar-loader.js"></script>
<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; }
:root { --primary: #1890ff; }
@@ -91,91 +81,8 @@
</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="#" 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 sidebar-item-active 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 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 id="sidebar-container"></div>
<div class="flex-1 flex flex-col overflow-hidden">
<!-- 顶部导航 -->
<header class="bg-white border-b border-gray-200 shadow-sm">
@@ -270,6 +177,11 @@
let compareTaskId = null;
let taskName = '';
let userQuestion = '';
let systemPrompt = '';
let temperature = 0.7;
let topP = 0.9;
let topK = 50;
let maxTokens = 2048;
let chatResults = [];
let useLocalStorageResults = false; // 是否使用localStorage中的结果
@@ -281,6 +193,13 @@
"根据我的理解,这个问题涉及到以下几个方面:首先,需要明确问题的具体背景;其次,要分析相关的技术方案;最后,需要评估实施的成本和收益。建议您先收集更多信息再做决定。"
];
// 模型类型常量
const MODEL_TYPE = {
API: 'api', // API调用模型如OpenAI、百度等
LOCAL: 'local', // 本地模型vLLM
TRAINED: 'trained' // 训练后的模型llamafactory
};
// 页面初始化
async function initPage() {
try {
@@ -288,8 +207,15 @@
compareTaskId = urlParams.get('taskId');
taskName = urlParams.get('taskName') || '对比任务';
userQuestion = decodeURIComponent(urlParams.get('question') || '');
systemPrompt = decodeURIComponent(urlParams.get('systemPrompt') || '');
temperature = parseFloat(urlParams.get('temperature') || 0.7);
topP = parseFloat(urlParams.get('topP') || 0.9);
topK = parseInt(urlParams.get('topK') || 50);
maxTokens = parseInt(urlParams.get('maxTokens') || 2048);
const needRealData = urlParams.get('real') === '1';
console.log('[INIT] 推理参数:', { temperature, topP, topK, maxTokens, systemPrompt });
// 设置用户提问
const questionTextEl = document.getElementById('questionText');
if (questionTextEl) {
@@ -345,25 +271,107 @@
}
}
// 调用批量对话 API 获取结果
async function fetchChatResults() {
// 使用 transformers 本地模型进行对话
async function chatWithLocalModel(modelPath, modelName) {
try {
const response = await fetch(`${API_BASE}/model-chat/batch`, {
const response = await fetch(`${API_BASE}/model-chat/local/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model_ids: selectedModelIds,
system_prompt: '',
model_path: modelPath,
system_prompt: systemPrompt,
user_question: userQuestion,
temperature: 0.7,
max_tokens: 2048
temperature: temperature,
max_tokens: maxTokens
})
});
const result = await response.json();
if (result.code === 0 && result.data) {
chatResults = result.data;
return {
model_id: modelName,
model_name: modelName,
success: true,
response: result.data.response || ''
};
} else {
return {
model_id: modelName,
model_name: modelName,
success: false,
error: result.message || '推理失败'
};
}
} catch (error) {
return {
model_id: modelName,
model_name: modelName,
success: false,
error: error.message
};
}
}
// 调用批量对话 API 获取结果
async function fetchChatResults() {
try {
// 1. 预加载本地和训练后的模型
console.log('[FETCH] 开始预加载本地/训练模型...');
const { localModelsNeedingPreload, trainedModelsNeedingPreload } = await preloadAllModels(selectedModelIds);
// 2. 获取所有模型详情,按类型分组
const apiModels = []; // API 调用模型 (使用 batch API)
const localModels = []; // 本地 transformers 模型
const trainedModels = []; // 训练后的 llamafactory 模型
for (const modelId of selectedModelIds) {
const model = await getModelDetails(modelId);
if (model) {
const modelType = getModelType(model);
if (modelType === MODEL_TYPE.API) {
apiModels.push(modelId);
} else if (modelType === MODEL_TYPE.LOCAL) {
localModels.push({ id: modelId, name: model.name || modelId, path: model.path });
} else if (modelType === MODEL_TYPE.TRAINED) {
trainedModels.push({ id: modelId, name: model.name || modelId, basePath: model.path || '' });
}
}
}
// 3. 收集结果
let results = [];
// API 模型使用批量接口
if (apiModels.length > 0) {
console.log(`[FETCH] 调用 API 模型批量接口: ${apiModels.length} 个模型`);
const response = await fetch(`${API_BASE}/model-chat/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model_ids: apiModels,
system_prompt: systemPrompt,
user_question: userQuestion,
temperature: temperature,
max_tokens: maxTokens
})
});
const result = await response.json();
if (result.code === 0 && result.data) {
results = results.concat(result.data);
}
}
// 本地 transformers 模型使用单独接口
for (const lm of localModels) {
console.log(`[FETCH] 调用本地模型: ${lm.name}, 路径: ${lm.path}`);
const result = await chatWithLocalModel(lm.path, lm.name);
results.push(result);
}
// 训练后的 llamafactory 模型使用单独接口(如果有的话)
// 这里假设 batch API 也能处理训练后的模型,如果没有则需要单独实现
chatResults = results;
console.log(`[FETCH] 共获取 ${results.length} 个模型的结果`);
} catch (error) {
console.error('调用API失败:', error);
}
@@ -415,6 +423,200 @@
}
}
// 获取模型详情
async function getModelDetails(modelId) {
// 支持 ID 数字或模型名称字符串
const model = allModels.find(m =>
m.id == modelId || m.id === modelId || m.name === modelId
);
if (model) return model;
// 如果没找到,尝试从 API 获取
try {
// 判断是数字ID还是名称
const isNumericId = /^\d+$/.test(modelId);
let apiUrl;
if (isNumericId) {
apiUrl = `${API_BASE}/model-manage/${modelId}`;
} else {
// 名称使用 name/<model_name> 端点
apiUrl = `${API_BASE}/model-manage/name/${encodeURIComponent(modelId)}`;
}
const response = await fetch(apiUrl);
const result = await response.json();
if (result.code === 0) {
return result.data;
}
} catch (e) {
console.error('获取模型详情失败:', e);
}
return null;
}
// 判断模型类型
function getModelType(model) {
if (!model) return null;
const source = model.model_source || '';
// 训练后的模型
if (source === 'trained' || model.is_trained === true) {
return MODEL_TYPE.TRAINED;
}
// 本地模型 (path 以 :// 开头表示 vLLM API 地址,或者是 transformers 本地路径)
if (source === 'local' || (model.path && model.path.includes('://'))) {
return MODEL_TYPE.LOCAL;
}
// API 调用模型
if (source === 'api') {
return MODEL_TYPE.API;
}
return null;
}
// 判断是否为本地模型或训练后的模型
function isLocalOrTrainedModel(model) {
const type = getModelType(model);
return type === MODEL_TYPE.LOCAL || type === MODEL_TYPE.TRAINED;
}
// 预加载训练后的模型 (使用 llamafactory)
async function preloadTrainedModel(modelName, baseModelPath) {
try {
const response = await fetch(`${API_BASE}/model-chat/trained/preload`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model_name: modelName,
train_method: 'lora',
base_model_path: baseModelPath
})
});
const result = await response.json();
if (result.code === 0) {
console.log(`[PRELOAD] 训练模型 ${modelName} 预加载成功`);
return true;
} else {
console.warn(`[PRELOAD] 训练模型 ${modelName} 预加载失败: ${result.message}`);
return false;
}
} catch (error) {
console.error(`[PRELOAD] 训练模型 ${modelName} 预加载异常:`, error);
return false;
}
}
// 预加载本地模型 (使用 transformers)
async function preloadLocalModel(modelPath, modelName) {
try {
const response = await fetch(`${API_BASE}/model-chat/local/preload`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model_path: modelPath,
model_name: modelName || '本地模型'
})
});
const result = await response.json();
if (result.code === 0) {
console.log(`[PRELOAD] 本地模型 ${modelName} 预加载成功`);
return true;
} else {
console.warn(`[PRELOAD] 本地模型 ${modelName} 预加载失败: ${result.message}`);
return false;
}
} catch (error) {
console.error(`[PRELOAD] 本地模型 ${modelName} 预加载异常:`, error);
return false;
}
}
// 更新卡片状态显示
function updateCardStatus(modelId, status, message) {
const statusEl = document.getElementById(`status-${modelId}`);
if (statusEl) {
statusEl.innerHTML = `<i class="fa ${status === 'loading' ? 'fa-spinner fa-spin' : status === 'success' ? 'fa-check-circle text-green-500' : 'fa-times-circle text-red-500'} mr-1"></i> ${message}`;
if (status === 'loading') {
statusEl.classList.remove('text-gray-400', 'text-green-500', 'text-red-500');
statusEl.classList.add('text-primary');
} else if (status === 'success') {
statusEl.classList.remove('text-primary', 'text-gray-400', 'text-red-500');
statusEl.classList.add('text-green-500');
} else if (status === 'error') {
statusEl.classList.remove('text-primary', 'text-gray-400', 'text-green-500');
statusEl.classList.add('text-red-500');
}
}
}
// 预加载所有本地和训练后的模型
async function preloadAllModels(modelIds) {
const localModelsNeedingPreload = [];
const trainedModelsNeedingPreload = [];
for (const modelId of modelIds) {
const model = await getModelDetails(modelId);
if (model) {
const modelType = getModelType(model);
// 本地模型 (transformers)
if (modelType === MODEL_TYPE.LOCAL) {
localModelsNeedingPreload.push({
id: modelId,
name: model.name || modelId,
path: model.path || ''
});
}
// 训练后的模型 (llamafactory)
if (modelType === MODEL_TYPE.TRAINED) {
trainedModelsNeedingPreload.push({
id: modelId,
name: model.name || modelId,
basePath: model.path || model.base_model_path || ''
});
}
}
}
// 预加载本地模型 (transformers)
if (localModelsNeedingPreload.length > 0) {
// 更新所有本地模型的状态为"加载中"
for (const lm of localModelsNeedingPreload) {
updateCardStatus(lm.id, 'loading', '正在加载模型...');
}
// 预加载每个本地模型
for (const lm of localModelsNeedingPreload) {
console.log(`[PRELOAD] 开始预加载本地模型: ${lm.name}, 路径: ${lm.path}`);
await preloadLocalModel(lm.path, lm.name);
}
// 更新状态为"加载完成"
for (const lm of localModelsNeedingPreload) {
updateCardStatus(lm.id, 'success', '模型已加载');
}
}
// 预加载训练后的模型 (llamafactory)
if (trainedModelsNeedingPreload.length > 0) {
// 更新所有训练模型的状态为"加载中"
for (const tm of trainedModelsNeedingPreload) {
updateCardStatus(tm.id, 'loading', '正在加载模型...');
}
// 预加载每个训练模型
for (const tm of trainedModelsNeedingPreload) {
console.log(`[PRELOAD] 开始预加载训练模型: ${tm.name}`);
await preloadTrainedModel(tm.name, tm.basePath);
}
// 更新状态为"加载完成"
for (const tm of trainedModelsNeedingPreload) {
updateCardStatus(tm.id, 'success', '模型已加载');
}
}
return { localModelsNeedingPreload, trainedModelsNeedingPreload };
}
// 初始化输出卡片
function initializeOutputCards(showLoading = false) {
const grid = document.getElementById('outputGrid');
@@ -425,10 +627,10 @@
? selectedModelIds.slice(0, 4)
: [1, 2, 3, 4];
const statusText = showLoading ? '加载中...' : '等待中';
const statusText = showLoading ? '准备中...' : '等待中';
const statusIcon = showLoading ? 'fa-spinner fa-spin' : 'fa-clock-o';
const statusClass = showLoading ? 'text-primary' : 'text-gray-400';
const contentText = showLoading ? '<span class="text-gray-400">正在调用模型API...</span>' : '<span class="text-gray-300">模型即将开始生成回答...</span>';
const contentText = showLoading ? '<span class="text-gray-400">正在加载模型并准备推理...</span>' : '<span class="text-gray-300">模型即将开始生成回答...</span>';
grid.innerHTML = displayModelIds.map((modelId, index) => {
// 支持 ID 数字或模型名称字符串匹配

View File

@@ -6,37 +6,13 @@
<title>添加评测维度 / 远光软件微调平台</title>
<script src="../lib/tailwindcss/tailwind.js"></script>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<script>
// 设置当前页面,供侧边栏高亮使用
window.sidebarCurrentPage = 'model-eval';
</script>
<!-- 侧边栏加载器 -->
<script src="../js/components/sidebar-loader.js"></script>
<style>
/* 侧边栏滑块动画 */
.sidebar-slider {
position: absolute;
width: 4px;
height: 0;
background-color: #1890ff;
border-radius: 0 2px 2px 0;
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
z-index: 10;
}
.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);
}
.nav-item-wrapper {
position: relative;
}
.nav-link {
position: relative;
z-index: 1;
}
.form-input {
width: 100%;
padding: 0.5rem 0.75rem;
@@ -183,94 +159,8 @@
</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-slider" id="sidebar-slider"></div>
<!-- 第一分区:模型服务 -->
<div class="sidebar-section-title">模型服务</div>
<div class="nav-item-wrapper">
<a href="main.html" 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="main.html?page=model-eval" 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="main.html?page=model-compare" 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="main.html?page=model-manage" data-page="model-manage" class="nav-link 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="main.html?page=dataset-manage" 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="main.html?page=data-generate" 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="main.html?page=config" 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="main.html?page=logs" 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 id="sidebar-container"></div>
<div class="flex-1 flex flex-col overflow-hidden">
<!-- 顶部导航 -->
<header class="bg-white border-b border-gray-200 shadow-sm">

View File

@@ -16,38 +16,14 @@
};
}
</script>
<script>
// 设置当前页面,供侧边栏高亮使用
window.sidebarCurrentPage = 'model-eval';
</script>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<!-- 侧边栏加载器 -->
<script src="../js/components/sidebar-loader.js"></script>
<style>
/* 侧边栏滑块动画 */
.sidebar-slider {
position: absolute;
width: 4px;
height: 0;
background-color: #1890ff;
border-radius: 0 2px 2px 0;
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
z-index: 10;
}
.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);
}
.nav-item-wrapper {
position: relative;
}
.nav-link {
position: relative;
z-index: 1;
}
.form-input {
width: 100%;
padding: 0.5rem 0.75rem;
@@ -104,94 +80,8 @@
</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-slider" id="sidebar-slider"></div>
<!-- 第一分区:模型服务 -->
<div class="sidebar-section-title">模型服务</div>
<div class="nav-item-wrapper">
<a href="main.html" 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="main.html?page=model-eval" 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="main.html?page=model-compare" 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="main.html?page=model-manage" data-page="model-manage" class="nav-link 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="main.html?page=dataset-manage" 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="main.html?page=data-generate" 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="main.html?page=config" 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="main.html?page=logs" 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 id="sidebar-container"></div>
<div class="flex-1 flex flex-col overflow-hidden">
<!-- 顶部导航 -->
<header class="bg-white border-b border-gray-200 shadow-sm">

View File

@@ -17,47 +17,23 @@
}
}
</script>
<script>
// 设置当前页面,供侧边栏高亮使用
window.sidebarCurrentPage = 'model-manage';
</script>
<!-- 侧边栏加载器 -->
<script src="../../js/components/sidebar-loader.js"></script>
<style>
.sidebar-item-active {
background-color: #1890ff !important;
color: white !important;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
</style>
</head>
<body class="bg-gray-100 h-screen flex overflow-hidden">
<!-- 侧边栏 -->
<aside class="w-56 bg-[#001529] text-white flex flex-col shrink-0">
<div class="h-14 flex items-center px-4 border-b border-[#001529]/30">
<i class="fa fa-cube text-primary text-xl"></i>
<span class="ml-2 font-medium text-lg">YG_FT</span>
</div>
<!-- 侧边栏容器 -->
<div id="sidebar-container"></div>
<nav class="flex-1 overflow-y-auto py-4">
<!-- 第一分区:模型管理 -->
<div class="sidebar-section-title">模型管理</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-cube w-5 text-center"></i>
<span class="ml-2">我的模型</span>
</a>
</div>
<div class="nav-item-wrapper">
<a href="main.html?page=model-create" data-page="model-create" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-plus w-5 text-center"></i>
<span class="ml-2">添加模型</span>
</a>
</div>
<div class="nav-item-wrapper">
<a href="main.html?page=model-compare" 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-clone 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="main.html?page=fine-tune" data-page="fine-tune" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">

View File

@@ -16,19 +16,14 @@
};
}
</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>
.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);
}
.form-input {
width: 100%;
padding: 0.5rem 0.75rem;
@@ -69,117 +64,12 @@
.text-primary { color: #1890ff; }
.border-primary { border-color: #1890ff; }
:root { --primary: #1890ff; --danger: #f5222d; --success: #52c41a; }
/* 侧边栏滑块动画 */
.sidebar-slider {
position: absolute;
width: 4px;
height: 0;
background-color: #1890ff;
border-radius: 0 2px 2px 0;
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
z-index: 10;
}
.nav-item-wrapper {
position: relative;
}
.nav-link {
position: relative;
z-index: 1;
}
</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-slider" id="sidebar-slider"></div>
<!-- 第一分区:模型服务 -->
<div class="sidebar-section-title">模型服务</div>
<div class="nav-item-wrapper">
<a href="main.html" 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="main.html?page=model-eval" 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="main.html?page=model-compare" 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="main.html?page=model-manage" data-page="model-manage" class="nav-link 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="main.html?page=dataset-manage" 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="main.html?page=data-generate" 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="main.html?page=config" 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="main.html?page=logs" 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 id="sidebar-container"></div>
<div class="flex-1 flex flex-col overflow-hidden">
<!-- 顶部导航 -->
<header class="bg-white border-b border-gray-200 shadow-sm">

View File

@@ -17,24 +17,14 @@
};
}
</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>
.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; }
@@ -88,91 +78,8 @@
</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 id="sidebar-container"></div>
<div class="flex-1 flex flex-col overflow-hidden">
<!-- 顶部导航 -->
<header class="bg-white border-b border-gray-200 shadow-sm">

View File

@@ -5,120 +5,40 @@
<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>
<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;
}
:root { --primary: #1890ff; --danger: #f5222d; --success: #52c41a; }
.bg-primary { background-color: var(--primary); }
.text-primary { color: var(--primary); }
.border-primary { border-color: var(--primary); }
.hover\:bg-primary\/90:hover { background-color: rgba(24, 144, 255, 0.9); }
.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; }
.sidebar-item-active { background-color: rgba(24, 144, 255, 0.1); color: #1890ff; border-left: 4px solid #1890ff; }
</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="main.html?page=fine-tune" 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 hidden">
<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="main.html?page=model-eval" 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="main.html?page=model-compare" 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><a href="main.html?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><a href="main.html?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><a href="main.html?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="main.html?page=model-manage" data-page="model-manage" class="nav-link 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="main.html?page=dataset-manage" 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="tools.html" data-page="data-generate" class="nav-link sidebar-item-active 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><a href="main.html?page=model-manage" class="nav-link 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><a href="main.html?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><a href="tools.html" class="nav-link sidebar-item-active flex items-center px-4 py-2.5"><i class="fa fa-wrench 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="main.html?page=config" 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="logs.html" 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>
<div><a href="hardware.html" 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><a href="logs.html" 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 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>