7 Commits
Author SHA1 Message Date
hz4th_coder 5169d368bf fix: v1.4.2
1. 填了模型路径后红色线框变橙色(已更改标记)
   - setParam时实时更新线框样式,保留焦点不重建DOM
2. 二进制程序区域宽度缩小一半,运行模式区域扩大
2026-07-20 00:30:21 +08:00
hz4th_coder c12c2240a2 fix: v1.4.1 - 三项优化
1. 选模型后参数自适应调整:
   - n_gpu_layers 设为模型实际层数(而非'all')
   - 多卡时 split_mode 设为 tensor
   - tensor_split 按各卡显存比例自动计算
   - 增减/切换显卡时也自动更新 tensor_split

2. 模型路径为空时红色线框标记

3. 参数值非默认时用橙色线框标记(非红非绿)
2026-07-20 00:21:09 +08:00
hz4th_coder 26d7ee0b66 feat: v1.4.0 - 五项优化
1. 模型选择: 点击搜索框弹出top5模型,支持搜索过滤
2. 选模型后自动调整参数: ctx_size设为模型默认上下文,n_gpu_layers设为all
   - models表新增 default_ctx 字段,各模型预设上下文值
3. 参数配置新增「已改参数」标签页
   - 放在最右边,绿色样式区别于其他标签
   - 显示所有被修改过(非默认值)的参数
4. 复制按钮移到命令文本框左上角悬空
5. 自然语言默认提示文本可在后台管理中编辑
   - 新增 /api/settings/public 接口
   - 后台系统设置新增 nl_default_text 配置项
2026-07-20 00:05:18 +08:00
hz4th_coder fb3d7290bf fix: v1.3.1 - 四项优化
1. VRAM/内存进度条改为仅 sticky 置顶显示
   - 去掉页面内重复的 vram-display 进度条
   - 只保留顶部 sticky bar,滚动时始终可见

2. 去掉参数修改标记
   - 不再用红色线框标记已更改的参数

3. 命令区域简化
   - 去掉独立的「复制命令」和「重新生成」按钮
   - 复制按钮改为命令文本框右下角悬空小按钮

4. h2 标题 margin-top 改为 0
2026-07-19 23:48:18 +08:00
hz4th_coder 3c27a205e5 feat: v1.3.0 - 五项优化
1. 显存/内存进度条置顶显示 (sticky)
   - 滚动页面时始终可见 VRAM 和内存占用情况
   - GPU+CPU 模式时内存条也同步显示在 sticky 区域

2. 系统内存配置合并到 GPU 配置区域内
   - GPU+CPU 模式时无缝衔接在显存占用下方
   - 去掉单独面板,去掉单位选择,固定为 GB
   - 填入 >0 即为有限制,0 为无限制

3. 参数同时显示简写和完整标志
   - 如 -ngl / --n-gpu-layers

4. 生成命令区域增加智能提示
   - 显存不足时提示调整参数或切换模式
   - 内存不足时提示增加上限或减少 GPU 层
   - 显存充足时显示确认信息
   - 未选模型/GPU 时给出引导提示

5. 自然语言输入改为多行 textarea
   - 支持多行输入
   - placeholder 说明可以自然对话描述需求
2026-07-19 23:35:20 +08:00
hz4th_coder 6691f37e45 feat: v1.2.0 - 三项优化
1. 模型选择改为两步式:先搜索选择基模型,再选择量化版本
   - 自动选中默认量化版本(Q4_K_M),后台可配置
   - 模型新增 base_model 字段,按基模型分组
   - 后台模型管理增加基模型字段

2. 参数搜索改为全局搜索
   - 搜索时跨所有分类显示匹配参数
   - 隐藏分类标签页,显示匹配数量

3. 自然语言支持 LLM 接口
   - 后台可配置 LLM API (URL/Key/Model/System Prompt)
   - 启用后优先调用 LLM 解析,失败自动回退正则解析
   - 兼容 OpenAI API 格式
