Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26d7ee0b66 | ||
|
|
fb3d7290bf | ||
|
|
3c27a205e5 |
@@ -621,6 +621,15 @@ def get_models_grouped():
|
||||
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():
|
||||
@@ -871,11 +880,11 @@ def admin_add_model():
|
||||
data = request.json
|
||||
db = get_db()
|
||||
db.execute(
|
||||
'''INSERT INTO models (base_model, name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, quant, description, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
|
||||
'''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()
|
||||
@@ -890,10 +899,10 @@ def admin_model_edit(mid):
|
||||
data = request.json
|
||||
db.execute(
|
||||
'''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=?, 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()
|
||||
|
||||
@@ -88,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
|
||||
@@ -313,66 +314,66 @@ def insert_default_data(conn):
|
||||
(v2_id,) + p)
|
||||
|
||||
# ===== Default models =====
|
||||
# (base_model, name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, quant, description, sort_order)
|
||||
# (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
|
||||
("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", "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 (FP16)", 15.5, 32, 4096, 8, 128, 32, "FP16", "Meta Llama 3 8B Instruct, FP16", 3),
|
||||
# Llama-3-70B-Instruct
|
||||
("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-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-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-8B-Instruct
|
||||
("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),
|
||||
("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),
|
||||
# Llama-3.1-70B-Instruct
|
||||
("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),
|
||||
("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),
|
||||
# Llama-3.1-405B-Instruct
|
||||
("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),
|
||||
# Qwen2.5-7B-Instruct
|
||||
("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),
|
||||
("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),
|
||||
# Qwen2.5-14B-Instruct
|
||||
("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),
|
||||
# Qwen2.5-32B-Instruct
|
||||
("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),
|
||||
("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),
|
||||
# Qwen2.5-72B-Instruct
|
||||
("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),
|
||||
# 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 (base_model, 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"))
|
||||
@@ -385,7 +386,7 @@ def insert_default_data(conn):
|
||||
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,不要其他文本。"))
|
||||
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()
|
||||
|
||||
|
||||
+2
-1
@@ -131,6 +131,7 @@
|
||||
<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">
|
||||
@@ -138,7 +139,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<table class="admin-table">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
+124
-13
@@ -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 {
|
||||
@@ -152,7 +248,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 +262,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); }
|
||||
@@ -234,9 +331,6 @@ header h1 {
|
||||
.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); }
|
||||
@@ -246,6 +340,8 @@ 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; position: relative; }
|
||||
@@ -282,7 +378,9 @@ 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.modified { border-color: var(--accent2); }
|
||||
|
||||
/* keep old .modified for backwards compat but remove red styling */
|
||||
|
||||
.param-item .param-flag {
|
||||
font-family: monospace;
|
||||
@@ -341,12 +439,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; }
|
||||
|
||||
+41
-28
@@ -12,6 +12,24 @@
|
||||
<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">
|
||||
@@ -34,11 +52,13 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- GPU Configuration -->
|
||||
<!-- GPU Configuration + System Memory (merged) -->
|
||||
<section class="panel" id="gpu-panel">
|
||||
<h2>🖥️ GPU 配置</h2>
|
||||
<div id="gpu-slots"></div>
|
||||
<button class="btn-add" id="btn-add-gpu" onclick="addGpuSlot()">+ 添加显卡</button>
|
||||
|
||||
<!-- VRAM Display -->
|
||||
<div class="vram-display" id="vram-display">
|
||||
<div class="vram-bar-container">
|
||||
<div class="vram-bar" id="vram-bar"></div>
|
||||
@@ -46,46 +66,38 @@
|
||||
</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 (two-step) -->
|
||||
<section class="panel">
|
||||
<h2>📦 选择模型</h2>
|
||||
<!-- Step 1: Select base model -->
|
||||
<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()" autocomplete="off" onfocus="this.select()">
|
||||
<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>
|
||||
<!-- 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>
|
||||
</section>
|
||||
|
||||
@@ -93,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>
|
||||
@@ -114,6 +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>
|
||||
<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>
|
||||
@@ -122,11 +135,11 @@
|
||||
<!-- 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>
|
||||
|
||||
+14
-11
@@ -241,14 +241,15 @@ function renderModelTable() {
|
||||
<td>${m.id}</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.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="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('');
|
||||
}
|
||||
@@ -263,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.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-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' : '');
|
||||
['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 = { 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 };
|
||||
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();
|
||||
@@ -305,6 +307,7 @@ function renderSettings() {
|
||||
'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',
|
||||
|
||||
+166
-44
@@ -6,6 +6,7 @@ let state = {
|
||||
mode: 'gpu', gpuSlots: [], showHidden: false,
|
||||
currentTab: 'common', paramSearchText: '',
|
||||
selectedModel: null, defaultQuant: 'Q4_K_M',
|
||||
lastEstimate: null,
|
||||
};
|
||||
|
||||
// ===== Init =====
|
||||
@@ -13,6 +14,7 @@ async function init() {
|
||||
await loadVersions();
|
||||
await loadGpus();
|
||||
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];
|
||||
@@ -20,6 +22,15 @@ async function init() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Load Data =====
|
||||
async function loadVersions() {
|
||||
const res = await fetch('/api/versions'); state.versions = await res.json();
|
||||
@@ -34,7 +45,6 @@ async function loadModelsGrouped() {
|
||||
state.defaultQuant = data.default_quant || 'Q4_K_M';
|
||||
state.baseModelList = Object.keys(state.modelsGrouped).sort();
|
||||
state.filteredBaseModels = state.baseModelList;
|
||||
// Also flat list for backward compat
|
||||
state.models = Object.values(state.modelsGrouped).flat();
|
||||
renderBaseModelDropdown();
|
||||
}
|
||||
@@ -49,14 +59,14 @@ async function loadParams(versionId) {
|
||||
renderParams(); generateCommand(); updateEstimate();
|
||||
}
|
||||
|
||||
// ===== Version Change =====
|
||||
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');
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -73,49 +83,56 @@ function addGpuSlot() { if (state.gpuSlots.length >= 4) return; const dg = state
|
||||
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(); }
|
||||
|
||||
// ===== Model Selection (two-step) =====
|
||||
// ===== Model Selection =====
|
||||
function filterBaseModels() {
|
||||
const text = document.getElementById('model-search').value.toLowerCase();
|
||||
state.filteredBaseModels = state.baseModelList.filter(n => n.toLowerCase().includes(text));
|
||||
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 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 quantStr = 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">${quantStr}</span></div>`;
|
||||
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;
|
||||
// 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);
|
||||
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;
|
||||
// Update quant buttons active state
|
||||
const variants = state.modelsGrouped[m.base_model] || [];
|
||||
renderQuantOptions(m.base_model, variants, id);
|
||||
// Show model detail
|
||||
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>
|
||||
@@ -126,9 +143,25 @@ function selectModel(id) {
|
||||
<span class="detail-item"><b>Head Dim:</b> ${m.head_dim}</span>
|
||||
<span class="detail-item"><b>量化:</b> ${m.quant || 'N/A'}</span>
|
||||
</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 'all' for GPU mode
|
||||
if (state.mode === 'gpu') {
|
||||
state.paramValues['n_gpu_layers'] = 'all';
|
||||
}
|
||||
// Update param UI if currently visible
|
||||
renderParams();
|
||||
generateCommand();
|
||||
}
|
||||
|
||||
// ===== Parameter Rendering =====
|
||||
function switchTab(cat) {
|
||||
state.currentTab = cat;
|
||||
@@ -160,6 +193,10 @@ function onParamSearch() {
|
||||
}
|
||||
|
||||
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 = state.params.filter(p =>
|
||||
@@ -175,7 +212,8 @@ function getFilteredParams() {
|
||||
function renderParams() {
|
||||
const container = document.getElementById('param-container');
|
||||
const all = getFilteredParams();
|
||||
if (state.paramSearchText) {
|
||||
// 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;
|
||||
@@ -192,10 +230,17 @@ function renderParams() {
|
||||
|
||||
function renderParamItem(p) {
|
||||
const val = state.paramValues[p.param_key];
|
||||
const isMod = isParamModified(p, val);
|
||||
const mc = isMod ? 'modified' : '';
|
||||
const mc = '';
|
||||
const vb = p.affects_vram ? '<span class="vram-badge">⚡显存</span>' : '';
|
||||
const flag = p.short_flag || p.long_flag;
|
||||
// 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') {
|
||||
inp = `<input type="checkbox" ${val ? 'checked' : ''} onchange="setParam('${p.param_key}', this.checked)">`;
|
||||
@@ -211,7 +256,7 @@ function renderParamItem(p) {
|
||||
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>` : '';
|
||||
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 ${mc}" data-key="${p.param_key}">${flagHtml}${vb}<span class="param-desc-text">${p.description}</span>${inp}${uh}</div>`;
|
||||
}
|
||||
|
||||
function isParamModified(p, val) {
|
||||
@@ -222,8 +267,8 @@ function isParamModified(p, val) {
|
||||
|
||||
function setParam(key, value) {
|
||||
state.paramValues[key] = value;
|
||||
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 on 'modified' tab, re-render to update the list
|
||||
if (state.currentTab === 'modified') renderParams();
|
||||
generateCommand(); updateEstimate();
|
||||
}
|
||||
|
||||
@@ -234,12 +279,16 @@ async function updateEstimate() {
|
||||
const params = collectParamsForEstimate();
|
||||
const gpuSel = state.gpuSlots.map(s => ({ name: s.name, vram_mb: s.vram_mb }));
|
||||
const sm = parseFloat(document.getElementById('sys-memory').value) || 0;
|
||||
const smu = document.getElementById('sys-memory-unit').value;
|
||||
let sysGb = 0; if (smu === '1') sysGb = sm; else if (smu === '2') sysGb = sm / 1024;
|
||||
// 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, sysGb);
|
||||
if (state.mode === 'gpu_cpu') renderRamDisplay(data, sysGb, isUnlimited);
|
||||
updateStickyBar(data, sysGb, isUnlimited);
|
||||
generateCommandHint(data, sysGb, isUnlimited);
|
||||
}
|
||||
|
||||
function collectParamsForEstimate() {
|
||||
@@ -256,20 +305,87 @@ function collectParamsForEstimate() {
|
||||
}
|
||||
|
||||
function renderVramDisplay(data) {
|
||||
const bar = document.getElementById('vram-bar'), label = document.getElementById('vram-label'), bd = document.getElementById('vram-breakdown');
|
||||
if (!data.total_vram_available_mb) { bar.style.width = '0%'; label.textContent = '请选择GPU'; bd.innerHTML = ''; return; }
|
||||
const pct = data.usage_percent || 0;
|
||||
bar.style.width = Math.min(pct, 100) + '%';
|
||||
bar.className = 'vram-bar' + (pct > 90 ? ' danger' : pct > 75 ? ' warning' : '');
|
||||
label.textContent = `VRAM: ${data.total_gb}GB / ${data.total_vram_available_gb}GB (${pct}%)`;
|
||||
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>`;
|
||||
// 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, sysGb) {
|
||||
const bar = document.getElementById('ram-bar'), label = document.getElementById('ram-label'), bd = document.getElementById('ram-breakdown');
|
||||
if (sysGb === 0) { bar.style.width = '0%'; label.textContent = `内存: ${data.cpu_total_gb}GB (无上限)`; }
|
||||
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>`;
|
||||
function renderRamDisplay(data, sysGb, isUnlimited) {
|
||||
// Update sticky ram bar instead of inline display
|
||||
// (sticky bar is already updated in updateStickyBar)
|
||||
}
|
||||
|
||||
// ===== 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 {
|
||||
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}%。` });
|
||||
}
|
||||
|
||||
// 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 =====
|
||||
@@ -304,7 +420,13 @@ function copyCommand() {
|
||||
const ta = document.createElement('textarea');
|
||||
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 o = btn.textContent; btn.textContent = '✅ 已复制!'; setTimeout(() => btn.textContent = o, 2000); } catch(e) { alert('复制失败,请手动选择文本复制'); }
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user