feat: v1.2.0 - 三项优化
1. 模型选择改为两步式:先搜索选择基模型,再选择量化版本 - 自动选中默认量化版本(Q4_K_M),后台可配置 - 模型新增 base_model 字段,按基模型分组 - 后台模型管理增加基模型字段 2. 参数搜索改为全局搜索 - 搜索时跨所有分类显示匹配参数 - 隐藏分类标签页,显示匹配数量 3. 自然语言支持 LLM 接口 - 后台可配置 LLM API (URL/Key/Model/System Prompt) - 启用后优先调用 LLM 解析,失败自动回退正则解析 - 兼容 OpenAI API 格式
This commit is contained in:
@@ -7,6 +7,7 @@ import json
|
|||||||
import re
|
import re
|
||||||
import math
|
import math
|
||||||
import functools
|
import functools
|
||||||
|
import urllib.request
|
||||||
from flask import Flask, request, jsonify, send_from_directory, session, redirect
|
from flask import Flask, request, jsonify, send_from_directory, session, redirect
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from db import get_db, init_db, DB_PATH
|
from db import get_db, init_db, DB_PATH
|
||||||
@@ -600,16 +601,86 @@ def get_models():
|
|||||||
db.close()
|
db.close()
|
||||||
return jsonify(result)
|
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})
|
||||||
|
|
||||||
|
|
||||||
# ----- Parse Natural Language -----
|
# ----- Parse Natural Language -----
|
||||||
@app.route('/api/parse-nl', methods=['POST'])
|
@app.route('/api/parse-nl', methods=['POST'])
|
||||||
def parse_nl():
|
def parse_nl():
|
||||||
data = request.json
|
data = request.json
|
||||||
text = data.get('text', '')
|
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)
|
result = parse_natural_language(text)
|
||||||
return jsonify(result)
|
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 ====================
|
# ==================== Admin API ====================
|
||||||
|
|
||||||
# All admin routes below require login
|
# All admin routes below require login
|
||||||
@@ -800,9 +871,9 @@ def admin_add_model():
|
|||||||
data = request.json
|
data = request.json
|
||||||
db = get_db()
|
db = get_db()
|
||||||
db.execute(
|
db.execute(
|
||||||
'''INSERT INTO models (name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, quant, description, sort_order)
|
'''INSERT INTO models (base_model, name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, quant, description, sort_order)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
|
||||||
(data['name'], data['size_gb'], data['layers'], data['embd'],
|
(data['base_model'], data['name'], data['size_gb'], data['layers'], data['embd'],
|
||||||
data['kv_heads'], data['head_dim'], data['attention_heads'],
|
data['kv_heads'], data['head_dim'], data['attention_heads'],
|
||||||
data.get('quant', ''), data.get('description', ''), data.get('sort_order', 0))
|
data.get('quant', ''), data.get('description', ''), data.get('sort_order', 0))
|
||||||
)
|
)
|
||||||
@@ -818,9 +889,9 @@ def admin_model_edit(mid):
|
|||||||
if request.method == 'PUT':
|
if request.method == 'PUT':
|
||||||
data = request.json
|
data = request.json
|
||||||
db.execute(
|
db.execute(
|
||||||
'''UPDATE models SET name=?, size_gb=?, layers=?, embd=?, kv_heads=?,
|
'''UPDATE models SET base_model=?, name=?, size_gb=?, layers=?, embd=?, kv_heads=?,
|
||||||
head_dim=?, attention_heads=?, quant=?, description=?, sort_order=? WHERE id=?''',
|
head_dim=?, attention_heads=?, quant=?, description=?, sort_order=? WHERE id=?''',
|
||||||
(data['name'], data['size_gb'], data['layers'], data['embd'],
|
(data['base_model'], data['name'], data['size_gb'], data['layers'], data['embd'],
|
||||||
data['kv_heads'], data['head_dim'], data['attention_heads'],
|
data['kv_heads'], data['head_dim'], data['attention_heads'],
|
||||||
data.get('quant', ''), data.get('description', ''), data.get('sort_order', 0), mid)
|
data.get('quant', ''), data.get('description', ''), data.get('sort_order', 0), mid)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ def init_db():
|
|||||||
c.execute('''
|
c.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS models (
|
CREATE TABLE IF NOT EXISTS models (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
base_model TEXT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
size_gb REAL NOT NULL,
|
size_gb REAL NOT NULL,
|
||||||
layers INTEGER NOT NULL,
|
layers INTEGER NOT NULL,
|
||||||
@@ -312,44 +313,79 @@ def insert_default_data(conn):
|
|||||||
(v2_id,) + p)
|
(v2_id,) + p)
|
||||||
|
|
||||||
# ===== Default models =====
|
# ===== Default models =====
|
||||||
|
# (base_model, name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, quant, description, sort_order)
|
||||||
default_models = [
|
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
|
||||||
("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", "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 (FP16)", 15.5, 32, 4096, 8, 128, 32, "FP16", "Meta Llama 3 8B Instruct, FP16", 3),
|
("Llama-3-8B-Instruct", "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-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-8B-Instruct", "Llama-3-8B-Instruct (FP16)", 15.5, 32, 4096, 8, 128, 32, "FP16", "Meta Llama 3 8B Instruct, FP16", 3),
|
||||||
("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
|
||||||
("Llama-3-70B-Instruct (FP16)", 138.0, 80, 8192, 8, 128, 64, "FP16", "Meta Llama 3 70B Instruct, FP16", 6),
|
("Llama-3-70B-Instruct", "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.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-70B-Instruct", "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.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-70B-Instruct", "Llama-3-70B-Instruct (FP16)", 138.0, 80, 8192, 8, 128, 64, "FP16", "Meta Llama 3 70B Instruct, FP16", 6),
|
||||||
("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-8B-Instruct
|
||||||
("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),
|
("Llama-3.1-8B-Instruct", "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),
|
||||||
("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),
|
("Llama-3.1-8B-Instruct", "Llama-3.1-8B-Instruct (Q8_0)", 8.5, 32, 4096, 8, 128, 32, "Q8_0", "Meta Llama 3.1 8B Instruct, Q8_0 量化", 8),
|
||||||
("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),
|
# Llama-3.1-70B-Instruct
|
||||||
("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),
|
("Llama-3.1-70B-Instruct", "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 量化", 9),
|
||||||
("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),
|
("Llama-3.1-70B-Instruct", "Llama-3.1-70B-Instruct (Q8_0)", 74.0, 80, 8192, 8, 128, 64, "Q8_0", "Meta Llama 3.1 70B Instruct, Q8_0 量化", 10),
|
||||||
("Qwen2.5-72B-Instruct (Q8_0)", 75.0, 80, 8192, 8, 128, 64, "Q8_0", "Qwen2.5 72B Instruct, Q8_0 量化", 15),
|
# Llama-3.1-405B-Instruct
|
||||||
("DeepSeek-V2-Chat (Q4_K_M)", 23.0, 60, 5120, 8, 128, 60, "Q4_K_M", "DeepSeek V2 Chat, Q4_K_M 量化", 16),
|
("Llama-3.1-405B-Instruct", "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 量化", 11),
|
||||||
("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),
|
# Qwen2.5-7B-Instruct
|
||||||
("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),
|
("Qwen2.5-7B-Instruct", "Qwen2.5-7B-Instruct (Q4_K_M)", 4.7, 28, 3584, 4, 128, 28, "Q4_K_M", "Qwen2.5 7B Instruct, Q4_K_M 量化", 12),
|
||||||
("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),
|
("Qwen2.5-7B-Instruct", "Qwen2.5-7B-Instruct (Q8_0)", 7.6, 28, 3584, 4, 128, 28, "Q8_0", "Qwen2.5 7B Instruct, Q8_0 量化", 13),
|
||||||
("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),
|
# Qwen2.5-14B-Instruct
|
||||||
("Mixtral-8x7B-Instruct (Q4_K_M)", 26.0, 32, 4096, 8, 128, 32, "Q4_K_M", "Mixtral 8x7B Instruct, Q4_K_M 量化", 21),
|
("Qwen2.5-14B-Instruct", "Qwen2.5-14B-Instruct (Q4_K_M)", 8.7, 40, 5120, 8, 128, 40, "Q4_K_M", "Qwen2.5 14B Instruct, Q4_K_M 量化", 14),
|
||||||
("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),
|
# Qwen2.5-32B-Instruct
|
||||||
("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),
|
("Qwen2.5-32B-Instruct", "Qwen2.5-32B-Instruct (Q4_K_M)", 19.5, 64, 5120, 8, 128, 64, "Q4_K_M", "Qwen2.5 32B Instruct, Q4_K_M 量化", 15),
|
||||||
("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),
|
("Qwen2.5-32B-Instruct", "Qwen2.5-32B-Instruct (Q8_0)", 32.0, 64, 5120, 8, 128, 64, "Q8_0", "Qwen2.5 32B Instruct, Q8_0 量化", 16),
|
||||||
("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),
|
# Qwen2.5-72B-Instruct
|
||||||
("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),
|
("Qwen2.5-72B-Instruct", "Qwen2.5-72B-Instruct (Q4_K_M)", 42.0, 80, 8192, 8, 128, 64, "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, "Q8_0", "Qwen2.5 72B Instruct, Q8_0 量化", 18),
|
||||||
|
# DeepSeek-V2-Chat
|
||||||
|
("DeepSeek-V2-Chat", "DeepSeek-V2-Chat (Q4_K_M)", 23.0, 60, 5120, 8, 128, 60, "Q4_K_M", "DeepSeek V2 Chat, Q4_K_M 量化", 19),
|
||||||
|
# DeepSeek-V2.5-Chat
|
||||||
|
("DeepSeek-V2.5-Chat", "DeepSeek-V2.5-Chat (Q4_K_M)", 23.0, 60, 5120, 8, 128, 60, "Q4_K_M", "DeepSeek V2.5 Chat, Q4_K_M 量化", 20),
|
||||||
|
# DeepSeek-R1-Distill-Qwen-32B
|
||||||
|
("DeepSeek-R1-Distill-Qwen-32B", "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 量化", 21),
|
||||||
|
("DeepSeek-R1-Distill-Qwen-32B", "DeepSeek-R1-Distill-Qwen-32B (Q8_0)", 32.0, 64, 5120, 8, 128, 64, "Q8_0", "DeepSeek R1 Distill Qwen 32B, Q8_0 量化", 22),
|
||||||
|
# DeepSeek-R1-Distill-Llama-70B
|
||||||
|
("DeepSeek-R1-Distill-Llama-70B", "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 量化", 23),
|
||||||
|
("DeepSeek-R1-Distill-Llama-70B", "DeepSeek-R1-Distill-Llama-70B (Q8_0)", 75.0, 80, 8192, 8, 128, 64, "Q8_0", "DeepSeek R1 Distill Llama 70B, Q8_0 量化", 24),
|
||||||
|
# Mistral-7B-Instruct-v0.3
|
||||||
|
("Mistral-7B-Instruct-v0.3", "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 量化", 25),
|
||||||
|
("Mistral-7B-Instruct-v0.3", "Mistral-7B-Instruct-v0.3 (Q8_0)", 7.5, 32, 4096, 8, 128, 32, "Q8_0", "Mistral 7B Instruct v0.3, Q8_0 量化", 26),
|
||||||
|
# Mixtral-8x7B-Instruct
|
||||||
|
("Mixtral-8x7B-Instruct", "Mixtral-8x7B-Instruct (Q4_K_M)", 26.0, 32, 4096, 8, 128, 32, "Q4_K_M", "Mixtral 8x7B Instruct, Q4_K_M 量化", 27),
|
||||||
|
# Gemma-2-9B-It
|
||||||
|
("Gemma-2-9B-It", "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 量化", 28),
|
||||||
|
# Gemma-2-27B-It
|
||||||
|
("Gemma-2-27B-It", "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 量化", 29),
|
||||||
|
# Phi-3-Mini-4K-Instruct
|
||||||
|
("Phi-3-Mini-4K-Instruct", "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 量化", 30),
|
||||||
|
# Phi-3-Medium-14B-Instruct
|
||||||
|
("Phi-3-Medium-14B-Instruct", "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 量化", 31),
|
||||||
|
# GLM-4-9B-Chat
|
||||||
|
("GLM-4-9B-Chat", "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 量化", 32),
|
||||||
|
("GLM-4-9B-Chat", "GLM-4-9B-Chat (Q8_0)", 9.0, 40, 4096, 4, 128, 40, "Q8_0", "Zhipu GLM-4 9B Chat, Q8_0 量化", 33),
|
||||||
]
|
]
|
||||||
|
|
||||||
for m in default_models:
|
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)
|
c.execute('''INSERT INTO models (base_model, name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, quant, description, sort_order)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', m)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', m)
|
||||||
|
|
||||||
# ===== Default settings =====
|
# ===== 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 (?, ?)", ("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_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_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_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 (?, ?)", ("llm_system_prompt", "你是一个llama.cpp命令行参数解析助手。用户会用自然语言描述他想运行的模型和参数配置,你需要将其解析为JSON格式的参数。\n\n可用的参数键包括: model, ctx_size, n_gpu_layers, threads, batch_size, temperature, top_k, top_p, flash_attn, port, host, parallel, split_mode, mlock, numa, repeat_penalty, presence_penalty, frequency_penalty, seed, min_p, typical, mirostat, mirostat_lr, mirostat_ent。\n\nGPU型号会通过 _gpu_name 字段返回,GPU数量通过 _gpu_count 返回。\n\n只返回JSON,不要其他文本。"))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -123,7 +123,8 @@
|
|||||||
<div class="admin-add-form">
|
<div class="admin-add-form">
|
||||||
<h3>添加新模型</h3>
|
<h3>添加新模型</h3>
|
||||||
<div class="form-grid">
|
<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-size" placeholder="大小(GB)" step="0.1">
|
||||||
<input type="number" id="model-layers" placeholder="层数">
|
<input type="number" id="model-layers" placeholder="层数">
|
||||||
<input type="number" id="model-embd" placeholder="嵌入维度">
|
<input type="number" id="model-embd" placeholder="嵌入维度">
|
||||||
@@ -137,7 +138,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="admin-table">
|
<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>大小(GB)</th><th>层数</th><th>EMBD</th><th>KV</th><th>HD</th><th>Heads</th><th>量化</th><th>排序</th><th>操作</th></tr></thead>
|
||||||
<tbody id="model-table-body"></tbody>
|
<tbody id="model-table-body"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+24
-3
@@ -177,7 +177,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 { 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; }
|
.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 { position: relative; }
|
||||||
.model-select-container input {
|
.model-select-container input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -215,8 +218,18 @@ header h1 {
|
|||||||
.model-name { font-size: 0.9em; }
|
.model-name { font-size: 0.9em; }
|
||||||
.model-meta { font-size: 0.8em; color: var(--text-dim); }
|
.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-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 { background: var(--bg-input); padding: 4px 10px; border-radius: 4px; }
|
||||||
.detail-item b { color: var(--accent2); }
|
.detail-item b { color: var(--accent2); }
|
||||||
|
|
||||||
@@ -235,7 +248,7 @@ header h1 {
|
|||||||
.tab-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
|
.tab-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
|
||||||
|
|
||||||
/* ===== Param Search ===== */
|
/* ===== Param Search ===== */
|
||||||
.param-search-container { margin-bottom: 12px; }
|
.param-search-container { margin-bottom: 12px; position: relative; }
|
||||||
.param-search-container input {
|
.param-search-container input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
@@ -246,6 +259,14 @@ header h1 {
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
.param-search-container input:focus { outline: none; border-color: var(--accent); }
|
.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) ===== */
|
/* ===== Parameter Items (one per line) ===== */
|
||||||
#param-container { display: flex; flex-direction: column; gap: 6px; }
|
#param-container { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
|||||||
+19
-11
@@ -39,8 +39,6 @@
|
|||||||
<h2>🖥️ GPU 配置</h2>
|
<h2>🖥️ GPU 配置</h2>
|
||||||
<div id="gpu-slots"></div>
|
<div id="gpu-slots"></div>
|
||||||
<button class="btn-add" id="btn-add-gpu" onclick="addGpuSlot()">+ 添加显卡</button>
|
<button class="btn-add" id="btn-add-gpu" onclick="addGpuSlot()">+ 添加显卡</button>
|
||||||
|
|
||||||
<!-- VRAM Display -->
|
|
||||||
<div class="vram-display" id="vram-display">
|
<div class="vram-display" id="vram-display">
|
||||||
<div class="vram-bar-container">
|
<div class="vram-bar-container">
|
||||||
<div class="vram-bar" id="vram-bar"></div>
|
<div class="vram-bar" id="vram-bar"></div>
|
||||||
@@ -71,13 +69,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Model Selection -->
|
<!-- Model Selection (two-step) -->
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>📦 选择模型</h2>
|
<h2>📦 选择模型</h2>
|
||||||
<div class="model-select-container">
|
<!-- Step 1: Select base model -->
|
||||||
<input type="text" id="model-search" placeholder="搜索模型名称..." oninput="filterModels()" autocomplete="off">
|
<div class="model-step">
|
||||||
<div class="model-dropdown" id="model-dropdown"></div>
|
<label class="model-step-label">第1步:选择模型</label>
|
||||||
|
<div class="model-select-container">
|
||||||
|
<input type="text" id="model-search" placeholder="搜索模型名称..." oninput="filterBaseModels()" autocomplete="off" onfocus="this.select()">
|
||||||
|
<div class="model-dropdown" id="model-dropdown"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Step 2: Select quant -->
|
||||||
|
<div class="model-step hidden" id="quant-step">
|
||||||
|
<label class="model-step-label">第2步:选择量化版本</label>
|
||||||
|
<div id="quant-options"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Selected model info -->
|
||||||
<div id="model-selected-info" class="model-selected-info"></div>
|
<div id="model-selected-info" class="model-selected-info"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -94,7 +102,11 @@
|
|||||||
<!-- Parameters -->
|
<!-- Parameters -->
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>⚙️ 参数配置</h2>
|
<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 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="sampling" onclick="switchTab('sampling')">采样参数</button>
|
||||||
<button class="tab-btn" data-cat="server" onclick="switchTab('server')">服务器参数</button>
|
<button class="tab-btn" data-cat="server" onclick="switchTab('server')">服务器参数</button>
|
||||||
@@ -103,9 +115,6 @@
|
|||||||
<button class="tab-btn" data-cat="logging" onclick="switchTab('logging')">日志</button>
|
<button class="tab-btn" data-cat="logging" onclick="switchTab('logging')">日志</button>
|
||||||
<button class="tab-btn" data-cat="advanced" onclick="switchTab('advanced')">高级</button>
|
<button class="tab-btn" data-cat="advanced" onclick="switchTab('advanced')">高级</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="param-search-container">
|
|
||||||
<input type="text" id="param-search" placeholder="搜索参数名称或标志..." oninput="onParamSearch()" autocomplete="off">
|
|
||||||
</div>
|
|
||||||
<div id="param-container"></div>
|
<div id="param-container"></div>
|
||||||
<button class="btn-toggle-advanced" onclick="toggleHiddenParams()" id="toggle-advanced-btn">▼ 显示更多参数</button>
|
<button class="btn-toggle-advanced" onclick="toggleHiddenParams()" id="toggle-advanced-btn">▼ 显示更多参数</button>
|
||||||
</section>
|
</section>
|
||||||
@@ -120,7 +129,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/main.js"></script>
|
<script src="/js/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+27
-9
@@ -239,7 +239,8 @@ function renderModelTable() {
|
|||||||
document.getElementById('model-table-body').innerHTML = adminState.models.map(m => `
|
document.getElementById('model-table-body').innerHTML = adminState.models.map(m => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${m.id}</td>
|
<td>${m.id}</td>
|
||||||
<td><input type="text" value="${m.name}" onchange="updateModel(${m.id}, 'name', this.value)"></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:70px"></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.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.embd}" onchange="updateModel(${m.id}, 'embd', this.value)" style="width:70px"></td>
|
||||||
@@ -254,6 +255,7 @@ function renderModelTable() {
|
|||||||
|
|
||||||
async function addModel() {
|
async function addModel() {
|
||||||
const data = {
|
const data = {
|
||||||
|
base_model: document.getElementById('model-basemodel').value,
|
||||||
name: document.getElementById('model-name').value,
|
name: document.getElementById('model-name').value,
|
||||||
size_gb: parseFloat(document.getElementById('model-size').value) || 0,
|
size_gb: parseFloat(document.getElementById('model-size').value) || 0,
|
||||||
layers: parseInt(document.getElementById('model-layers').value) || 0,
|
layers: parseInt(document.getElementById('model-layers').value) || 0,
|
||||||
@@ -265,16 +267,16 @@ async function addModel() {
|
|||||||
description: document.getElementById('model-desc').value,
|
description: document.getElementById('model-desc').value,
|
||||||
sort_order: parseInt(document.getElementById('model-order').value) || 0,
|
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) });
|
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-quant','model-desc','model-order'].forEach(id => document.getElementById(id).value = id === 'model-order' ? '0' : '');
|
||||||
await loadAdminModels();
|
await loadAdminModels();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateModel(id, field, value) {
|
async function updateModel(id, field, value) {
|
||||||
const m = adminState.models.find(m => m.id === id);
|
const m = adminState.models.find(m => m.id === id);
|
||||||
if (!m) return;
|
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, quant: m.quant, description: m.description, sort_order: m.sort_order };
|
||||||
if (['size_gb'].includes(field)) data[field] = parseFloat(value) || 0;
|
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','sort_order'].includes(field)) data[field] = parseInt(value) || 0;
|
||||||
else data[field] = value;
|
else data[field] = value;
|
||||||
@@ -297,11 +299,27 @@ async function loadAdminSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSettings() {
|
function renderSettings() {
|
||||||
document.getElementById('settings-form').innerHTML = Object.entries(adminState.settings).map(([key, value]) => `
|
const labels = {
|
||||||
<div class="setting-item">
|
'admin_password': '管理密码',
|
||||||
<label>${key}</label>
|
'default_gpu': '默认GPU',
|
||||||
<input type="text" value="${value}" onchange="adminState.settings['${key}'] = this.value">
|
'default_version': '默认版本',
|
||||||
</div>`).join('');
|
'default_mode': '默认模式',
|
||||||
|
'default_quant': '默认量化版本',
|
||||||
|
'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() {
|
async function saveSettings() {
|
||||||
|
|||||||
+150
-320
@@ -1,59 +1,42 @@
|
|||||||
// ===== State =====
|
// ===== State =====
|
||||||
let state = {
|
let state = {
|
||||||
versions: [],
|
versions: [], gpus: [], models: [], modelsGrouped: {},
|
||||||
gpus: [],
|
baseModelList: [], filteredBaseModels: [],
|
||||||
models: [],
|
currentVersionId: null, params: [], paramValues: {},
|
||||||
filteredModels: [],
|
mode: 'gpu', gpuSlots: [], showHidden: false,
|
||||||
selectedModel: null,
|
currentTab: 'common', paramSearchText: '',
|
||||||
currentVersionId: null,
|
selectedModel: null, defaultQuant: 'Q4_K_M',
|
||||||
params: [],
|
|
||||||
paramValues: {},
|
|
||||||
mode: 'gpu',
|
|
||||||
gpuSlots: [],
|
|
||||||
showHidden: false,
|
|
||||||
currentTab: 'common',
|
|
||||||
paramSearchText: '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== Init =====
|
// ===== Init =====
|
||||||
async function init() {
|
async function init() {
|
||||||
await loadVersions();
|
await loadVersions();
|
||||||
await loadGpus();
|
await loadGpus();
|
||||||
await loadModels();
|
await loadModelsGrouped();
|
||||||
const defaultVer = state.versions.find(v => v.version_tag === 'b10068') || state.versions[0];
|
const dv = state.versions.find(v => v.version_tag === 'b10068') || state.versions[0];
|
||||||
if (defaultVer) {
|
if (dv) { state.currentVersionId = dv.id; document.getElementById('version-select').value = dv.id; await loadParams(dv.id); }
|
||||||
state.currentVersionId = defaultVer.id;
|
const dg = state.gpus.find(g => g.name === 'RTX 3090') || state.gpus[0];
|
||||||
document.getElementById('version-select').value = defaultVer.id;
|
if (dg) { state.gpuSlots = [{ ...dg }]; renderGpuSlots(); }
|
||||||
await loadParams(defaultVer.id);
|
renderParams(); generateCommand(); updateEstimate();
|
||||||
}
|
|
||||||
const defaultGpu = state.gpus.find(g => g.name === 'RTX 3090') || state.gpus[0];
|
|
||||||
if (defaultGpu) {
|
|
||||||
state.gpuSlots = [{ ...defaultGpu }];
|
|
||||||
renderGpuSlots();
|
|
||||||
}
|
|
||||||
renderParams();
|
|
||||||
generateCommand();
|
|
||||||
updateEstimate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Load Data =====
|
// ===== Load Data =====
|
||||||
async function loadVersions() {
|
async function loadVersions() {
|
||||||
const res = await fetch('/api/versions');
|
const res = await fetch('/api/versions'); state.versions = await res.json();
|
||||||
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('');
|
||||||
const sel = document.getElementById('version-select');
|
|
||||||
sel.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() {
|
async function loadModelsGrouped() {
|
||||||
const res = await fetch('/api/gpus');
|
const res = await fetch('/api/models/grouped');
|
||||||
state.gpus = await res.json();
|
const data = await res.json();
|
||||||
}
|
state.modelsGrouped = data.models || {};
|
||||||
|
state.defaultQuant = data.default_quant || 'Q4_K_M';
|
||||||
async function loadModels() {
|
state.baseModelList = Object.keys(state.modelsGrouped).sort();
|
||||||
const res = await fetch('/api/models');
|
state.filteredBaseModels = state.baseModelList;
|
||||||
state.models = await res.json();
|
// Also flat list for backward compat
|
||||||
state.filteredModels = state.models;
|
state.models = Object.values(state.modelsGrouped).flat();
|
||||||
renderModelDropdown();
|
renderBaseModelDropdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadParams(versionId) {
|
async function loadParams(versionId) {
|
||||||
@@ -61,98 +44,67 @@ async function loadParams(versionId) {
|
|||||||
state.params = await res.json();
|
state.params = await res.json();
|
||||||
state.paramValues = {};
|
state.paramValues = {};
|
||||||
state.params.forEach(p => {
|
state.params.forEach(p => {
|
||||||
if (p.param_type === 'boolean') {
|
state.paramValues[p.param_key] = p.param_type === 'boolean' ? (p.default_value === 'true') : (p.default_value || '');
|
||||||
state.paramValues[p.param_key] = p.default_value === 'true';
|
|
||||||
} else {
|
|
||||||
state.paramValues[p.param_key] = p.default_value || '';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
renderParams();
|
renderParams(); generateCommand(); updateEstimate();
|
||||||
generateCommand();
|
|
||||||
updateEstimate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Version Change =====
|
// ===== Version Change =====
|
||||||
async function onVersionChange() {
|
async function onVersionChange() { state.currentVersionId = parseInt(document.getElementById('version-select').value); await loadParams(state.currentVersionId); }
|
||||||
const sel = document.getElementById('version-select');
|
|
||||||
state.currentVersionId = parseInt(sel.value);
|
|
||||||
await loadParams(state.currentVersionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Mode Switch =====
|
// ===== Mode Switch =====
|
||||||
function switchMode(mode) {
|
function switchMode(mode) {
|
||||||
state.mode = mode;
|
state.mode = mode;
|
||||||
document.querySelectorAll('.mode-btn').forEach(btn => {
|
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.mode === mode));
|
||||||
btn.classList.toggle('active', btn.dataset.mode === mode);
|
|
||||||
});
|
|
||||||
document.getElementById('memory-panel').classList.toggle('hidden', mode !== 'gpu_cpu');
|
document.getElementById('memory-panel').classList.toggle('hidden', mode !== 'gpu_cpu');
|
||||||
generateCommand();
|
generateCommand(); updateEstimate();
|
||||||
updateEstimate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== GPU Slots =====
|
// ===== GPU Slots =====
|
||||||
function renderGpuSlots() {
|
function renderGpuSlots() {
|
||||||
const container = document.getElementById('gpu-slots');
|
document.getElementById('gpu-slots').innerHTML = state.gpuSlots.map((gpu, i) => {
|
||||||
container.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('');
|
||||||
const options = state.gpus.map(g =>
|
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>`;
|
||||||
`<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>
|
|
||||||
`;
|
|
||||||
}).join('');
|
}).join('');
|
||||||
document.getElementById('btn-add-gpu').style.display = state.gpuSlots.length >= 4 ? 'none' : 'block';
|
document.getElementById('btn-add-gpu').style.display = state.gpuSlots.length >= 4 ? 'none' : 'block';
|
||||||
updateEstimate();
|
updateEstimate();
|
||||||
}
|
}
|
||||||
|
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(); generateCommand(); }
|
||||||
|
function removeGpuSlot(i) { state.gpuSlots.splice(i, 1); renderGpuSlots(); generateCommand(); }
|
||||||
|
function updateGpuSlot(i, name) { const g = state.gpus.find(g => g.name === name); if (g) state.gpuSlots[i] = { ...g }; renderGpuSlots(); generateCommand(); }
|
||||||
|
|
||||||
function addGpuSlot() {
|
// ===== Model Selection (two-step) =====
|
||||||
if (state.gpuSlots.length >= 4) return;
|
function filterBaseModels() {
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Model Selection =====
|
|
||||||
function filterModels() {
|
|
||||||
const text = document.getElementById('model-search').value.toLowerCase();
|
const text = document.getElementById('model-search').value.toLowerCase();
|
||||||
state.filteredModels = state.models.filter(m =>
|
state.filteredBaseModels = state.baseModelList.filter(n => n.toLowerCase().includes(text));
|
||||||
m.name.toLowerCase().includes(text) ||
|
renderBaseModelDropdown();
|
||||||
(m.quant || '').toLowerCase().includes(text) ||
|
document.getElementById('model-dropdown').style.display = state.filteredBaseModels.length > 0 ? 'block' : 'none';
|
||||||
(m.description || '').toLowerCase().includes(text)
|
|
||||||
);
|
|
||||||
renderModelDropdown();
|
|
||||||
document.getElementById('model-dropdown').style.display = state.filteredModels.length > 0 ? 'block' : 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderModelDropdown() {
|
function renderBaseModelDropdown() {
|
||||||
const dd = document.getElementById('model-dropdown');
|
document.getElementById('model-dropdown').innerHTML = state.filteredBaseModels.map(n => {
|
||||||
dd.innerHTML = state.filteredModels.map(m => `
|
const quants = state.modelsGrouped[n] || [];
|
||||||
<div class="model-option" onclick="selectModel(${m.id})">
|
const quantStr = quants.map(q => q.quant).filter(Boolean).join(', ');
|
||||||
<span class="model-name">${m.name}</span>
|
return `<div class="model-option" onclick="selectBaseModel('${n}')"><span class="model-name">${n}</span><span class="model-meta">${quantStr}</span></div>`;
|
||||||
<span class="model-meta">${m.size_gb}GB | ${m.layers}L | ${m.quant || ''}</span>
|
}).join('');
|
||||||
</div>
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
// Show quant step
|
||||||
|
document.getElementById('quant-step').classList.remove('hidden');
|
||||||
|
// Auto-select default quant
|
||||||
|
const defaultVariant = variants.find(v => v.quant === state.defaultQuant) || variants[0];
|
||||||
|
renderQuantOptions(baseName, variants, defaultVariant.id);
|
||||||
|
selectModel(defaultVariant.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('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,19 +112,20 @@ function selectModel(id) {
|
|||||||
const m = state.models.find(m => m.id === id);
|
const m = state.models.find(m => m.id === id);
|
||||||
if (!m) return;
|
if (!m) return;
|
||||||
state.selectedModel = m;
|
state.selectedModel = m;
|
||||||
document.getElementById('model-search').value = m.name;
|
// Update quant buttons active state
|
||||||
document.getElementById('model-dropdown').style.display = 'none';
|
const variants = state.modelsGrouped[m.base_model] || [];
|
||||||
|
renderQuantOptions(m.base_model, variants, id);
|
||||||
|
// Show model detail
|
||||||
document.getElementById('model-selected-info').innerHTML = `
|
document.getElementById('model-selected-info').innerHTML = `
|
||||||
<div class="model-detail">
|
<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.size_gb} GB</span>
|
||||||
<span class="detail-item"><b>层数:</b> ${m.layers}</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>KV Heads:</b> ${m.kv_heads}</span>
|
||||||
<span class="detail-item"><b>Head Dim:</b> ${m.head_dim}</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>
|
<span class="detail-item"><b>量化:</b> ${m.quant || 'N/A'}</span>
|
||||||
</div>
|
</div>`;
|
||||||
`;
|
|
||||||
updateEstimate();
|
updateEstimate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,25 +134,39 @@ function switchTab(cat) {
|
|||||||
state.currentTab = cat;
|
state.currentTab = cat;
|
||||||
state.paramSearchText = '';
|
state.paramSearchText = '';
|
||||||
document.getElementById('param-search').value = '';
|
document.getElementById('param-search').value = '';
|
||||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
document.getElementById('search-hint').textContent = '';
|
||||||
btn.classList.toggle('active', btn.dataset.cat === cat);
|
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.cat === cat));
|
||||||
});
|
document.getElementById('param-tabs').style.display = '';
|
||||||
renderParams();
|
renderParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onParamSearch() {
|
function onParamSearch() {
|
||||||
state.paramSearchText = document.getElementById('param-search').value.toLowerCase();
|
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();
|
renderParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilteredParams() {
|
function getFilteredParams() {
|
||||||
let params = state.params.filter(p => p.category === state.currentTab);
|
let params = state.params.filter(p => p.category === state.currentTab);
|
||||||
if (state.paramSearchText) {
|
if (state.paramSearchText) {
|
||||||
params = params.filter(p =>
|
params = state.params.filter(p =>
|
||||||
p.param_key.toLowerCase().includes(state.paramSearchText) ||
|
p.param_key.toLowerCase().includes(state.paramSearchText) ||
|
||||||
(p.long_flag || '').toLowerCase().includes(state.paramSearchText) ||
|
(p.long_flag||'').toLowerCase().includes(state.paramSearchText) ||
|
||||||
(p.short_flag || '').toLowerCase().includes(state.paramSearchText) ||
|
(p.short_flag||'').toLowerCase().includes(state.paramSearchText) ||
|
||||||
(p.description || '').toLowerCase().includes(state.paramSearchText)
|
(p.description||'').toLowerCase().includes(state.paramSearchText)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return params;
|
return params;
|
||||||
@@ -207,121 +174,72 @@ function getFilteredParams() {
|
|||||||
|
|
||||||
function renderParams() {
|
function renderParams() {
|
||||||
const container = document.getElementById('param-container');
|
const container = document.getElementById('param-container');
|
||||||
const allParams = getFilteredParams();
|
const all = getFilteredParams();
|
||||||
|
|
||||||
// When searching, show all matching regardless of important/hidden
|
|
||||||
if (state.paramSearchText) {
|
if (state.paramSearchText) {
|
||||||
container.innerHTML = allParams.map(p => renderParamItem(p)).join('');
|
container.innerHTML = all.map(p => renderParamItem(p)).join('');
|
||||||
document.getElementById('toggle-advanced-btn').style.display = 'none';
|
document.getElementById('toggle-advanced-btn').style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const imp = all.filter(p => p.is_important === 1);
|
||||||
const importantParams = allParams.filter(p => p.is_important === 1);
|
const other = all.filter(p => p.is_important === 0);
|
||||||
const otherParams = allParams.filter(p => p.is_important === 0);
|
let html = imp.map(p => renderParamItem(p)).join('');
|
||||||
|
if (state.showHidden) html += other.map(p => renderParamItem(p)).join('');
|
||||||
let html = importantParams.map(p => renderParamItem(p)).join('');
|
|
||||||
if (state.showHidden) {
|
|
||||||
html += otherParams.map(p => renderParamItem(p)).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
const btn = document.getElementById('toggle-advanced-btn');
|
||||||
const toggleBtn = document.getElementById('toggle-advanced-btn');
|
if (other.length > 0 && !state.paramSearchText) { btn.style.display = 'block'; btn.textContent = state.showHidden ? '▲ 收起更多参数' : '▼ 显示更多参数'; }
|
||||||
if (otherParams.length > 0 && !state.paramSearchText) {
|
else btn.style.display = 'none';
|
||||||
toggleBtn.style.display = 'block';
|
|
||||||
toggleBtn.textContent = state.showHidden ? '▲ 收起更多参数' : '▼ 显示更多参数';
|
|
||||||
} else {
|
|
||||||
toggleBtn.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderParamItem(p) {
|
function renderParamItem(p) {
|
||||||
const val = state.paramValues[p.param_key];
|
const val = state.paramValues[p.param_key];
|
||||||
const defaultVal = p.default_value;
|
const isMod = isParamModified(p, val);
|
||||||
const isModified = isParamModified(p, val);
|
const mc = isMod ? 'modified' : '';
|
||||||
const modifiedClass = isModified ? 'modified' : '';
|
const vb = p.affects_vram ? '<span class="vram-badge">⚡显存</span>' : '';
|
||||||
const vramBadge = p.affects_vram ? '<span class="vram-badge">⚡显存</span>' : '';
|
|
||||||
const flag = p.short_flag || p.long_flag;
|
const flag = p.short_flag || p.long_flag;
|
||||||
|
let inp = '';
|
||||||
let inputHtml = '';
|
|
||||||
if (p.param_type === 'boolean') {
|
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') {
|
} else if (p.param_type === 'select') {
|
||||||
const options = Array.isArray(p.options) ? p.options : [];
|
const opts = Array.isArray(p.options) ? p.options : [];
|
||||||
inputHtml = `<select onchange="setParam('${p.param_key}', this.value)">
|
inp = `<select onchange="setParam('${p.param_key}', this.value)">${opts.map(o => `<option value="${o}" ${String(val)===String(o) ? 'selected':''}>${o}</option>`).join('')}</select>`;
|
||||||
${options.map(o => `<option value="${o}" ${String(val) === String(o) ? 'selected' : ''}>${o}</option>`).join('')}
|
|
||||||
</select>`;
|
|
||||||
} else if (p.param_type === 'number') {
|
} else if (p.param_type === 'number') {
|
||||||
const step = p.step || 'any';
|
const step = p.step || 'any';
|
||||||
const min = p.min_value !== null ? `min="${p.min_value}"` : '';
|
const min = p.min_value !== null ? `min="${p.min_value}"` : '';
|
||||||
const max = p.max_value !== null ? `max="${p.max_value}"` : '';
|
const max = p.max_value !== null ? `max="${p.max_value}"` : '';
|
||||||
inputHtml = `<input type="number" value="${val}" step="${step}" ${min} ${max}
|
inp = `<input type="number" value="${val}" step="${step}" ${min} ${max} onchange="setParam('${p.param_key}', this.value)" data-key="${p.param_key}">`;
|
||||||
onchange="setParam('${p.param_key}', this.value)" data-key="${p.param_key}">`;
|
|
||||||
} else {
|
} 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 uh = p.unit ? `<span class="param-unit">${p.unit}</span>` : '';
|
||||||
const unitHtml = p.unit ? `<span class="param-unit">${p.unit}</span>` : '';
|
return `<div class="param-item ${mc}" data-key="${p.param_key}"><span class="param-flag">${flag}</span>${vb}<span class="param-desc-text">${p.description}</span>${inp}${uh}</div>`;
|
||||||
|
|
||||||
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>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isParamModified(p, val) {
|
function isParamModified(p, val) {
|
||||||
const defaultVal = p.default_value;
|
const dv = p.default_value;
|
||||||
if (p.param_type === 'boolean') {
|
if (p.param_type === 'boolean') return (val===true||val==='true') !== (dv==='true');
|
||||||
return (val === true || val === 'true') !== (defaultVal === 'true');
|
return String(val) !== String(dv);
|
||||||
}
|
|
||||||
return String(val) !== String(defaultVal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setParam(key, value) {
|
function setParam(key, value) {
|
||||||
state.paramValues[key] = value;
|
state.paramValues[key] = value;
|
||||||
// Only update modified visual state, command, and estimate — do NOT re-render
|
|
||||||
const item = document.querySelector(`.param-item[data-key="${key}"]`);
|
const item = document.querySelector(`.param-item[data-key="${key}"]`);
|
||||||
if (item) {
|
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'); }
|
||||||
const p = state.params.find(p => p.param_key === key);
|
generateCommand(); updateEstimate();
|
||||||
if (p && isParamModified(p, value)) {
|
|
||||||
item.classList.add('modified');
|
|
||||||
} else {
|
|
||||||
item.classList.remove('modified');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
generateCommand();
|
|
||||||
updateEstimate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleHiddenParams() {
|
function toggleHiddenParams() { state.showHidden = !state.showHidden; renderParams(); }
|
||||||
state.showHidden = !state.showHidden;
|
|
||||||
renderParams();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== VRAM Estimation =====
|
// ===== VRAM Estimation =====
|
||||||
async function updateEstimate() {
|
async function updateEstimate() {
|
||||||
const params = collectParamsForEstimate();
|
const params = collectParamsForEstimate();
|
||||||
const gpuSelections = state.gpuSlots.map(s => ({ name: s.name, vram_mb: s.vram_mb }));
|
const gpuSel = state.gpuSlots.map(s => ({ name: s.name, vram_mb: s.vram_mb }));
|
||||||
|
const sm = parseFloat(document.getElementById('sys-memory').value) || 0;
|
||||||
const sysMemory = parseFloat(document.getElementById('sys-memory').value) || 0;
|
const smu = document.getElementById('sys-memory-unit').value;
|
||||||
const sysMemUnit = document.getElementById('sys-memory-unit').value;
|
let sysGb = 0; if (smu === '1') sysGb = sm; else if (smu === '2') sysGb = sm / 1024;
|
||||||
let sysMemoryGb = 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 }) });
|
||||||
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 data = await res.json();
|
const data = await res.json();
|
||||||
renderVramDisplay(data);
|
renderVramDisplay(data);
|
||||||
if (state.mode === 'gpu_cpu') renderRamDisplay(data, sysMemoryGb);
|
if (state.mode === 'gpu_cpu') renderRamDisplay(data, sysGb);
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectParamsForEstimate() {
|
function collectParamsForEstimate() {
|
||||||
@@ -333,124 +251,49 @@ function collectParamsForEstimate() {
|
|||||||
params._model_kv_heads = state.selectedModel.kv_heads;
|
params._model_kv_heads = state.selectedModel.kv_heads;
|
||||||
params._model_head_dim = state.selectedModel.head_dim;
|
params._model_head_dim = state.selectedModel.head_dim;
|
||||||
params._model_heads = state.selectedModel.attention_heads;
|
params._model_heads = state.selectedModel.attention_heads;
|
||||||
} else {
|
} 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; }
|
||||||
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;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderVramDisplay(data) {
|
function renderVramDisplay(data) {
|
||||||
const bar = document.getElementById('vram-bar');
|
const bar = document.getElementById('vram-bar'), label = document.getElementById('vram-label'), bd = document.getElementById('vram-breakdown');
|
||||||
const label = document.getElementById('vram-label');
|
if (!data.total_vram_available_mb) { bar.style.width = '0%'; label.textContent = '请选择GPU'; bd.innerHTML = ''; return; }
|
||||||
const breakdown = document.getElementById('vram-breakdown');
|
const pct = data.usage_percent || 0;
|
||||||
|
bar.style.width = Math.min(pct, 100) + '%';
|
||||||
if (!data.total_vram_available_mb || data.total_vram_available_mb === 0) {
|
bar.className = 'vram-bar' + (pct > 90 ? ' danger' : pct > 75 ? ' warning' : '');
|
||||||
bar.style.width = '0%';
|
label.textContent = `VRAM: ${data.total_gb}GB / ${data.total_vram_available_gb}GB (${pct}%)`;
|
||||||
label.textContent = '请选择GPU';
|
bd.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>`;
|
||||||
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>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRamDisplay(data, sysMemoryGb) {
|
function renderRamDisplay(data, sysGb) {
|
||||||
const bar = document.getElementById('ram-bar');
|
const bar = document.getElementById('ram-bar'), label = document.getElementById('ram-label'), bd = document.getElementById('ram-breakdown');
|
||||||
const label = document.getElementById('ram-label');
|
if (sysGb === 0) { bar.style.width = '0%'; label.textContent = `内存: ${data.cpu_total_gb}GB (无上限)`; }
|
||||||
const breakdown = document.getElementById('ram-breakdown');
|
else { const pct = data.cpu_usage_percent || 0; bar.style.width = Math.min(pct, 100) + '%'; bar.className = 'vram-bar ram-bar' + (pct > 90 ? ' danger' : pct > 75 ? ' warning' : ''); label.textContent = `内存: ${data.cpu_total_gb}GB / ${sysGb}GB (${pct}%)`; }
|
||||||
|
bd.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>`;
|
||||||
if (sysMemoryGb === 0) {
|
|
||||||
bar.style.width = '0%';
|
|
||||||
label.textContent = `内存: ${data.cpu_total_gb}GB (无上限)`;
|
|
||||||
} 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}%)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Natural Language =====
|
// ===== Natural Language =====
|
||||||
async function parseNaturalLanguage() {
|
async function parseNaturalLanguage() {
|
||||||
const text = document.getElementById('nl-input').value;
|
const text = document.getElementById('nl-input').value;
|
||||||
if (!text.trim()) return;
|
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();
|
const data = await res.json();
|
||||||
|
let html = '<div>解析结果:</div>';
|
||||||
let resultHtml = '<div>解析结果:</div>';
|
|
||||||
|
|
||||||
if (data._gpu_name) {
|
if (data._gpu_name) {
|
||||||
const gpu = state.gpus.find(g => g.name === data._gpu_name);
|
const g = state.gpus.find(g => g.name === data._gpu_name);
|
||||||
if (gpu) {
|
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._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>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (data._mode) { switchMode(data._mode); html += `<span class="parsed-param"><span class="key">模式:</span> <span class="value">${data._mode}</span></span>`; }
|
||||||
if (data._mode) {
|
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>`; }
|
||||||
switchMode(data._mode);
|
document.getElementById('nl-result').innerHTML = html;
|
||||||
resultHtml += `<span class="parsed-param"><span class="key">模式:</span> <span class="value">${data._mode}</span></span>`;
|
renderParams(); generateCommand(); updateEstimate();
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Generate Command =====
|
// ===== Generate Command =====
|
||||||
async function generateCommand() {
|
async function generateCommand() {
|
||||||
const params = { ...state.paramValues };
|
const params = { ...state.paramValues };
|
||||||
Object.keys(params).forEach(k => { if (k.startsWith('_')) delete params[k]; });
|
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();
|
const data = await res.json();
|
||||||
document.getElementById('command-output').textContent = data.command;
|
document.getElementById('command-output').textContent = data.command;
|
||||||
}
|
}
|
||||||
@@ -458,24 +301,11 @@ async function generateCommand() {
|
|||||||
// ===== Copy =====
|
// ===== Copy =====
|
||||||
function copyCommand() {
|
function copyCommand() {
|
||||||
const text = document.getElementById('command-output').textContent;
|
const text = document.getElementById('command-output').textContent;
|
||||||
// Use textarea fallback for non-HTTPS environments
|
|
||||||
const ta = document.createElement('textarea');
|
const ta = document.createElement('textarea');
|
||||||
ta.value = text;
|
ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px';
|
||||||
ta.style.position = 'fixed';
|
document.body.appendChild(ta); ta.select();
|
||||||
ta.style.left = '-9999px';
|
try { document.execCommand('copy'); const btn = document.querySelector('.btn-copy'); const o = btn.textContent; btn.textContent = '✅ 已复制!'; setTimeout(() => btn.textContent = o, 2000); } catch(e) { alert('复制失败,请手动选择文本复制'); }
|
||||||
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('复制失败,请手动选择文本复制');
|
|
||||||
}
|
|
||||||
document.body.removeChild(ta);
|
document.body.removeChild(ta);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Init =====
|
|
||||||
window.addEventListener('DOMContentLoaded', init);
|
window.addEventListener('DOMContentLoaded', init);
|
||||||
Reference in New Issue
Block a user