2026-07-19 22:57:38 +08:00
hz4th_coder a115c5f7b4 docs: 添加 README.md 2026-07-19 18:50:43 +08:00
8 changed files with 810 additions and 422 deletions
+106
View File
@@ -0,0 +1,106 @@
# 🦙 llama.cpp 命令生成器
> 一个用于生成 llama.cpp 执行命令的 Web 工具,支持参数可视化配置、自然语言生成、实时显存估算。
## ✨ 功能特性
### 命令生成
- **多版本支持**:内置 b6310 和 b10068 两个版本的参数定义,可按版本精准匹配参数
- **GPU 模式 / GPU+CPU 模式**:默认 GPU 模式,可切换混合模式查看系统内存占用
- **多 GPU**:最多支持 4 张显卡,每张可选不同型号,自动计算总显存
- **智能输出**:仅输出非默认值的参数,生成干净可用的命令
### 自然语言生成
输入自然语言描述,自动解析为参数配置:
```
用RTX 4090跑70B模型, 上下文8192, 温度0.7, flash attention
```
支持中文和英文关键词,涵盖 GPU 型号、数量、上下文、温度、Top-K/P、线程数、端口等。
### 实时显存估算
- 选择模型和 GPU 后实时计算显存占用
- 分项显示:模型权重 / KV缓存 / 计算开销 / CUDA开销
- 进度条颜色预警:绿(正常) → 黄(警告) → 红(超限)
- GPU+CPU 模式额外显示 CPU 侧权重和 KV 缓存占用
### 参数管理
- 参数按分类标签页组织:通用 / 采样 / 服务器 / 模型来源 / LoRA / 日志 / 高级
- 重要参数直接显示,次要参数点击展开
- 影响显存的参数标记 ⚡ 标识
- 支持参数搜索(按键名、标志、描述)
### 模型选择
- 内置 26 个常见模型预设(Llama 3/3.1、Qwen2.5、DeepSeek V2/R1、Mistral、Gemma2、Phi-3、GLM-4 等)
- 支持搜索过滤
- 后台可增删改
### 后台管理
- **GPU 管理**:增删改查显卡型号、显存、计算能力
- **版本管理**:增删改查 llama.cpp 版本
- **参数管理**:按版本增删改查参数定义
- **模型管理**:增删改查模型预设
- **系统设置**:修改管理密码等配置
## 🚀 部署
### 环境要求
- Python 3.8+
- Flask
- flask-cors
### 安装
```bash
cd llama-cmd-gen
pip install flask flask-cors
```
### 启动
```bash
python3 app.py
# 或
./start.sh
```
服务默认监听 `0.0.0.0:16052`
### 访问
- **主界面**`http://<host>:16052/`
- **后台管理**`http://<host>:16052/admin`(需密码登录,默认 `admin123`
## 📁 项目结构
```
llama-cmd-gen/
├── app.py # Flask 主应用 + API
├── db.py # 数据库初始化 + 默认数据
├── data.db # SQLite 数据库(运行时生成)
├── start.sh # 启动脚本
├── requirements.txt # Python 依赖
├── logs/
│ └── app.log # 运行日志
└── static/
├── index.html # 主界面
├── admin.html # 后台管理界面
├── css/
│ └── style.css # 样式
└── js/
├── main.js # 主界面逻辑
└── admin.js # 后台逻辑
```
## 🛠️ 技术栈
- **后端**Python / Flask / SQLite
- **前端**:原生 HTML / CSS / JavaScript(无框架依赖)
- **数据**SQLite 本地存储,包含 GPU、版本、参数、模型等表
## 📝 版本历史
- **v1.1.0** — 修复输入焦点丢失、加参数搜索、模型选择器、后台密码登录、复制兼容性
- **v1.0.0** — 初始版本:命令生成、自然语言解析、显存估算、多 GPU、后台管理
## 📄 License
MIT
+88 -8
View File
@@ -7,6 +7,7 @@ import json
import re
import math
import functools
import urllib.request
from flask import Flask, request, jsonify, send_from_directory, session, redirect
from flask_cors import CORS
from db import get_db, init_db, DB_PATH
@@ -600,16 +601,95 @@ def get_models():
db.close()
return jsonify(result)
@app.route('/api/models/grouped')
def get_models_grouped():
db = get_db()
models = db.execute('SELECT * FROM models ORDER BY base_model, sort_order, name').fetchall()
db.close()
grouped = {}
for m in models:
d = dict(m)
base = d['base_model']
if base not in grouped:
grouped[base] = []
grouped[base].append(d)
# Get default quant from settings
db2 = get_db()
dq = db2.execute("SELECT value FROM settings WHERE key = 'default_quant'").fetchone()
db2.close()
default_quant = dq['value'] if dq else 'Q4_K_M'
return jsonify({'models': grouped, 'default_quant': default_quant})
# ----- Public Settings (only nl_default_text) -----
@app.route('/api/settings/public')
def get_public_settings():
db = get_db()
nl = db.execute("SELECT value FROM settings WHERE key = 'nl_default_text'").fetchone()
db.close()
return jsonify({'nl_default_text': nl['value'] if nl else ''})
# ----- Parse Natural Language -----
@app.route('/api/parse-nl', methods=['POST'])
def parse_nl():
data = request.json
text = data.get('text', '')
# Try LLM API first if enabled
db = get_db()
llm_enabled = db.execute("SELECT value FROM settings WHERE key = 'llm_enabled'").fetchone()
if llm_enabled and llm_enabled['value'] == 'true':
llm_url = db.execute("SELECT value FROM settings WHERE key = 'llm_api_url'").fetchone()
llm_key = db.execute("SELECT value FROM settings WHERE key = 'llm_api_key'").fetchone()
llm_model = db.execute("SELECT value FROM settings WHERE key = 'llm_api_model'").fetchone()
llm_prompt = db.execute("SELECT value FROM settings WHERE key = 'llm_system_prompt'").fetchone()
db.close()
url = llm_url['value'] if llm_url else ''
key = llm_key['value'] if llm_key else ''
model = llm_model['value'] if llm_model else ''
system_prompt = llm_prompt['value'] if llm_prompt else ''
if url:
try:
result = call_llm_for_parsing(url, key, model, system_prompt, text)
if result:
return jsonify(result)
except Exception as e:
print(f'LLM parse failed: {e}', file=sys.stderr)
else:
db.close()
# Fallback to regex parsing
result = parse_natural_language(text)
return jsonify(result)
def call_llm_for_parsing(url, key, model, system_prompt, user_text):
"""Call LLM API to parse natural language into params."""
headers = {'Content-Type': 'application/json'}
if key:
headers['Authorization'] = f'Bearer {key}'
body = {
'model': model,
'messages': [
{'role': 'system', 'content': system_prompt},
{'role': 'user', 'content': user_text}
],
'temperature': 0.1,
'max_tokens': 2000,
}
req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method='POST')
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode('utf-8'))
# OpenAI-compatible response
content = data['choices'][0]['message']['content']
# Try to extract JSON from the response
content = content.strip()
if content.startswith('```'):
content = re.sub(r'^```\w*\n?', '', content)
content = re.sub(r'\n?```$', '', content)
result = json.loads(content)
return result
# ==================== Admin API ====================
# All admin routes below require login
@@ -800,11 +880,11 @@ def admin_add_model():
data = request.json
db = get_db()
db.execute(
'''INSERT INTO models (name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, quant, description, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
(data['name'], data['size_gb'], data['layers'], data['embd'],
'''INSERT INTO models (base_model, name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, default_ctx, quant, description, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
(data['base_model'], data['name'], data['size_gb'], data['layers'], data['embd'],
data['kv_heads'], data['head_dim'], data['attention_heads'],
data.get('quant', ''), data.get('description', ''), data.get('sort_order', 0))
data.get('default_ctx', 0), data.get('quant', ''), data.get('description', ''), data.get('sort_order', 0))
)
db.commit()
db.close()
@@ -818,11 +898,11 @@ def admin_model_edit(mid):
if request.method == 'PUT':
data = request.json
db.execute(
'''UPDATE models SET name=?, size_gb=?, layers=?, embd=?, kv_heads=?,
head_dim=?, attention_heads=?, quant=?, description=?, sort_order=? WHERE id=?''',
(data['name'], data['size_gb'], data['layers'], data['embd'],
'''UPDATE models SET base_model=?, name=?, size_gb=?, layers=?, embd=?, kv_heads=?,
head_dim=?, attention_heads=?, default_ctx=?, quant=?, description=?, sort_order=? WHERE id=?''',
(data['base_model'], data['name'], data['size_gb'], data['layers'], data['embd'],
data['kv_heads'], data['head_dim'], data['attention_heads'],
data.get('quant', ''), data.get('description', ''), data.get('sort_order', 0), mid)
data.get('default_ctx', 0), data.get('quant', ''), data.get('description', ''), data.get('sort_order', 0), mid)
)
db.commit()
db.close()
+65 -28
View File
@@ -80,6 +80,7 @@ def init_db():
c.execute('''
CREATE TABLE IF NOT EXISTS models (
id INTEGER PRIMARY KEY AUTOINCREMENT,
base_model TEXT NOT NULL,
name TEXT NOT NULL,
size_gb REAL NOT NULL,
layers INTEGER NOT NULL,
@@ -87,6 +88,7 @@ def init_db():
kv_heads INTEGER NOT NULL,
head_dim INTEGER NOT NULL,
attention_heads INTEGER NOT NULL,
default_ctx INTEGER DEFAULT 0,
quant TEXT DEFAULT '',
description TEXT DEFAULT '',
sort_order INTEGER DEFAULT 0
@@ -312,44 +314,79 @@ def insert_default_data(conn):
(v2_id,) + p)
# ===== Default models =====
# (base_model, name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, default_ctx, quant, description, sort_order)
default_models = [
("Llama-3-8B-Instruct (Q4_K_M)", 4.9, 32, 4096, 8, 128, 32, "Q4_K_M", "Meta Llama 3 8B Instruct, Q4_K_M 量化", 1),
("Llama-3-8B-Instruct (Q8_0)", 8.5, 32, 4096, 8, 128, 32, "Q8_0", "Meta Llama 3 8B Instruct, Q8_0 量化", 2),
("Llama-3-8B-Instruct (FP16)", 15.5, 32, 4096, 8, 128, 32, "FP16", "Meta Llama 3 8B Instruct, FP16", 3),
("Llama-3-70B-Instruct (Q4_K_M)", 38.5, 80, 8192, 8, 128, 64, "Q4_K_M", "Meta Llama 3 70B Instruct, Q4_K_M 量化", 4),
("Llama-3-70B-Instruct (Q8_0)", 74.0, 80, 8192, 8, 128, 64, "Q8_0", "Meta Llama 3 70B Instruct, Q8_0 量化", 5),
("Llama-3-70B-Instruct (FP16)", 138.0, 80, 8192, 8, 128, 64, "FP16", "Meta Llama 3 70B Instruct, FP16", 6),
("Llama-3.1-8B-Instruct (Q4_K_M)", 4.9, 32, 4096, 8, 128, 32, "Q4_K_M", "Meta Llama 3.1 8B Instruct, Q4_K_M 量化", 7),
("Llama-3.1-70B-Instruct (Q4_K_M)", 38.5, 80, 8192, 8, 128, 64, "Q4_K_M", "Meta Llama 3.1 70B Instruct, Q4_K_M 量化", 8),
("Llama-3.1-70B-Instruct (Q8_0)", 74.0, 80, 8192, 8, 128, 64, "Q8_0", "Meta Llama 3.1 70B Instruct, Q8_0 量化", 9),
("Llama-3.1-405B-Instruct (Q4_K_M)", 226.0, 126, 16384, 8, 128, 128, "Q4_K_M", "Meta Llama 3.1 405B Instruct, Q4_K_M 量化", 10),
("Qwen2.5-7B-Instruct (Q4_K_M)", 4.7, 28, 3584, 4, 128, 28, "Q4_K_M", "Qwen2.5 7B Instruct, Q4_K_M 量化", 11),
("Qwen2.5-14B-Instruct (Q4_K_M)", 8.7, 40, 5120, 8, 128, 40, "Q4_K_M", "Qwen2.5 14B Instruct, Q4_K_M 量化", 12),
("Qwen2.5-32B-Instruct (Q4_K_M)", 19.5, 64, 5120, 8, 128, 64, "Q4_K_M", "Qwen2.5 32B Instruct, Q4_K_M 量化", 13),
("Qwen2.5-72B-Instruct (Q4_K_M)", 42.0, 80, 8192, 8, 128, 64, "Q4_K_M", "Qwen2.5 72B Instruct, Q4_K_M 量化", 14),
("Qwen2.5-72B-Instruct (Q8_0)", 75.0, 80, 8192, 8, 128, 64, "Q8_0", "Qwen2.5 72B Instruct, Q8_0 量化", 15),
("DeepSeek-V2-Chat (Q4_K_M)", 23.0, 60, 5120, 8, 128, 60, "Q4_K_M", "DeepSeek V2 Chat, Q4_K_M 量化", 16),
("DeepSeek-V2.5-Chat (Q4_K_M)", 23.0, 60, 5120, 8, 128, 60, "Q4_K_M", "DeepSeek V2.5 Chat, Q4_K_M 量化", 17),
("DeepSeek-R1-Distill-Qwen-32B (Q4_K_M)", 19.5, 64, 5120, 8, 128, 64, "Q4_K_M", "DeepSeek R1 Distill Qwen 32B, Q4_K_M 量化", 18),
("DeepSeek-R1-Distill-Llama-70B (Q4_K_M)", 42.0, 80, 8192, 8, 128, 64, "Q4_K_M", "DeepSeek R1 Distill Llama 70B, Q4_K_M 量化", 19),
("Mistral-7B-Instruct-v0.3 (Q4_K_M)", 4.4, 32, 4096, 8, 128, 32, "Q4_K_M", "Mistral 7B Instruct v0.3, Q4_K_M 量化", 20),
("Mixtral-8x7B-Instruct (Q4_K_M)", 26.0, 32, 4096, 8, 128, 32, "Q4_K_M", "Mixtral 8x7B Instruct, Q4_K_M 量化", 21),
("Gemma-2-9B-It (Q4_K_M)", 5.4, 42, 3584, 4, 256, 14, "Q4_K_M", "Google Gemma 2 9B It, Q4_K_M 量化", 22),
("Gemma-2-27B-It (Q4_K_M)", 16.5, 46, 4608, 4, 128, 36, "Q4_K_M", "Google Gemma 2 27B It, Q4_K_M 量化", 23),
("Phi-3-Mini-4K-Instruct (Q4_K_M)", 2.5, 32, 3072, 32, 96, 32, "Q4_K_M", "Microsoft Phi-3 Mini 4K Instruct, Q4_K_M 量化", 24),
("Phi-3-Medium-14B-Instruct (Q4_K_M)", 8.4, 40, 5120, 10, 128, 40, "Q4_K_M", "Microsoft Phi-3 Medium 14B Instruct, Q4_K_M 量化", 25),
("GLM-4-9B-Chat (Q4_K_M)", 5.5, 40, 4096, 4, 128, 40, "Q4_K_M", "Zhipu GLM-4 9B Chat, Q4_K_M 量化", 26),
# Llama-3-8B-Instruct (ctx 8192)
("Llama-3-8B-Instruct", "Llama-3-8B-Instruct (Q4_K_M)", 4.9, 32, 4096, 8, 128, 32, 8192, "Q4_K_M", "Meta Llama 3 8B Instruct, Q4_K_M 量化", 1),
("Llama-3-8B-Instruct", "Llama-3-8B-Instruct (Q8_0)", 8.5, 32, 4096, 8, 128, 32, 8192, "Q8_0", "Meta Llama 3 8B Instruct, Q8_0 量化", 2),
("Llama-3-8B-Instruct", "Llama-3-8B-Instruct (FP16)", 15.5, 32, 4096, 8, 128, 32, 8192, "FP16", "Meta Llama 3 8B Instruct, FP16", 3),
# Llama-3-70B-Instruct (ctx 8192)
("Llama-3-70B-Instruct", "Llama-3-70B-Instruct (Q4_K_M)", 38.5, 80, 8192, 8, 128, 64, 8192, "Q4_K_M", "Meta Llama 3 70B Instruct, Q4_K_M 量化", 4),
("Llama-3-70B-Instruct", "Llama-3-70B-Instruct (Q8_0)", 74.0, 80, 8192, 8, 128, 64, 8192, "Q8_0", "Meta Llama 3 70B Instruct, Q8_0 量化", 5),
("Llama-3-70B-Instruct", "Llama-3-70B-Instruct (FP16)", 138.0, 80, 8192, 8, 128, 64, 8192, "FP16", "Meta Llama 3 70B Instruct, FP16", 6),
# Llama-3.1-8B-Instruct (ctx 131072)
("Llama-3.1-8B-Instruct", "Llama-3.1-8B-Instruct (Q4_K_M)", 4.9, 32, 4096, 8, 128, 32, 131072, "Q4_K_M", "Meta Llama 3.1 8B Instruct, Q4_K_M 量化", 7),
("Llama-3.1-8B-Instruct", "Llama-3.1-8B-Instruct (Q8_0)", 8.5, 32, 4096, 8, 128, 32, 131072, "Q8_0", "Meta Llama 3.1 8B Instruct, Q8_0 量化", 8),
# Llama-3.1-70B-Instruct (ctx 131072)
("Llama-3.1-70B-Instruct", "Llama-3.1-70B-Instruct (Q4_K_M)", 38.5, 80, 8192, 8, 128, 64, 131072, "Q4_K_M", "Meta Llama 3.1 70B Instruct, Q4_K_M 量化", 9),
("Llama-3.1-70B-Instruct", "Llama-3.1-70B-Instruct (Q8_0)", 74.0, 80, 8192, 8, 128, 64, 131072, "Q8_0", "Meta Llama 3.1 70B Instruct, Q8_0 量化", 10),
# Llama-3.1-405B-Instruct (ctx 131072)
("Llama-3.1-405B-Instruct", "Llama-3.1-405B-Instruct (Q4_K_M)", 226.0, 126, 16384, 8, 128, 128, 131072, "Q4_K_M", "Meta Llama 3.1 405B Instruct, Q4_K_M 量化", 11),
# Qwen2.5-7B-Instruct (ctx 32768)
("Qwen2.5-7B-Instruct", "Qwen2.5-7B-Instruct (Q4_K_M)", 4.7, 28, 3584, 4, 128, 28, 32768, "Q4_K_M", "Qwen2.5 7B Instruct, Q4_K_M 量化", 12),
("Qwen2.5-7B-Instruct", "Qwen2.5-7B-Instruct (Q8_0)", 7.6, 28, 3584, 4, 128, 28, 32768, "Q8_0", "Qwen2.5 7B Instruct, Q8_0 量化", 13),
# Qwen2.5-14B-Instruct (ctx 32768)
("Qwen2.5-14B-Instruct", "Qwen2.5-14B-Instruct (Q4_K_M)", 8.7, 40, 5120, 8, 128, 40, 32768, "Q4_K_M", "Qwen2.5 14B Instruct, Q4_K_M 量化", 14),
# Qwen2.5-32B-Instruct (ctx 32768)
("Qwen2.5-32B-Instruct", "Qwen2.5-32B-Instruct (Q4_K_M)", 19.5, 64, 5120, 8, 128, 64, 32768, "Q4_K_M", "Qwen2.5 32B Instruct, Q4_K_M 量化", 15),
("Qwen2.5-32B-Instruct", "Qwen2.5-32B-Instruct (Q8_0)", 32.0, 64, 5120, 8, 128, 64, 32768, "Q8_0", "Qwen2.5 32B Instruct, Q8_0 量化", 16),
# Qwen2.5-72B-Instruct (ctx 32768)
("Qwen2.5-72B-Instruct", "Qwen2.5-72B-Instruct (Q4_K_M)", 42.0, 80, 8192, 8, 128, 64, 32768, "Q4_K_M", "Qwen2.5 72B Instruct, Q4_K_M 量化", 17),
("Qwen2.5-72B-Instruct", "Qwen2.5-72B-Instruct (Q8_0)", 75.0, 80, 8192, 8, 128, 64, 32768, "Q8_0", "Qwen2.5 72B Instruct, Q8_0 量化", 18),
# DeepSeek-V2-Chat (ctx 4096)
("DeepSeek-V2-Chat", "DeepSeek-V2-Chat (Q4_K_M)", 23.0, 60, 5120, 8, 128, 60, 4096, "Q4_K_M", "DeepSeek V2 Chat, Q4_K_M 量化", 19),
# DeepSeek-V2.5-Chat (ctx 4096)
("DeepSeek-V2.5-Chat", "DeepSeek-V2.5-Chat (Q4_K_M)", 23.0, 60, 5120, 8, 128, 60, 4096, "Q4_K_M", "DeepSeek V2.5 Chat, Q4_K_M 量化", 20),
# DeepSeek-R1-Distill-Qwen-32B (ctx 131072)
("DeepSeek-R1-Distill-Qwen-32B", "DeepSeek-R1-Distill-Qwen-32B (Q4_K_M)", 19.5, 64, 5120, 8, 128, 64, 131072, "Q4_K_M", "DeepSeek R1 Distill Qwen 32B, Q4_K_M 量化", 21),
("DeepSeek-R1-Distill-Qwen-32B", "DeepSeek-R1-Distill-Qwen-32B (Q8_0)", 32.0, 64, 5120, 8, 128, 64, 131072, "Q8_0", "DeepSeek R1 Distill Qwen 32B, Q8_0 量化", 22),
# DeepSeek-R1-Distill-Llama-70B (ctx 131072)
("DeepSeek-R1-Distill-Llama-70B", "DeepSeek-R1-Distill-Llama-70B (Q4_K_M)", 42.0, 80, 8192, 8, 128, 64, 131072, "Q4_K_M", "DeepSeek R1 Distill Llama 70B, Q4_K_M 量化", 23),
("DeepSeek-R1-Distill-Llama-70B", "DeepSeek-R1-Distill-Llama-70B (Q8_0)", 75.0, 80, 8192, 8, 128, 64, 131072, "Q8_0", "DeepSeek R1 Distill Llama 70B, Q8_0 量化", 24),
# Mistral-7B-Instruct-v0.3 (ctx 32768)
("Mistral-7B-Instruct-v0.3", "Mistral-7B-Instruct-v0.3 (Q4_K_M)", 4.4, 32, 4096, 8, 128, 32, 32768, "Q4_K_M", "Mistral 7B Instruct v0.3, Q4_K_M 量化", 25),
("Mistral-7B-Instruct-v0.3", "Mistral-7B-Instruct-v0.3 (Q8_0)", 7.5, 32, 4096, 8, 128, 32, 32768, "Q8_0", "Mistral 7B Instruct v0.3, Q8_0 量化", 26),
# Mixtral-8x7B-Instruct (ctx 32768)
("Mixtral-8x7B-Instruct", "Mixtral-8x7B-Instruct (Q4_K_M)", 26.0, 32, 4096, 8, 128, 32, 32768, "Q4_K_M", "Mixtral 8x7B Instruct, Q4_K_M 量化", 27),
# Gemma-2-9B-It (ctx 8192)
("Gemma-2-9B-It", "Gemma-2-9B-It (Q4_K_M)", 5.4, 42, 3584, 4, 256, 14, 8192, "Q4_K_M", "Google Gemma 2 9B It, Q4_K_M 量化", 28),
# Gemma-2-27B-It (ctx 8192)
("Gemma-2-27B-It", "Gemma-2-27B-It (Q4_K_M)", 16.5, 46, 4608, 4, 128, 36, 8192, "Q4_K_M", "Google Gemma 2 27B It, Q4_K_M 量化", 29),
# Phi-3-Mini-4K-Instruct (ctx 4096)
("Phi-3-Mini-4K-Instruct", "Phi-3-Mini-4K-Instruct (Q4_K_M)", 2.5, 32, 3072, 32, 96, 32, 4096, "Q4_K_M", "Microsoft Phi-3 Mini 4K Instruct, Q4_K_M 量化", 30),
# Phi-3-Medium-14B-Instruct (ctx 14336)
("Phi-3-Medium-14B-Instruct", "Phi-3-Medium-14B-Instruct (Q4_K_M)", 8.4, 40, 5120, 10, 128, 40, 14336, "Q4_K_M", "Microsoft Phi-3 Medium 14B Instruct, Q4_K_M 量化", 31),
# GLM-4-9B-Chat (ctx 131072)
("GLM-4-9B-Chat", "GLM-4-9B-Chat (Q4_K_M)", 5.5, 40, 4096, 4, 128, 40, 131072, "Q4_K_M", "Zhipu GLM-4 9B Chat, Q4_K_M 量化", 32),
("GLM-4-9B-Chat", "GLM-4-9B-Chat (Q8_0)", 9.0, 40, 4096, 4, 128, 40, 131072, "Q8_0", "Zhipu GLM-4 9B Chat, Q8_0 量化", 33),
]
for m in default_models:
c.execute('''INSERT INTO models (name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, quant, description, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', m)
c.execute('''INSERT INTO models (base_model, name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, default_ctx, quant, description, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', m)
# ===== Default settings =====
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("admin_password", "admin123"))
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("default_gpu", "RTX 3090"))
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("default_version", "b10068"))
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("default_mode", "gpu"))
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("default_quant", "Q4_K_M"))
# LLM API settings for natural language parsing
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("llm_enabled", "false"))
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("llm_api_url", ""))
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("llm_api_key", ""))
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("llm_api_model", ""))
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("nl_default_text", "用自然语言描述你想要的配置,支持多行输入。\n例如:\n用RTX 4090跑Llama-3-70B,上下文8192\n温度0.7,开启flash attention\n端口设为8080"))
conn.commit()
+4 -2
View File
@@ -123,13 +123,15 @@
<div class="admin-add-form">
<h3>添加新模型</h3>
<div class="form-grid">
<input type="text" id="model-name" placeholder="模型名称">
<input type="text" id="model-basemodel" placeholder="模型 (如: Llama-3-8B-Instruct)">
<input type="text" id="model-name" placeholder="完整名称 (含量化)">
<input type="number" id="model-size" placeholder="大小(GB)" step="0.1">
<input type="number" id="model-layers" placeholder="层数">
<input type="number" id="model-embd" placeholder="嵌入维度">
<input type="number" id="model-kv" placeholder="KV Heads">
<input type="number" id="model-hdim" placeholder="Head Dim">
<input type="number" id="model-heads" placeholder="Attention Heads">
<input type="number" id="model-ctx" placeholder="默认上下文" value="0">
<input type="text" id="model-quant" placeholder="量化 (如: Q4_K_M)">
<input type="text" id="model-desc" placeholder="描述">
<input type="number" id="model-order" placeholder="排序" value="0">
@@ -137,7 +139,7 @@
</div>
</div>
<table class="admin-table">
<thead><tr><th>ID</th><th>名称</th><th>大小(GB)</th><th>层数</th><th>EMBD</th><th>KV</th><th>HD</th><th>Heads</th><th>量化</th><th>排序</th><th>操作</th></tr></thead>
<thead><tr><th>ID</th><th>基模型</th><th>名称</th><th>大小</th><th>层数</th><th>EMBD</th><th>KV</th><th>HD</th><th>Heads</th><th>默认CTX</th><th>量化</th><th>排序</th><th>操作</th></tr></thead>
<tbody id="model-table-body"></tbody>
</table>
</section>
+152 -16
View File
@@ -14,6 +14,102 @@
--radius: 8px;
}
/* ===== Sticky Memory Bar ===== */
.sticky-mem-bar {
position: sticky;
top: 0;
z-index: 200;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 6px 12px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
.sticky-vram-row, .sticky-ram-row {
display: flex;
align-items: center;
gap: 8px;
}
.sticky-ram-row { margin-top: 4px; }
.sticky-ram-row.hidden { display: none; }
.sticky-label {
font-size: 0.8em;
font-weight: 600;
color: var(--text-dim);
min-width: 60px;
}
.sticky-bar-container {
position: relative;
flex: 1;
height: 22px;
background: var(--bg);
border-radius: 4px;
overflow: hidden;
border: 1px solid var(--border);
}
.sticky-bar {
height: 100%;
background: linear-gradient(90deg, #4ecca3, #e9c46a);
border-radius: 3px;
transition: width 0.3s;
width: 0%;
}
.sticky-bar.warning { background: linear-gradient(90deg, #e9c46a, #f4a261); }
.sticky-bar.danger { background: linear-gradient(90deg, #f4a261, #e94560); }
.ram-sticky-bar { background: linear-gradient(90deg, #4e9cca, #4ecca3); }
.sticky-bar-text {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-size: 0.75em;
font-weight: 600;
color: white;
text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
white-space: nowrap;
}
/* ===== Inline Memory Config ===== */
.inline-memory-config { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--border); }
.inline-memory-config.hidden { display: none; }
.memory-input-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.memory-input-row label { font-size: 0.85em; color: var(--text-dim); }
.memory-input-row input {
background: var(--bg-input); border: 1px solid var(--border); color: var(--text);
padding: 6px 10px; border-radius: 4px; width: 100px;
}
.memory-unit { font-size: 0.85em; color: var(--text-dim); }
/* ===== Parameter Items (show long flag) ===== */
.param-item .param-flag-long {
font-family: monospace;
font-size: 0.8em;
color: var(--text-dim);
background: var(--bg);
padding: 2px 6px;
border-radius: 3px;
min-width: 80px;
text-align: center;
flex-shrink: 0;
}
/* ===== Command Hints ===== */
.command-hint { margin-top: 8px; font-size: 0.85em; }
.hint-error { color: var(--accent); padding: 6px 10px; background: rgba(233, 69, 96, 0.1); border-radius: 4px; margin-bottom: 4px; border-left: 3px solid var(--accent); }
.hint-warning { color: #e9c46a; padding: 6px 10px; background: rgba(233, 196, 106, 0.1); border-radius: 4px; margin-bottom: 4px; border-left: 3px solid #e9c46a; }
.hint-info { color: var(--text-dim); padding: 6px 10px; background: rgba(136, 146, 176, 0.1); border-radius: 4px; margin-bottom: 4px; border-left: 3px solid var(--text-dim); }
.hint-ok { color: var(--accent2); padding: 6px 10px; background: rgba(78, 204, 163, 0.1); border-radius: 4px; margin-bottom: 4px; border-left: 3px solid var(--accent2); }
/* ===== Natural Language Textarea ===== */
.nl-input-container { display: flex; gap: 8px; align-items: flex-end; }
.nl-input-container textarea {
flex: 1; background: var(--bg-input); border: 1px solid var(--border); color: var(--text);
padding: 10px 14px; border-radius: var(--radius); font-size: 0.95em; resize: vertical; font-family: inherit;
line-height: 1.5;
}
.nl-input-container textarea:focus { outline: none; border-color: var(--accent); }
.nl-input-container button { padding: 10px 24px; background: var(--accent2); color: var(--bg); border: none; border-radius: var(--radius); cursor: pointer; font-weight: 600; flex-shrink: 0; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
@@ -113,6 +209,8 @@ header h1 {
.top-controls { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 20px; }
.control-group { display: flex; flex-direction: column; gap: 6px; flex: 1; min-width: 200px; }
.control-group.control-group-small { flex: 0.5; min-width: 140px; }
.control-group.control-group-large { flex: 2; min-width: 300px; }
.control-group label { font-size: 0.85em; color: var(--text-dim); font-weight: 500; }
@@ -152,7 +250,7 @@ header h1 {
box-shadow: var(--shadow);
}
.panel h2 { font-size: 1.2em; margin-bottom: 16px; color: var(--accent2); border-bottom: 1px solid var(--border); padding-bottom: 8px; }
.panel h2 { font-size: 1.2em; margin-top: 0; margin-bottom: 16px; color: var(--accent2); border-bottom: 1px solid var(--border); padding-bottom: 8px; }
.hidden { display: none !important; }
@@ -166,8 +264,9 @@ header h1 {
.btn-add { background: transparent; border: 1px dashed var(--border); color: var(--text-dim); padding: 8px 16px; border-radius: var(--radius); cursor: pointer; width: 100%; margin-top: 8px; }
.btn-add:hover { border-color: var(--accent2); color: var(--accent2); }
/* ===== VRAM Display ===== */
.vram-display { margin-top: 16px; padding: 12px; background: var(--bg-input); border-radius: var(--radius); }
/* ===== VRAM Display (hidden when sticky bar visible) ===== */
.vram-display { margin-top: 16px; padding: 12px; background: var(--bg-input); border-radius: var(--radius); display: none; }
.vram-display.visible { display: block; }
.vram-bar-container { position: relative; height: 30px; background: var(--bg); border-radius: 4px; overflow: hidden; border: 1px solid var(--border); }
.vram-bar { height: 100%; background: linear-gradient(90deg, #4ecca3, #e9c46a); border-radius: 3px; transition: width 0.3s; width: 0%; }
.vram-bar.warning { background: linear-gradient(90deg, #e9c46a, #f4a261); }
@@ -177,7 +276,10 @@ header h1 {
.vram-breakdown { margin-top: 10px; font-size: 0.85em; color: var(--text-dim); display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 6px; }
.vram-breakdown .breakdown-item { display: flex; justify-content: space-between; padding: 4px 8px; background: var(--bg); border-radius: 4px; }
/* ===== Model Selection ===== */
/* ===== Model Selection (two-step) ===== */
.model-step { margin-bottom: 12px; }
.model-step.hidden { display: none; }
.model-step-label { display: block; font-size: 0.85em; color: var(--text-dim); margin-bottom: 6px; }
.model-select-container { position: relative; }
.model-select-container input {
width: 100%;
@@ -215,15 +317,22 @@ header h1 {
.model-name { font-size: 0.9em; }
.model-meta { font-size: 0.8em; color: var(--text-dim); }
.quant-btn {
display: inline-flex; flex-direction: column; align-items: center; gap: 2px;
padding: 8px 16px; background: var(--bg-input); border: 1px solid var(--border);
color: var(--text); border-radius: var(--radius); cursor: pointer; font-size: 0.85em; margin-right: 8px; margin-bottom: 8px; transition: all 0.15s;
}
.quant-btn:hover { border-color: var(--accent2); }
.quant-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
.quant-size { font-size: 0.75em; color: var(--text-dim); }
.quant-btn.active .quant-size { color: rgba(255,255,255,0.8); }
.model-selected-info { margin-top: 12px; }
.model-detail { display: flex; gap: 16px; flex-wrap: wrap; font-size: 0.85em; }
.model-detail { display: flex; gap: 12px; flex-wrap: wrap; font-size: 0.85em; }
.detail-item { background: var(--bg-input); padding: 4px 10px; border-radius: 4px; }
.detail-item b { color: var(--accent2); }
/* ===== Natural Language ===== */
.nl-input-container { display: flex; gap: 8px; }
.nl-input-container input { flex: 1; background: var(--bg-input); border: 1px solid var(--border); color: var(--text); padding: 10px 14px; border-radius: var(--radius); }
.nl-input-container button { padding: 10px 24px; background: var(--accent2); color: var(--bg); border: none; border-radius: var(--radius); cursor: pointer; font-weight: 600; }
.nl-result { margin-top: 10px; font-size: 0.9em; color: var(--text-dim); }
.parsed-param { display: inline-block; margin: 2px 4px; padding: 2px 8px; background: var(--bg-input); border-radius: 4px; border: 1px solid var(--border); }
.parsed-param .key { color: var(--accent2); }
@@ -233,9 +342,11 @@ header h1 {
.param-tabs { display: flex; gap: 4px; margin-bottom: 12px; flex-wrap: wrap; }
.tab-btn { padding: 6px 14px; background: var(--bg-input); border: 1px solid var(--border); color: var(--text-dim); border-radius: var(--radius); cursor: pointer; font-size: 0.85em; }
.tab-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
.tab-btn.tab-modified { border-color: var(--accent2); color: var(--accent2); margin-left: auto; }
.tab-btn.tab-modified.active { background: var(--accent2); color: var(--bg); border-color: var(--accent2); }
/* ===== Param Search ===== */
.param-search-container { margin-bottom: 12px; }
.param-search-container { margin-bottom: 12px; position: relative; }
.param-search-container input {
width: 100%;
background: var(--bg-input);
@@ -246,6 +357,14 @@ header h1 {
font-size: 0.9em;
}
.param-search-container input:focus { outline: none; border-color: var(--accent); }
.search-hint { font-size: 0.8em; color: var(--text-dim); margin-left: 8px; }
/* ===== Settings (wide textarea) ===== */
.settings-form .setting-item-wide { grid-column: 1 / -1; }
.settings-form textarea {
background: var(--bg-input); border: 1px solid var(--border); color: var(--text);
padding: 8px 12px; border-radius: var(--radius); font-size: 0.85em; resize: vertical; width: 100%; font-family: inherit;
}
/* ===== Parameter Items (one per line) ===== */
#param-container { display: flex; flex-direction: column; gap: 6px; }
@@ -261,7 +380,11 @@ header h1 {
transition: border-color 0.15s;
}
.param-item:hover { border-color: var(--accent2); }
.param-item.modified { border-color: var(--accent); background: rgba(233, 69, 96, 0.05); }
.param-item.param-empty { border-color: var(--accent); border-width: 2px; }
.param-item.param-changed { border-color: #e9c46a; }
.param-item.modified { border-color: var(--accent2); }
/* keep old .modified for backwards compat but remove red styling */
.param-item .param-flag {
font-family: monospace;
@@ -320,12 +443,25 @@ header h1 {
/* ===== Command Output ===== */
.command-panel { position: sticky; bottom: 20px; }
.command-output { background: #0d1117; border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; font-family: 'Cascadia Code', 'Fira Code', monospace; font-size: 0.9em; color: #4ecca3; word-break: break-all; min-height: 60px; white-space: pre-wrap; }
.command-actions { margin-top: 12px; display: flex; gap: 8px; }
.btn-copy, .btn-gen { padding: 8px 20px; border: none; border-radius: var(--radius); cursor: pointer; font-size: 0.9em; font-weight: 500; }
.btn-copy { background: var(--accent2); color: var(--bg); }
.btn-copy:hover { opacity: 0.85; }
.btn-gen { background: var(--bg-input); color: var(--text); border: 1px solid var(--border); }
.command-output-wrap { position: relative; }
.command-output { position: relative; background: #0d1117; border: 1px solid var(--border); border-radius: var(--radius); padding: 16px 16px 16px 70px; font-family: 'Cascadia Code', 'Fira Code', monospace; font-size: 0.9em; color: #4ecca3; word-break: break-all; min-height: 60px; white-space: pre-wrap; }
.command-copy-btn {
position: absolute;
top: 6px;
left: 6px;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-dim);
padding: 3px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 0.75em;
transition: all 0.15s;
opacity: 0.7;
z-index: 5;
}
.command-copy-btn:hover { opacity: 1; border-color: var(--accent2); color: var(--accent2); }
.command-copy-btn.copied { background: var(--accent2); color: var(--bg); border-color: var(--accent2); opacity: 1; }
/* ===== Admin ===== */
.admin-tabs { display: flex; gap: 4px; margin-bottom: 20px; flex-wrap: wrap; }
+56 -35
View File
@@ -12,20 +12,38 @@
<h1>🦙 llama.cpp 命令生成器</h1>
</header>
<!-- Sticky memory bar (always visible on top) -->
<div id="sticky-mem-bar" class="sticky-mem-bar">
<div class="sticky-vram-row" id="sticky-vram-row">
<span class="sticky-label">🎮 VRAM</span>
<div class="sticky-bar-container">
<div class="sticky-bar" id="sticky-vram-bar"></div>
<span class="sticky-bar-text" id="sticky-vram-text">等待配置...</span>
</div>
</div>
<div class="sticky-ram-row hidden" id="sticky-ram-row">
<span class="sticky-label">💾 内存</span>
<div class="sticky-bar-container">
<div class="sticky-bar ram-sticky-bar" id="sticky-ram-bar"></div>
<span class="sticky-bar-text" id="sticky-ram-text">等待配置...</span>
</div>
</div>
</div>
<!-- Version & Mode Selection -->
<section class="top-controls">
<div class="control-group">
<label>llama.cpp 版本</label>
<select id="version-select" onchange="onVersionChange()"></select>
</div>
<div class="control-group">
<div class="control-group control-group-large">
<label>运行模式</label>
<div class="mode-switch">
<button class="mode-btn active" data-mode="gpu" onclick="switchMode('gpu')">🎮 GPU 模式</button>
<button class="mode-btn" data-mode="gpu_cpu" onclick="switchMode('gpu_cpu')">🎮+💻 GPU+CPU 模式</button>
</div>
</div>
<div class="control-group">
<div class="control-group control-group-small">
<label>二进制程序</label>
<select id="binary-select">
<option value="llama-server">llama-server</option>
@@ -34,7 +52,7 @@
</div>
</section>
<!-- GPU Configuration -->
<!-- GPU Configuration + System Memory (merged) -->
<section class="panel" id="gpu-panel">
<h2>🖥️ GPU 配置</h2>
<div id="gpu-slots"></div>
@@ -48,35 +66,37 @@
</div>
<div class="vram-breakdown" id="vram-breakdown"></div>
</div>
</section>
<!-- System Memory (for GPU+CPU mode) -->
<section class="panel hidden" id="memory-panel">
<h2>💾 系统内存配置</h2>
<div class="control-group">
<label>系统内存上限</label>
<input type="number" id="sys-memory" value="0" min="0" step="1" onchange="updateEstimate()">
<select id="sys-memory-unit" onchange="updateEstimate()">
<option value="0">无限制</option>
<option value="1">GB</option>
<option value="2">MB</option>
</select>
</div>
<div class="vram-display" id="ram-display">
<div class="vram-bar-container">
<div class="vram-bar ram-bar" id="ram-bar"></div>
<div class="vram-label" id="ram-label">内存: 等待配置...</div>
<!-- System Memory (inline, only for GPU+CPU mode) -->
<div class="inline-memory-config hidden" id="inline-memory-config">
<div class="memory-input-row">
<label>系统内存上限</label>
<input type="number" id="sys-memory" value="0" min="0" step="1" onchange="updateEstimate()" placeholder="0 = 无限制">
<span class="memory-unit">GB</span>
</div>
<div class="vram-display" id="ram-display">
<div class="vram-bar-container">
<div class="vram-bar ram-bar" id="ram-bar"></div>
<div class="vram-label" id="ram-label">内存: 等待配置...</div>
</div>
<div class="vram-breakdown" id="ram-breakdown"></div>
</div>
<div class="vram-breakdown" id="ram-breakdown"></div>
</div>
</section>
<!-- Model Selection -->
<!-- Model Selection (two-step) -->
<section class="panel">
<h2>📦 选择模型</h2>
<div class="model-select-container">
<input type="text" id="model-search" placeholder="搜索模型名称..." oninput="filterModels()" autocomplete="off">
<div class="model-dropdown" id="model-dropdown"></div>
<div class="model-step">
<label class="model-step-label">第1步:选择模型</label>
<div class="model-select-container">
<input type="text" id="model-search" placeholder="搜索模型名称..." oninput="filterBaseModels()" onfocus="onModelSearchFocus()" onblur="onModelSearchBlur()" autocomplete="off">
<div class="model-dropdown" id="model-dropdown"></div>
</div>
</div>
<div class="model-step hidden" id="quant-step">
<label class="model-step-label">第2步:选择量化版本</label>
<div id="quant-options"></div>
</div>
<div id="model-selected-info" class="model-selected-info"></div>
</section>
@@ -85,7 +105,7 @@
<section class="panel">
<h2>💬 自然语言生成</h2>
<div class="nl-input-container">
<input type="text" id="nl-input" placeholder="例如: 用RTX 4090跑70B模型, 上下文8192, 温度0.7, flash attention" />
<textarea id="nl-input" rows="3" placeholder="加载中..."></textarea>
<button onclick="parseNaturalLanguage()">解析</button>
</div>
<div id="nl-result" class="nl-result"></div>
@@ -94,7 +114,11 @@
<!-- Parameters -->
<section class="panel">
<h2>⚙️ 参数配置</h2>
<div class="param-tabs">
<div class="param-search-container">
<input type="text" id="param-search" placeholder="搜索全部参数..." oninput="onParamSearch()" autocomplete="off">
<span id="search-hint" class="search-hint"></span>
</div>
<div class="param-tabs" id="param-tabs">
<button class="tab-btn active" data-cat="common" onclick="switchTab('common')">通用参数</button>
<button class="tab-btn" data-cat="sampling" onclick="switchTab('sampling')">采样参数</button>
<button class="tab-btn" data-cat="server" onclick="switchTab('server')">服务器参数</button>
@@ -102,9 +126,7 @@
<button class="tab-btn" data-cat="lora" onclick="switchTab('lora')">LoRA</button>
<button class="tab-btn" data-cat="logging" onclick="switchTab('logging')">日志</button>
<button class="tab-btn" data-cat="advanced" onclick="switchTab('advanced')">高级</button>
</div>
<div class="param-search-container">
<input type="text" id="param-search" placeholder="搜索参数名称或标志..." oninput="onParamSearch()" autocomplete="off">
<button class="tab-btn tab-modified" data-cat="modified" onclick="switchTab('modified')">已改参数</button>
</div>
<div id="param-container"></div>
<button class="btn-toggle-advanced" onclick="toggleHiddenParams()" id="toggle-advanced-btn">▼ 显示更多参数</button>
@@ -113,14 +135,13 @@
<!-- Generated Command -->
<section class="panel command-panel">
<h2>📋 生成命令</h2>
<div class="command-output" id="command-output">请在上方配置参数...</div>
<div class="command-actions">
<button onclick="copyCommand()" class="btn-copy">📋 复制命令</button>
<button onclick="generateCommand()" class="btn-gen">🔄 重新生成</button>
<div class="command-output-wrap">
<button class="command-copy-btn" onclick="copyCommand()" id="copy-btn">📋 复制</button>
<div class="command-output" id="command-output">请在上方配置参数...</div>
</div>
<div class="command-hint" id="command-hint"></div>
</section>
</div>
<script src="/js/main.js"></script>
</body>
</html>
+39 -18
View File
@@ -239,21 +239,24 @@ function renderModelTable() {
document.getElementById('model-table-body').innerHTML = adminState.models.map(m => `
<tr>
<td>${m.id}</td>
<td><input type="text" value="${m.name}" onchange="updateModel(${m.id}, 'name', this.value)"></td>
<td><input type="number" value="${m.size_gb}" step="0.1" onchange="updateModel(${m.id}, 'size_gb', this.value)" style="width:70px"></td>
<td><input type="number" value="${m.layers}" onchange="updateModel(${m.id}, 'layers', this.value)" style="width:60px"></td>
<td><input type="number" value="${m.embd}" onchange="updateModel(${m.id}, 'embd', this.value)" style="width:70px"></td>
<td><input type="number" value="${m.kv_heads}" onchange="updateModel(${m.id}, 'kv_heads', this.value)" style="width:60px"></td>
<td><input type="number" value="${m.head_dim}" onchange="updateModel(${m.id}, 'head_dim', this.value)" style="width:60px"></td>
<td><input type="number" value="${m.attention_heads}" onchange="updateModel(${m.id}, 'attention_heads', this.value)" style="width:60px"></td>
<td><input type="text" value="${m.quant || ''}" onchange="updateModel(${m.id}, 'quant', this.value)" style="width:80px"></td>
<td><input type="number" value="${m.sort_order}" onchange="updateModel(${m.id}, 'sort_order', this.value)" style="width:50px"></td>
<td><input type="text" value="${m.base_model}" onchange="updateModel(${m.id}, 'base_model', this.value)" style="width:140px"></td>
<td><input type="text" value="${m.name}" onchange="updateModel(${m.id}, 'name', this.value)" style="width:200px"></td>
<td><input type="number" value="${m.size_gb}" step="0.1" onchange="updateModel(${m.id}, 'size_gb', this.value)" style="width:60px"></td>
<td><input type="number" value="${m.layers}" onchange="updateModel(${m.id}, 'layers', this.value)" style="width:50px"></td>
<td><input type="number" value="${m.embd}" onchange="updateModel(${m.id}, 'embd', this.value)" style="width:60px"></td>
<td><input type="number" value="${m.kv_heads}" onchange="updateModel(${m.id}, 'kv_heads', this.value)" style="width:50px"></td>
<td><input type="number" value="${m.head_dim}" onchange="updateModel(${m.id}, 'head_dim', this.value)" style="width:50px"></td>
<td><input type="number" value="${m.attention_heads}" onchange="updateModel(${m.id}, 'attention_heads', this.value)" style="width:50px"></td>
<td><input type="number" value="${m.default_ctx || 0}" onchange="updateModel(${m.id}, 'default_ctx', this.value)" style="width:70px"></td>
<td><input type="text" value="${m.quant || ''}" onchange="updateModel(${m.id}, 'quant', this.value)" style="width:70px"></td>
<td><input type="number" value="${m.sort_order}" onchange="updateModel(${m.id}, 'sort_order', this.value)" style="width:40px"></td>
<td><button class="btn-action btn-delete" onclick="deleteModel(${m.id})">删除</button></td>
</tr>`).join('');
}
async function addModel() {
const data = {
base_model: document.getElementById('model-basemodel').value,
name: document.getElementById('model-name').value,
size_gb: parseFloat(document.getElementById('model-size').value) || 0,
layers: parseInt(document.getElementById('model-layers').value) || 0,
@@ -261,22 +264,23 @@ async function addModel() {
kv_heads: parseInt(document.getElementById('model-kv').value) || 0,
head_dim: parseInt(document.getElementById('model-hdim').value) || 0,
attention_heads: parseInt(document.getElementById('model-heads').value) || 0,
default_ctx: parseInt(document.getElementById('model-ctx').value) || 0,
quant: document.getElementById('model-quant').value,
description: document.getElementById('model-desc').value,
sort_order: parseInt(document.getElementById('model-order').value) || 0,
};
if (!data.name || !data.size_gb || !data.layers) { alert('请填写模型名称、大小和层数'); return; }
if (!data.base_model || !data.name || !data.size_gb || !data.layers) { alert('请填写模型名称、大小和层数'); return; }
await fetch('/api/admin/models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
['model-name','model-size','model-layers','model-embd','model-kv','model-hdim','model-heads','model-quant','model-desc','model-order'].forEach(id => document.getElementById(id).value = id === 'model-order' ? '0' : '');
['model-basemodel','model-name','model-size','model-layers','model-embd','model-kv','model-hdim','model-heads','model-ctx','model-quant','model-desc','model-order'].forEach(id => document.getElementById(id).value = id === 'model-order' ? '0' : '');
await loadAdminModels();
}
async function updateModel(id, field, value) {
const m = adminState.models.find(m => m.id === id);
if (!m) return;
const data = { name: m.name, size_gb: m.size_gb, layers: m.layers, embd: m.embd, kv_heads: m.kv_heads, head_dim: m.head_dim, attention_heads: m.attention_heads, quant: m.quant, description: m.description, sort_order: m.sort_order };
const data = { base_model: m.base_model, name: m.name, size_gb: m.size_gb, layers: m.layers, embd: m.embd, kv_heads: m.kv_heads, head_dim: m.head_dim, attention_heads: m.attention_heads, default_ctx: m.default_ctx || 0, quant: m.quant, description: m.description, sort_order: m.sort_order };
if (['size_gb'].includes(field)) data[field] = parseFloat(value) || 0;
else if (['layers','embd','kv_heads','head_dim','attention_heads','sort_order'].includes(field)) data[field] = parseInt(value) || 0;
else if (['layers','embd','kv_heads','head_dim','attention_heads','default_ctx','sort_order'].includes(field)) data[field] = parseInt(value) || 0;
else data[field] = value;
await fetch(`/api/admin/models/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
await loadAdminModels();
@@ -297,11 +301,28 @@ async function loadAdminSettings() {
}
function renderSettings() {
document.getElementById('settings-form').innerHTML = Object.entries(adminState.settings).map(([key, value]) => `
<div class="setting-item">
<label>${key}</label>
<input type="text" value="${value}" onchange="adminState.settings['${key}'] = this.value">
</div>`).join('');
const labels = {
'admin_password': '管理密码',
'default_gpu': '默认GPU',
'default_version': '默认版本',
'default_mode': '默认模式',
'default_quant': '默认量化版本',
'nl_default_text': '自然语言默认提示文本',
'llm_enabled': '启用LLM解析 (true/false)',
'llm_api_url': 'LLM API URL',
'llm_api_key': 'LLM API Key',
'llm_api_model': 'LLM 模型名',
'llm_system_prompt': 'LLM 系统提示词',
};
document.getElementById('settings-form').innerHTML = Object.entries(adminState.settings).map(([key, value]) => {
const label = labels[key] || key;
const isLong = key === 'llm_system_prompt';
if (isLong) {
return `<div class="setting-item setting-item-wide"><label>${label}</label><textarea rows="4" onchange="adminState.settings['${key}'] = this.value">${value}</textarea></div>`;
}
const type = key === 'llm_api_key' ? 'password' : 'text';
return `<div class="setting-item"><label>${label}</label><input type="${type}" value="${value}" onchange="adminState.settings['${key}'] = this.value"></div>`;
}).join('');
}
async function saveSettings() {
+300 -315
View File
@@ -1,59 +1,52 @@
// ===== State =====
let state = {
versions: [],
gpus: [],
models: [],
filteredModels: [],
selectedModel: null,
currentVersionId: null,
params: [],
paramValues: {},
mode: 'gpu',
gpuSlots: [],
showHidden: false,
currentTab: 'common',
paramSearchText: '',
versions: [], gpus: [], models: [], modelsGrouped: {},
baseModelList: [], filteredBaseModels: [],
currentVersionId: null, params: [], paramValues: {},
mode: 'gpu', gpuSlots: [], showHidden: false,
currentTab: 'common', paramSearchText: '',
selectedModel: null, defaultQuant: 'Q4_K_M',
lastEstimate: null,
};
// ===== Init =====
async function init() {
await loadVersions();
await loadGpus();
await loadModels();
const defaultVer = state.versions.find(v => v.version_tag === 'b10068') || state.versions[0];
if (defaultVer) {
state.currentVersionId = defaultVer.id;
document.getElementById('version-select').value = defaultVer.id;
await loadParams(defaultVer.id);
await loadModelsGrouped();
await loadNlDefaultText();
const dv = state.versions.find(v => v.version_tag === 'b10068') || state.versions[0];
if (dv) { state.currentVersionId = dv.id; document.getElementById('version-select').value = dv.id; await loadParams(dv.id); }
const dg = state.gpus.find(g => g.name === 'RTX 3090') || state.gpus[0];
if (dg) { state.gpuSlots = [{ ...dg }]; renderGpuSlots(); }
renderParams(); generateCommand(); updateEstimate();
}
async function loadNlDefaultText() {
const res = await fetch('/api/settings/public');
const data = await res.json();
const ta = document.getElementById('nl-input');
if (data.nl_default_text) {
ta.placeholder = data.nl_default_text;
}
const defaultGpu = state.gpus.find(g => g.name === 'RTX 3090') || state.gpus[0];
if (defaultGpu) {
state.gpuSlots = [{ ...defaultGpu }];
renderGpuSlots();
}
renderParams();
generateCommand();
updateEstimate();
}
// ===== Load Data =====
async function loadVersions() {
const res = await fetch('/api/versions');
state.versions = await res.json();
const sel = document.getElementById('version-select');
sel.innerHTML = state.versions.map(v => `<option value="${v.id}">${v.version_tag} - ${v.description}</option>`).join('');
const res = await fetch('/api/versions'); state.versions = await res.json();
document.getElementById('version-select').innerHTML = state.versions.map(v => `<option value="${v.id}">${v.version_tag} - ${v.description}</option>`).join('');
}
async function loadGpus() { const res = await fetch('/api/gpus'); state.gpus = await res.json(); }
async function loadGpus() {
const res = await fetch('/api/gpus');
state.gpus = await res.json();
}
async function loadModels() {
const res = await fetch('/api/models');
state.models = await res.json();
state.filteredModels = state.models;
renderModelDropdown();
async function loadModelsGrouped() {
const res = await fetch('/api/models/grouped');
const data = await res.json();
state.modelsGrouped = data.models || {};
state.defaultQuant = data.default_quant || 'Q4_K_M';
state.baseModelList = Object.keys(state.modelsGrouped).sort();
state.filteredBaseModels = state.baseModelList;
state.models = Object.values(state.modelsGrouped).flat();
renderBaseModelDropdown();
}
async function loadParams(versionId) {
@@ -61,145 +54,170 @@ async function loadParams(versionId) {
state.params = await res.json();
state.paramValues = {};
state.params.forEach(p => {
if (p.param_type === 'boolean') {
state.paramValues[p.param_key] = p.default_value === 'true';
} else {
state.paramValues[p.param_key] = p.default_value || '';
}
state.paramValues[p.param_key] = p.param_type === 'boolean' ? (p.default_value === 'true') : (p.default_value || '');
});
renderParams();
generateCommand();
updateEstimate();
renderParams(); generateCommand(); updateEstimate();
}
// ===== Version Change =====
async function onVersionChange() {
const sel = document.getElementById('version-select');
state.currentVersionId = parseInt(sel.value);
await loadParams(state.currentVersionId);
}
async function onVersionChange() { state.currentVersionId = parseInt(document.getElementById('version-select').value); await loadParams(state.currentVersionId); }
// ===== Mode Switch =====
function switchMode(mode) {
state.mode = mode;
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
document.getElementById('memory-panel').classList.toggle('hidden', mode !== 'gpu_cpu');
generateCommand();
updateEstimate();
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.mode === mode));
const showRam = mode === 'gpu_cpu';
document.getElementById('inline-memory-config').classList.toggle('hidden', !showRam);
document.getElementById('sticky-ram-row').classList.toggle('hidden', !showRam);
generateCommand(); updateEstimate();
}
// ===== GPU Slots =====
function renderGpuSlots() {
const container = document.getElementById('gpu-slots');
container.innerHTML = state.gpuSlots.map((gpu, i) => {
const options = state.gpus.map(g =>
`<option value="${g.name}" ${g.name === gpu.name ? 'selected' : ''}>${g.name} (${(g.vram_mb / 1024).toFixed(0)}GB)</option>`
).join('');
return `
<div class="gpu-slot">
<span class="gpu-index">GPU ${i + 1}</span>
<select onchange="updateGpuSlot(${i}, this.value)">
<option value="">-- 选择GPU --</option>
${options}
</select>
<span class="vram-info">显存: ${(gpu.vram_mb / 1024).toFixed(1)} GB</span>
${state.gpuSlots.length > 1 ? `<button class="btn-remove" onclick="removeGpuSlot(${i})">×</button>` : ''}
</div>
`;
document.getElementById('gpu-slots').innerHTML = state.gpuSlots.map((gpu, i) => {
const opts = state.gpus.map(g => `<option value="${g.name}" ${g.name === gpu.name ? 'selected' : ''}>${g.name} (${(g.vram_mb/1024).toFixed(0)}GB)</option>`).join('');
return `<div class="gpu-slot"><span class="gpu-index">GPU ${i+1}</span><select onchange="updateGpuSlot(${i}, this.value)"><option value="">-- 选择GPU --</option>${opts}</select><span class="vram-info">显存: ${(gpu.vram_mb/1024).toFixed(1)} GB</span>${state.gpuSlots.length > 1 ? `<button class="btn-remove" onclick="removeGpuSlot(${i})">×</button>` : ''}</div>`;
}).join('');
document.getElementById('btn-add-gpu').style.display = state.gpuSlots.length >= 4 ? 'none' : 'block';
updateEstimate();
}
function addGpuSlot() {
if (state.gpuSlots.length >= 4) return;
const defaultGpu = state.gpus.find(g => g.name === 'RTX 3090') || state.gpus[0];
state.gpuSlots.push({ ...defaultGpu });
renderGpuSlots();
generateCommand();
}
function removeGpuSlot(index) {
state.gpuSlots.splice(index, 1);
renderGpuSlots();
generateCommand();
}
function updateGpuSlot(index, name) {
const gpu = state.gpus.find(g => g.name === name);
if (gpu) state.gpuSlots[index] = { ...gpu };
renderGpuSlots();
generateCommand();
}
function addGpuSlot() { if (state.gpuSlots.length >= 4) return; const dg = state.gpus.find(g => g.name === 'RTX 3090') || state.gpus[0]; state.gpuSlots.push({ ...dg }); renderGpuSlots(); if (state.selectedModel) applyModelDefaults(state.selectedModel); generateCommand(); }
function removeGpuSlot(i) { state.gpuSlots.splice(i, 1); renderGpuSlots(); if (state.gpuSlots.length > 1) { if (state.selectedModel) applyModelDefaults(state.selectedModel); } else { state.paramValues['split_mode'] = state.params.find(p=>p.param_key==='split_mode')?.default_value || 'layer'; state.paramValues['tensor_split'] = state.params.find(p=>p.param_key==='tensor_split')?.default_value || ''; } generateCommand(); }
function updateGpuSlot(i, name) { const g = state.gpus.find(g => g.name === name); if (g) state.gpuSlots[i] = { ...g }; renderGpuSlots(); if (state.gpuSlots.length > 1 && state.selectedModel) applyModelDefaults(state.selectedModel); generateCommand(); }
// ===== Model Selection =====
function filterModels() {
function filterBaseModels() {
const text = document.getElementById('model-search').value.toLowerCase();
state.filteredModels = state.models.filter(m =>
m.name.toLowerCase().includes(text) ||
(m.quant || '').toLowerCase().includes(text) ||
(m.description || '').toLowerCase().includes(text)
);
renderModelDropdown();
document.getElementById('model-dropdown').style.display = state.filteredModels.length > 0 ? 'block' : 'none';
if (!text) {
// Show top 5 when input is empty (on focus)
state.filteredBaseModels = state.baseModelList.slice(0, 5);
} else {
state.filteredBaseModels = state.baseModelList.filter(n => n.toLowerCase().includes(text));
}
renderBaseModelDropdown();
document.getElementById('model-dropdown').style.display = state.filteredBaseModels.length > 0 ? 'block' : 'none';
}
function renderModelDropdown() {
const dd = document.getElementById('model-dropdown');
dd.innerHTML = state.filteredModels.map(m => `
<div class="model-option" onclick="selectModel(${m.id})">
<span class="model-name">${m.name}</span>
<span class="model-meta">${m.size_gb}GB | ${m.layers}L | ${m.quant || ''}</span>
</div>
function onModelSearchFocus() {
if (!document.getElementById('model-search').value) {
state.filteredBaseModels = state.baseModelList.slice(0, 5);
renderBaseModelDropdown();
document.getElementById('model-dropdown').style.display = 'block';
}
}
function onModelSearchBlur() {
// Delay to allow click on option
setTimeout(() => { document.getElementById('model-dropdown').style.display = 'none'; }, 200);
}
function renderBaseModelDropdown() {
document.getElementById('model-dropdown').innerHTML = state.filteredBaseModels.map(n => {
const quants = state.modelsGrouped[n] || [];
const qs = quants.map(q => q.quant).filter(Boolean).join(', ');
return `<div class="model-option" onclick="selectBaseModel('${n}')"><span class="model-name">${n}</span><span class="model-meta">${qs}</span></div>`;
}).join('');
}
function selectBaseModel(baseName) {
document.getElementById('model-search').value = baseName;
document.getElementById('model-dropdown').style.display = 'none';
const variants = state.modelsGrouped[baseName] || [];
if (variants.length === 0) return;
document.getElementById('quant-step').classList.remove('hidden');
const dv = variants.find(v => v.quant === state.defaultQuant) || variants[0];
renderQuantOptions(baseName, variants, dv.id);
selectModel(dv.id);
}
function renderQuantOptions(baseName, variants, selectedId) {
document.getElementById('quant-options').innerHTML = variants.map(v => `
<button class="quant-btn ${v.id === selectedId ? 'active' : ''}" onclick="selectModel(${v.id})">${v.quant || 'FP16'}<span class="quant-size">${v.size_gb}GB</span></button>
`).join('');
}
function selectModel(id) {
const m = state.models.find(m => m.id === id);
if (!m) return;
state.selectedModel = m;
document.getElementById('model-search').value = m.name;
document.getElementById('model-dropdown').style.display = 'none';
renderQuantOptions(m.base_model, state.modelsGrouped[m.base_model] || [], id);
document.getElementById('model-selected-info').innerHTML = `
<div class="model-detail">
<span class="detail-item"><b>模型:</b> ${m.name}</span>
<span class="detail-item"><b>大小:</b> ${m.size_gb} GB</span>
<span class="detail-item"><b>层数:</b> ${m.layers}</span>
<span class="detail-item"><b>嵌入维度:</b> ${m.embd}</span>
<span class="detail-item"><b>嵌入:</b> ${m.embd}</span>
<span class="detail-item"><b>KV Heads:</b> ${m.kv_heads}</span>
<span class="detail-item"><b>Head Dim:</b> ${m.head_dim}</span>
<span class="detail-item"><b>Attention Heads:</b> ${m.attention_heads}</span>
<span class="detail-item"><b>量化:</b> ${m.quant || 'N/A'}</span>
</div>
`;
</div>`;
// Auto-adjust params based on selected model
applyModelDefaults(m);
updateEstimate();
}
function applyModelDefaults(m) {
// Set ctx_size to model's default context if available
if (m.default_ctx && m.default_ctx > 0) {
state.paramValues['ctx_size'] = String(m.default_ctx);
}
// Set n_gpu_layers to model's layer count (not 'all', use actual number)
if (m.layers && m.layers > 0) {
state.paramValues['n_gpu_layers'] = String(m.layers);
}
// Multi-GPU: set split_mode to tensor, compute tensor_split by VRAM ratio
if (state.gpuSlots.length > 1) {
state.paramValues['split_mode'] = 'tensor';
updateTensorSplit();
}
// Update param UI if currently visible
renderParams();
generateCommand();
}
function updateTensorSplit() {
if (state.gpuSlots.length <= 1) return;
const totalVram = state.gpuSlots.reduce((s, g) => s + (g.vram_mb || 0), 0);
if (totalVram > 0) {
const ratios = state.gpuSlots.map(g => (g.vram_mb / totalVram).toFixed(2));
state.paramValues['tensor_split'] = ratios.join(',');
}
}
// ===== Parameter Rendering =====
function switchTab(cat) {
state.currentTab = cat;
state.paramSearchText = '';
document.getElementById('param-search').value = '';
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.cat === cat);
});
document.getElementById('search-hint').textContent = '';
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.cat === cat));
document.getElementById('param-tabs').style.display = '';
renderParams();
}
function onParamSearch() {
state.paramSearchText = document.getElementById('param-search').value.toLowerCase();
const hint = document.getElementById('search-hint');
if (state.paramSearchText) {
const count = state.params.filter(p =>
p.param_key.toLowerCase().includes(state.paramSearchText) ||
(p.long_flag||'').toLowerCase().includes(state.paramSearchText) ||
(p.short_flag||'').toLowerCase().includes(state.paramSearchText) ||
(p.description||'').toLowerCase().includes(state.paramSearchText)
).length;
hint.textContent = `找到 ${count} 个匹配参数`;
document.getElementById('param-tabs').style.display = 'none';
} else {
hint.textContent = '';
document.getElementById('param-tabs').style.display = '';
}
renderParams();
}
function getFilteredParams() {
// 'modified' is a special tab showing all modified params
if (state.currentTab === 'modified') {
return state.params.filter(p => isParamModified(p, state.paramValues[p.param_key]));
}
let params = state.params.filter(p => p.category === state.currentTab);
if (state.paramSearchText) {
params = params.filter(p =>
params = state.params.filter(p =>
p.param_key.toLowerCase().includes(state.paramSearchText) ||
(p.long_flag || '').toLowerCase().includes(state.paramSearchText) ||
(p.short_flag || '').toLowerCase().includes(state.paramSearchText) ||
(p.description || '').toLowerCase().includes(state.paramSearchText)
(p.long_flag||'').toLowerCase().includes(state.paramSearchText) ||
(p.short_flag||'').toLowerCase().includes(state.paramSearchText) ||
(p.description||'').toLowerCase().includes(state.paramSearchText)
);
}
return params;
@@ -207,121 +225,103 @@ function getFilteredParams() {
function renderParams() {
const container = document.getElementById('param-container');
const allParams = getFilteredParams();
// When searching, show all matching regardless of important/hidden
if (state.paramSearchText) {
container.innerHTML = allParams.map(p => renderParamItem(p)).join('');
const all = getFilteredParams();
// For 'modified' tab, show all (no important/hidden distinction)
if (state.currentTab === 'modified' || state.paramSearchText) {
container.innerHTML = all.map(p => renderParamItem(p)).join('');
document.getElementById('toggle-advanced-btn').style.display = 'none';
return;
}
const importantParams = allParams.filter(p => p.is_important === 1);
const otherParams = allParams.filter(p => p.is_important === 0);
let html = importantParams.map(p => renderParamItem(p)).join('');
if (state.showHidden) {
html += otherParams.map(p => renderParamItem(p)).join('');
}
const imp = all.filter(p => p.is_important === 1);
const other = all.filter(p => p.is_important === 0);
let html = imp.map(p => renderParamItem(p)).join('');
if (state.showHidden) html += other.map(p => renderParamItem(p)).join('');
container.innerHTML = html;
const toggleBtn = document.getElementById('toggle-advanced-btn');
if (otherParams.length > 0 && !state.paramSearchText) {
toggleBtn.style.display = 'block';
toggleBtn.textContent = state.showHidden ? '▲ 收起更多参数' : '▼ 显示更多参数';
} else {
toggleBtn.style.display = 'none';
}
const btn = document.getElementById('toggle-advanced-btn');
if (other.length > 0 && !state.paramSearchText) { btn.style.display = 'block'; btn.textContent = state.showHidden ? '▲ 收起更多参数' : '▼ 显示更多参数'; }
else btn.style.display = 'none';
}
function renderParamItem(p) {
const val = state.paramValues[p.param_key];
const defaultVal = p.default_value;
const isModified = isParamModified(p, val);
const modifiedClass = isModified ? 'modified' : '';
const vramBadge = p.affects_vram ? '<span class="vram-badge">⚡显存</span>' : '';
const flag = p.short_flag || p.long_flag;
let inputHtml = '';
// Determine item class: empty-required (red), modified (orange), or normal
let itemClass = '';
if (p.param_key === 'model' && (!val || val.trim() === '')) {
itemClass = 'param-empty';
} else if (isParamModified(p, val)) {
itemClass = 'param-changed';
}
const vb = p.affects_vram ? '<span class="vram-badge">⚡显存</span>' : '';
// Show both short and long flag
const shortFlag = p.short_flag || '';
const longFlag = p.long_flag || '';
let flagHtml;
if (shortFlag && longFlag && shortFlag !== longFlag) {
flagHtml = `<span class="param-flag">${shortFlag}</span><span class="param-flag-long">${longFlag}</span>`;
} else {
flagHtml = `<span class="param-flag">${longFlag}</span>`;
}
let inp = '';
if (p.param_type === 'boolean') {
inputHtml = `<input type="checkbox" ${val ? 'checked' : ''} onchange="setParam('${p.param_key}', this.checked)">`;
inp = `<input type="checkbox" ${val ? 'checked' : ''} onchange="setParam('${p.param_key}', this.checked)">`;
} else if (p.param_type === 'select') {
const options = Array.isArray(p.options) ? p.options : [];
inputHtml = `<select onchange="setParam('${p.param_key}', this.value)">
${options.map(o => `<option value="${o}" ${String(val) === String(o) ? 'selected' : ''}>${o}</option>`).join('')}
</select>`;
const opts = Array.isArray(p.options) ? p.options : [];
inp = `<select onchange="setParam('${p.param_key}', this.value)">${opts.map(o => `<option value="${o}" ${String(val)===String(o) ? 'selected':''}>${o}</option>`).join('')}</select>`;
} else if (p.param_type === 'number') {
const step = p.step || 'any';
const min = p.min_value !== null ? `min="${p.min_value}"` : '';
const max = p.max_value !== null ? `max="${p.max_value}"` : '';
inputHtml = `<input type="number" value="${val}" step="${step}" ${min} ${max}
onchange="setParam('${p.param_key}', this.value)" data-key="${p.param_key}">`;
inp = `<input type="number" value="${val}" step="${step}" ${min} ${max} onchange="setParam('${p.param_key}', this.value)" data-key="${p.param_key}">`;
} else {
inputHtml = `<input type="text" value="${val}" onchange="setParam('${p.param_key}', this.value)" data-key="${p.param_key}">`;
inp = `<input type="text" value="${val}" onchange="setParam('${p.param_key}', this.value)" data-key="${p.param_key}">`;
}
const unitHtml = p.unit ? `<span class="param-unit">${p.unit}</span>` : '';
return `
<div class="param-item ${modifiedClass}" data-key="${p.param_key}">
<span class="param-flag">${flag}</span>
${vramBadge}
<span class="param-desc-text">${p.description}</span>
${inputHtml}
${unitHtml}
</div>
`;
const uh = p.unit ? `<span class="param-unit">${p.unit}</span>` : '';
return `<div class="param-item ${itemClass}" data-key="${p.param_key}">${flagHtml}${vb}<span class="param-desc-text">${p.description}</span>${inp}${uh}</div>`;
}
function isParamModified(p, val) {
const defaultVal = p.default_value;
if (p.param_type === 'boolean') {
return (val === true || val === 'true') !== (defaultVal === 'true');
}
return String(val) !== String(defaultVal);
const dv = p.default_value;
if (p.param_type === 'boolean') return (val===true||val==='true') !== (dv==='true');
return String(val) !== String(dv);
}
function setParam(key, value) {
state.paramValues[key] = value;
// Only update modified visual state, command, and estimate — do NOT re-render
// Update this item's visual state without full re-render (preserves focus)
const item = document.querySelector(`.param-item[data-key="${key}"]`);
if (item) {
const p = state.params.find(p => p.param_key === key);
if (p && isParamModified(p, value)) {
item.classList.add('modified');
} else {
item.classList.remove('modified');
if (p) {
item.classList.remove('param-empty', 'param-changed');
if (p.param_key === 'model' && (!value || String(value).trim() === '')) {
item.classList.add('param-empty');
} else if (isParamModified(p, value)) {
item.classList.add('param-changed');
}
}
}
generateCommand();
updateEstimate();
// If on 'modified' tab, re-render to update the list
if (state.currentTab === 'modified') renderParams();
generateCommand(); updateEstimate();
}
function toggleHiddenParams() {
state.showHidden = !state.showHidden;
renderParams();
}
function toggleHiddenParams() { state.showHidden = !state.showHidden; renderParams(); }
// ===== VRAM Estimation =====
async function updateEstimate() {
const params = collectParamsForEstimate();
const gpuSelections = state.gpuSlots.map(s => ({ name: s.name, vram_mb: s.vram_mb }));
const sysMemory = parseFloat(document.getElementById('sys-memory').value) || 0;
const sysMemUnit = document.getElementById('sys-memory-unit').value;
let sysMemoryGb = 0;
if (sysMemUnit === '1') sysMemoryGb = sysMemory;
else if (sysMemUnit === '2') sysMemoryGb = sysMemory / 1024;
const res = await fetch('/api/estimate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ params, gpu_selections: gpuSelections, mode: state.mode, system_memory_gb: sysMemoryGb })
});
const gpuSel = state.gpuSlots.map(s => ({ name: s.name, vram_mb: s.vram_mb }));
const sm = parseFloat(document.getElementById('sys-memory').value) || 0;
// No unit select anymore - always GB, 0 = unlimited
const sysGb = sm > 0 ? sm : 0;
const isUnlimited = sm === 0;
const res = await fetch('/api/estimate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ params, gpu_selections: gpuSel, mode: state.mode, system_memory_gb: sysGb }) });
const data = await res.json();
state.lastEstimate = data;
renderVramDisplay(data);
if (state.mode === 'gpu_cpu') renderRamDisplay(data, sysMemoryGb);
if (state.mode === 'gpu_cpu') renderRamDisplay(data, sysGb, isUnlimited);
updateStickyBar(data, sysGb, isUnlimited);
generateCommandHint(data, sysGb, isUnlimited);
}
function collectParamsForEstimate() {
@@ -333,124 +333,116 @@ function collectParamsForEstimate() {
params._model_kv_heads = state.selectedModel.kv_heads;
params._model_head_dim = state.selectedModel.head_dim;
params._model_heads = state.selectedModel.attention_heads;
} else {
params._model_size_gb = 0;
params._model_layers = 0;
params._model_embd = 0;
params._model_kv_heads = 0;
params._model_head_dim = 0;
params._model_heads = 0;
}
} else { params._model_size_gb = 0; params._model_layers = 0; params._model_embd = 0; params._model_kv_heads = 0; params._model_head_dim = 0; params._model_heads = 0; }
return params;
}
function renderVramDisplay(data) {
const bar = document.getElementById('vram-bar');
const label = document.getElementById('vram-label');
const breakdown = document.getElementById('vram-breakdown');
if (!data.total_vram_available_mb || data.total_vram_available_mb === 0) {
bar.style.width = '0%';
label.textContent = '请选择GPU';
breakdown.innerHTML = '';
return;
}
const percent = data.usage_percent || 0;
bar.style.width = Math.min(percent, 100) + '%';
bar.className = 'vram-bar' + (percent > 90 ? ' danger' : percent > 75 ? ' warning' : '');
label.textContent = `VRAM: ${data.total_gb}GB / ${data.total_vram_available_gb}GB (${percent}%)`;
breakdown.innerHTML = `
<div class="breakdown-item"><span class="label">模型权重</span><span class="value">${data.weights_gb}GB</span></div>
<div class="breakdown-item"><span class="label">KV缓存</span><span class="value">${data.kv_cache_gb}GB</span></div>
<div class="breakdown-item"><span class="label">计算/开销</span><span class="value">${(data.compute_mb / 1024).toFixed(2)}GB</span></div>
<div class="breakdown-item"><span class="label">CUDA开销</span><span class="value">${(data.cuda_overhead_mb / 1024).toFixed(2)}GB</span></div>
`;
// Update sticky bar instead of inline display
updateStickyBar(data, parseFloat(document.getElementById('sys-memory').value) || 0, (parseFloat(document.getElementById('sys-memory').value) || 0) === 0);
}
function renderRamDisplay(data, sysMemoryGb) {
const bar = document.getElementById('ram-bar');
const label = document.getElementById('ram-label');
const breakdown = document.getElementById('ram-breakdown');
function renderRamDisplay(data, sysGb, isUnlimited) {
// Update sticky ram bar instead of inline display
// (sticky bar is already updated in updateStickyBar)
}
if (sysMemoryGb === 0) {
bar.style.width = '0%';
label.textContent = `内存: ${data.cpu_total_gb}GB (无上限)`;
// ===== Sticky top bar =====
function updateStickyBar(data, sysGb, isUnlimited) {
const vr = document.getElementById('sticky-vram-row');
const rr = document.getElementById('sticky-ram-row');
// VRAM
if (data.total_vram_available_mb) {
const pct = data.usage_percent || 0;
const bar = document.getElementById('sticky-vram-bar');
bar.style.width = Math.min(pct, 100) + '%';
bar.className = 'sticky-bar' + (pct > 90 ? ' danger' : pct > 75 ? ' warning' : '');
document.getElementById('sticky-vram-text').textContent = `VRAM: ${data.total_gb}GB / ${data.total_vram_available_gb}GB (${pct}%)`;
} else {
const percent = data.cpu_usage_percent || 0;
bar.style.width = Math.min(percent, 100) + '%';
bar.className = 'vram-bar ram-bar' + (percent > 90 ? ' danger' : percent > 75 ? ' warning' : '');
label.textContent = `内存: ${data.cpu_total_gb}GB / ${sysMemoryGb}GB (${percent}%)`;
document.getElementById('sticky-vram-text').textContent = 'VRAM: 请选择GPU';
}
// RAM
if (state.mode === 'gpu_cpu') {
rr.classList.remove('hidden');
if (isUnlimited) {
document.getElementById('sticky-ram-bar').style.width = '0%';
document.getElementById('sticky-ram-text').textContent = `内存: ${data.cpu_total_gb}GB (无限制)`;
} else {
const pct = data.cpu_usage_percent || 0;
const bar = document.getElementById('sticky-ram-bar');
bar.style.width = Math.min(pct, 100) + '%';
bar.className = 'sticky-bar ram-sticky-bar' + (pct > 90 ? ' danger' : pct > 75 ? ' warning' : '');
document.getElementById('sticky-ram-text').textContent = `内存: ${data.cpu_total_gb}GB / ${sysGb}GB (${pct}%)`;
}
} else {
rr.classList.add('hidden');
}
}
// ===== Command Hint =====
function generateCommandHint(data, sysGb, isUnlimited) {
const hint = document.getElementById('command-hint');
let hints = [];
// Check VRAM
if (data.usage_percent && data.usage_percent > 100) {
hints.push({ type: 'error', text: `⚠️ 显存不足!预计需要 ${data.total_gb}GB,但仅有 ${data.total_vram_available_gb}GB。建议:减少 GPU 层数(n_gpu_layers)、减小上下文(ctx_size)、使用更低量化版本,或切换到 GPU+CPU 模式。` });
} else if (data.usage_percent && data.usage_percent > 90) {
hints.push({ type: 'warning', text: `⚡ 显存接近上限 (${data.usage_percent}%),可能存在 OOM 风险。建议适当减小上下文或 GPU 层数。` });
} else if (data.usage_percent && data.usage_percent > 75) {
hints.push({ type: 'info', text: `️ 显存使用率 ${data.usage_percent}%,留有余量但不多。` });
} else if (state.selectedModel && data.usage_percent && data.usage_percent <= 75) {
hints.push({ type: 'ok', text: `✅ 显存充足,预计占用 ${data.usage_percent}%。` });
}
breakdown.innerHTML = `
<div class="breakdown-item"><span class="label">CPU模型权重</span><span class="value">${data.cpu_weights_gb}GB</span></div>
<div class="breakdown-item"><span class="label">CPU KV缓存</span><span class="value">${(data.cpu_kv_cache_mb / 1024).toFixed(2)}GB</span></div>
`;
// Check RAM (GPU+CPU mode)
if (state.mode === 'gpu_cpu' && !isUnlimited) {
if (data.cpu_usage_percent && data.cpu_usage_percent > 100) {
hints.push({ type: 'error', text: `⚠️ 系统内存不足!预计需要 ${data.cpu_total_gb}GB,但上限仅 ${sysGb}GB。建议:增加内存上限、减少 GPU 层数让更多权重留在 GPU、或减小上下文。` });
} else if (data.cpu_usage_percent && data.cpu_usage_percent > 90) {
hints.push({ type: 'warning', text: `⚡ 内存使用率 ${data.cpu_usage_percent}%,接近上限。` });
}
}
// No model selected
if (!state.selectedModel) {
hints.push({ type: 'info', text: '💡 请在上方选择模型,以便进行准确的显存估算。' });
}
// No GPU selected
if (state.gpuSlots.length === 0 || !state.gpuSlots[0].name) {
hints.push({ type: 'info', text: '💡 请选择 GPU 显卡。' });
}
if (hints.length === 0) {
hints.push({ type: 'ok', text: '✅ 配置看起来没问题,可以复制使用。' });
}
hint.innerHTML = hints.map(h => `<div class="hint-${h.type}">${h.text}</div>`).join('');
}
// ===== Natural Language =====
async function parseNaturalLanguage() {
const text = document.getElementById('nl-input').value;
if (!text.trim()) return;
const res = await fetch('/api/parse-nl', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
const res = await fetch('/api/parse-nl', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) });
const data = await res.json();
let resultHtml = '<div>解析结果:</div>';
let html = '<div>解析结果:</div>';
if (data._gpu_name) {
const gpu = state.gpus.find(g => g.name === data._gpu_name);
if (gpu) {
if (data._gpu_count && data._gpu_count > 1) {
state.gpuSlots = [];
for (let i = 0; i < Math.min(data._gpu_count, 4); i++) state.gpuSlots.push({ ...gpu });
} else {
state.gpuSlots = [{ ...gpu }];
}
renderGpuSlots();
resultHtml += `<span class="parsed-param"><span class="key">GPU:</span> <span class="value">${data._gpu_name}${data._gpu_count > 1 ? ' x' + data._gpu_count : ''}</span></span>`;
}
const g = state.gpus.find(g => g.name === data._gpu_name);
if (g) { const cnt = data._gpu_count > 1 ? Math.min(data._gpu_count, 4) : 1; state.gpuSlots = []; for (let i = 0; i < cnt; i++) state.gpuSlots.push({ ...g }); renderGpuSlots(); html += `<span class="parsed-param"><span class="key">GPU:</span> <span class="value">${data._gpu_name}${cnt > 1 ? ' x'+cnt : ''}</span></span>`; }
}
if (data._mode) {
switchMode(data._mode);
resultHtml += `<span class="parsed-param"><span class="key">模式:</span> <span class="value">${data._mode}</span></span>`;
}
for (const [key, value] of Object.entries(data)) {
if (key.startsWith('_')) continue;
state.paramValues[key] = value;
resultHtml += `<span class="parsed-param"><span class="key">${key}:</span> <span class="value">${value}</span></span>`;
}
document.getElementById('nl-result').innerHTML = resultHtml;
renderParams();
generateCommand();
updateEstimate();
if (data._mode) { switchMode(data._mode); html += `<span class="parsed-param"><span class="key">模式:</span> <span class="value">${data._mode}</span></span>`; }
for (const [k, v] of Object.entries(data)) { if (k.startsWith('_')) continue; state.paramValues[k] = v; html += `<span class="parsed-param"><span class="key">${k}:</span> <span class="value">${v}</span></span>`; }
document.getElementById('nl-result').innerHTML = html;
renderParams(); generateCommand(); updateEstimate();
}
// ===== Generate Command =====
async function generateCommand() {
const params = { ...state.paramValues };
Object.keys(params).forEach(k => { if (k.startsWith('_')) delete params[k]; });
const res = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
version_id: state.currentVersionId,
params,
mode: state.mode,
gpu_selections: state.gpuSlots,
binary: document.getElementById('binary-select').value,
})
});
const res = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version_id: state.currentVersionId, params, mode: state.mode, gpu_selections: state.gpuSlots, binary: document.getElementById('binary-select').value }) });
const data = await res.json();
document.getElementById('command-output').textContent = data.command;
}
@@ -458,24 +450,17 @@ async function generateCommand() {
// ===== Copy =====
function copyCommand() {
const text = document.getElementById('command-output').textContent;
// Use textarea fallback for non-HTTPS environments
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px';
document.body.appendChild(ta); ta.select();
try {
document.execCommand('copy');
const btn = document.querySelector('.btn-copy');
const orig = btn.textContent;
btn.textContent = '✅ 已复制!';
setTimeout(() => btn.textContent = orig, 2000);
} catch(e) {
alert('复制失败,请手动选择文本复制');
}
const btn = document.getElementById('copy-btn');
btn.textContent = '✅ 已复制';
btn.classList.add('copied');
setTimeout(() => { btn.textContent = '📋 复制'; btn.classList.remove('copied'); }, 2000);
} catch(e) { alert('复制失败,请手动选择文本复制'); }
document.body.removeChild(ta);
}
// ===== Init =====
window.addEventListener('DOMContentLoaded', init);