diff --git a/app.py b/app.py index 7fe71d2..f1ce2cc 100644 --- a/app.py +++ b/app.py @@ -7,6 +7,7 @@ import json import re import math import functools +import urllib.request from flask import Flask, request, jsonify, send_from_directory, session, redirect from flask_cors import CORS from db import get_db, init_db, DB_PATH @@ -600,16 +601,86 @@ def get_models(): db.close() return jsonify(result) +@app.route('/api/models/grouped') +def get_models_grouped(): + db = get_db() + models = db.execute('SELECT * FROM models ORDER BY base_model, sort_order, name').fetchall() + db.close() + grouped = {} + for m in models: + d = dict(m) + base = d['base_model'] + if base not in grouped: + grouped[base] = [] + grouped[base].append(d) + # Get default quant from settings + db2 = get_db() + dq = db2.execute("SELECT value FROM settings WHERE key = 'default_quant'").fetchone() + db2.close() + default_quant = dq['value'] if dq else 'Q4_K_M' + return jsonify({'models': grouped, 'default_quant': default_quant}) + # ----- Parse Natural Language ----- @app.route('/api/parse-nl', methods=['POST']) def parse_nl(): data = request.json text = data.get('text', '') + # Try LLM API first if enabled + db = get_db() + llm_enabled = db.execute("SELECT value FROM settings WHERE key = 'llm_enabled'").fetchone() + if llm_enabled and llm_enabled['value'] == 'true': + llm_url = db.execute("SELECT value FROM settings WHERE key = 'llm_api_url'").fetchone() + llm_key = db.execute("SELECT value FROM settings WHERE key = 'llm_api_key'").fetchone() + llm_model = db.execute("SELECT value FROM settings WHERE key = 'llm_api_model'").fetchone() + llm_prompt = db.execute("SELECT value FROM settings WHERE key = 'llm_system_prompt'").fetchone() + db.close() + url = llm_url['value'] if llm_url else '' + key = llm_key['value'] if llm_key else '' + model = llm_model['value'] if llm_model else '' + system_prompt = llm_prompt['value'] if llm_prompt else '' + if url: + try: + result = call_llm_for_parsing(url, key, model, system_prompt, text) + if result: + return jsonify(result) + except Exception as e: + print(f'LLM parse failed: {e}', file=sys.stderr) + else: + db.close() + # Fallback to regex parsing result = parse_natural_language(text) return jsonify(result) +def call_llm_for_parsing(url, key, model, system_prompt, user_text): + """Call LLM API to parse natural language into params.""" + headers = {'Content-Type': 'application/json'} + if key: + headers['Authorization'] = f'Bearer {key}' + body = { + 'model': model, + 'messages': [ + {'role': 'system', 'content': system_prompt}, + {'role': 'user', 'content': user_text} + ], + 'temperature': 0.1, + 'max_tokens': 2000, + } + req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method='POST') + with urllib.request.urlopen(req, timeout=30) as resp: + data = json.loads(resp.read().decode('utf-8')) + # OpenAI-compatible response + content = data['choices'][0]['message']['content'] + # Try to extract JSON from the response + content = content.strip() + if content.startswith('```'): + content = re.sub(r'^```\w*\n?', '', content) + content = re.sub(r'\n?```$', '', content) + result = json.loads(content) + return result + + # ==================== Admin API ==================== # All admin routes below require login @@ -800,9 +871,9 @@ def admin_add_model(): data = request.json db = get_db() db.execute( - '''INSERT INTO models (name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, quant, description, sort_order) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', - (data['name'], data['size_gb'], data['layers'], data['embd'], + '''INSERT INTO models (base_model, name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, 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)) ) @@ -818,9 +889,9 @@ def admin_model_edit(mid): if request.method == 'PUT': data = request.json 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=?''', - (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.get('quant', ''), data.get('description', ''), data.get('sort_order', 0), mid) ) diff --git a/db.py b/db.py index 2122509..2a7a0b4 100644 --- a/db.py +++ b/db.py @@ -80,6 +80,7 @@ def init_db(): c.execute(''' CREATE TABLE IF NOT EXISTS models ( id INTEGER PRIMARY KEY AUTOINCREMENT, + base_model TEXT NOT NULL, name TEXT NOT NULL, size_gb REAL NOT NULL, layers INTEGER NOT NULL, @@ -312,44 +313,79 @@ 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) default_models = [ - ("Llama-3-8B-Instruct (Q4_K_M)", 4.9, 32, 4096, 8, 128, 32, "Q4_K_M", "Meta Llama 3 8B Instruct, Q4_K_M 量化", 1), - ("Llama-3-8B-Instruct (Q8_0)", 8.5, 32, 4096, 8, 128, 32, "Q8_0", "Meta Llama 3 8B Instruct, Q8_0 量化", 2), - ("Llama-3-8B-Instruct (FP16)", 15.5, 32, 4096, 8, 128, 32, "FP16", "Meta Llama 3 8B Instruct, FP16", 3), - ("Llama-3-70B-Instruct (Q4_K_M)", 38.5, 80, 8192, 8, 128, 64, "Q4_K_M", "Meta Llama 3 70B Instruct, Q4_K_M 量化", 4), - ("Llama-3-70B-Instruct (Q8_0)", 74.0, 80, 8192, 8, 128, 64, "Q8_0", "Meta Llama 3 70B Instruct, Q8_0 量化", 5), - ("Llama-3-70B-Instruct (FP16)", 138.0, 80, 8192, 8, 128, 64, "FP16", "Meta Llama 3 70B Instruct, FP16", 6), - ("Llama-3.1-8B-Instruct (Q4_K_M)", 4.9, 32, 4096, 8, 128, 32, "Q4_K_M", "Meta Llama 3.1 8B Instruct, Q4_K_M 量化", 7), - ("Llama-3.1-70B-Instruct (Q4_K_M)", 38.5, 80, 8192, 8, 128, 64, "Q4_K_M", "Meta Llama 3.1 70B Instruct, Q4_K_M 量化", 8), - ("Llama-3.1-70B-Instruct (Q8_0)", 74.0, 80, 8192, 8, 128, 64, "Q8_0", "Meta Llama 3.1 70B Instruct, Q8_0 量化", 9), - ("Llama-3.1-405B-Instruct (Q4_K_M)", 226.0, 126, 16384, 8, 128, 128, "Q4_K_M", "Meta Llama 3.1 405B Instruct, Q4_K_M 量化", 10), - ("Qwen2.5-7B-Instruct (Q4_K_M)", 4.7, 28, 3584, 4, 128, 28, "Q4_K_M", "Qwen2.5 7B Instruct, Q4_K_M 量化", 11), - ("Qwen2.5-14B-Instruct (Q4_K_M)", 8.7, 40, 5120, 8, 128, 40, "Q4_K_M", "Qwen2.5 14B Instruct, Q4_K_M 量化", 12), - ("Qwen2.5-32B-Instruct (Q4_K_M)", 19.5, 64, 5120, 8, 128, 64, "Q4_K_M", "Qwen2.5 32B Instruct, Q4_K_M 量化", 13), - ("Qwen2.5-72B-Instruct (Q4_K_M)", 42.0, 80, 8192, 8, 128, 64, "Q4_K_M", "Qwen2.5 72B Instruct, Q4_K_M 量化", 14), - ("Qwen2.5-72B-Instruct (Q8_0)", 75.0, 80, 8192, 8, 128, 64, "Q8_0", "Qwen2.5 72B Instruct, Q8_0 量化", 15), - ("DeepSeek-V2-Chat (Q4_K_M)", 23.0, 60, 5120, 8, 128, 60, "Q4_K_M", "DeepSeek V2 Chat, Q4_K_M 量化", 16), - ("DeepSeek-V2.5-Chat (Q4_K_M)", 23.0, 60, 5120, 8, 128, 60, "Q4_K_M", "DeepSeek V2.5 Chat, Q4_K_M 量化", 17), - ("DeepSeek-R1-Distill-Qwen-32B (Q4_K_M)", 19.5, 64, 5120, 8, 128, 64, "Q4_K_M", "DeepSeek R1 Distill Qwen 32B, Q4_K_M 量化", 18), - ("DeepSeek-R1-Distill-Llama-70B (Q4_K_M)", 42.0, 80, 8192, 8, 128, 64, "Q4_K_M", "DeepSeek R1 Distill Llama 70B, Q4_K_M 量化", 19), - ("Mistral-7B-Instruct-v0.3 (Q4_K_M)", 4.4, 32, 4096, 8, 128, 32, "Q4_K_M", "Mistral 7B Instruct v0.3, Q4_K_M 量化", 20), - ("Mixtral-8x7B-Instruct (Q4_K_M)", 26.0, 32, 4096, 8, 128, 32, "Q4_K_M", "Mixtral 8x7B Instruct, Q4_K_M 量化", 21), - ("Gemma-2-9B-It (Q4_K_M)", 5.4, 42, 3584, 4, 256, 14, "Q4_K_M", "Google Gemma 2 9B It, Q4_K_M 量化", 22), - ("Gemma-2-27B-It (Q4_K_M)", 16.5, 46, 4608, 4, 128, 36, "Q4_K_M", "Google Gemma 2 27B It, Q4_K_M 量化", 23), - ("Phi-3-Mini-4K-Instruct (Q4_K_M)", 2.5, 32, 3072, 32, 96, 32, "Q4_K_M", "Microsoft Phi-3 Mini 4K Instruct, Q4_K_M 量化", 24), - ("Phi-3-Medium-14B-Instruct (Q4_K_M)", 8.4, 40, 5120, 10, 128, 40, "Q4_K_M", "Microsoft Phi-3 Medium 14B Instruct, Q4_K_M 量化", 25), - ("GLM-4-9B-Chat (Q4_K_M)", 5.5, 40, 4096, 4, 128, 40, "Q4_K_M", "Zhipu GLM-4 9B Chat, Q4_K_M 量化", 26), + # Llama-3-8B-Instruct + ("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), ] for m in default_models: - c.execute('''INSERT INTO models (name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, quant, description, sort_order) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', m) + c.execute('''INSERT INTO models (base_model, name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, quant, description, sort_order) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', m) # ===== Default settings ===== c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("admin_password", "admin123")) c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("default_gpu", "RTX 3090")) c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("default_version", "b10068")) c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("default_mode", "gpu")) + c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("default_quant", "Q4_K_M")) + # LLM API settings for natural language parsing + c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("llm_enabled", "false")) + c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("llm_api_url", "")) + c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("llm_api_key", "")) + c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("llm_api_model", "")) + c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("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() diff --git a/static/admin.html b/static/admin.html index 00c51d8..486b870 100644 --- a/static/admin.html +++ b/static/admin.html @@ -123,7 +123,8 @@

添加新模型

- + + @@ -137,7 +138,7 @@
- +
ID名称大小(GB)层数EMBDKVHDHeads量化排序操作
ID基模型名称大小(GB)层数EMBDKVHDHeads量化排序操作
diff --git a/static/css/style.css b/static/css/style.css index 7a94a76..ca1341f 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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 .breakdown-item { display: flex; justify-content: space-between; padding: 4px 8px; background: var(--bg); border-radius: 4px; } -/* ===== Model Selection ===== */ +/* ===== Model Selection (two-step) ===== */ +.model-step { margin-bottom: 12px; } +.model-step.hidden { display: none; } +.model-step-label { display: block; font-size: 0.85em; color: var(--text-dim); margin-bottom: 6px; } .model-select-container { position: relative; } .model-select-container input { width: 100%; @@ -215,8 +218,18 @@ header h1 { .model-name { font-size: 0.9em; } .model-meta { font-size: 0.8em; color: var(--text-dim); } +.quant-btn { + display: inline-flex; flex-direction: column; align-items: center; gap: 2px; + padding: 8px 16px; background: var(--bg-input); border: 1px solid var(--border); + color: var(--text); border-radius: var(--radius); cursor: pointer; font-size: 0.85em; margin-right: 8px; margin-bottom: 8px; transition: all 0.15s; +} +.quant-btn:hover { border-color: var(--accent2); } +.quant-btn.active { background: var(--accent); color: white; border-color: var(--accent); } +.quant-size { font-size: 0.75em; color: var(--text-dim); } +.quant-btn.active .quant-size { color: rgba(255,255,255,0.8); } + .model-selected-info { margin-top: 12px; } -.model-detail { display: flex; gap: 16px; flex-wrap: wrap; font-size: 0.85em; } +.model-detail { display: flex; gap: 12px; flex-wrap: wrap; font-size: 0.85em; } .detail-item { background: var(--bg-input); padding: 4px 10px; border-radius: 4px; } .detail-item b { color: var(--accent2); } @@ -235,7 +248,7 @@ header h1 { .tab-btn.active { background: var(--accent); color: white; border-color: var(--accent); } /* ===== Param Search ===== */ -.param-search-container { margin-bottom: 12px; } +.param-search-container { margin-bottom: 12px; position: relative; } .param-search-container input { width: 100%; background: var(--bg-input); @@ -246,6 +259,14 @@ header h1 { font-size: 0.9em; } .param-search-container input:focus { outline: none; border-color: var(--accent); } +.search-hint { font-size: 0.8em; color: var(--text-dim); margin-left: 8px; } + +/* ===== Settings (wide textarea) ===== */ +.settings-form .setting-item-wide { grid-column: 1 / -1; } +.settings-form textarea { + background: var(--bg-input); border: 1px solid var(--border); color: var(--text); + padding: 8px 12px; border-radius: var(--radius); font-size: 0.85em; resize: vertical; width: 100%; font-family: inherit; +} /* ===== Parameter Items (one per line) ===== */ #param-container { display: flex; flex-direction: column; gap: 6px; } diff --git a/static/index.html b/static/index.html index a5e6936..49b653b 100644 --- a/static/index.html +++ b/static/index.html @@ -39,8 +39,6 @@

🖥️ GPU 配置

- -
@@ -71,13 +69,23 @@
- +

📦 选择模型

-
- -
+ +
+ +
+ +
+
+ + +
@@ -94,7 +102,11 @@

⚙️ 参数配置

-
+
+ + +
+
@@ -103,9 +115,6 @@
-
- -
@@ -120,7 +129,6 @@
- diff --git a/static/js/admin.js b/static/js/admin.js index b421402..50933ae 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -239,7 +239,8 @@ function renderModelTable() { document.getElementById('model-table-body').innerHTML = adminState.models.map(m => ` ${m.id} - + + @@ -254,6 +255,7 @@ function renderModelTable() { async function addModel() { const data = { + base_model: document.getElementById('model-basemodel').value, name: document.getElementById('model-name').value, size_gb: parseFloat(document.getElementById('model-size').value) || 0, layers: parseInt(document.getElementById('model-layers').value) || 0, @@ -265,16 +267,16 @@ async function addModel() { description: document.getElementById('model-desc').value, sort_order: parseInt(document.getElementById('model-order').value) || 0, }; - if (!data.name || !data.size_gb || !data.layers) { alert('请填写模型名称、大小和层数'); return; } + if (!data.base_model || !data.name || !data.size_gb || !data.layers) { alert('请填写基模型、名称、大小和层数'); return; } await fetch('/api/admin/models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); - ['model-name','model-size','model-layers','model-embd','model-kv','model-hdim','model-heads','model-quant','model-desc','model-order'].forEach(id => document.getElementById(id).value = id === 'model-order' ? '0' : ''); + ['model-basemodel','model-name','model-size','model-layers','model-embd','model-kv','model-hdim','model-heads','model-quant','model-desc','model-order'].forEach(id => document.getElementById(id).value = id === 'model-order' ? '0' : ''); await loadAdminModels(); } async function updateModel(id, field, value) { const m = adminState.models.find(m => m.id === id); if (!m) return; - const data = { name: m.name, size_gb: m.size_gb, layers: m.layers, embd: m.embd, kv_heads: m.kv_heads, head_dim: m.head_dim, attention_heads: m.attention_heads, quant: m.quant, description: m.description, sort_order: m.sort_order }; + const data = { base_model: m.base_model, name: m.name, size_gb: m.size_gb, layers: m.layers, embd: m.embd, kv_heads: m.kv_heads, head_dim: m.head_dim, attention_heads: m.attention_heads, 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 data[field] = value; @@ -297,11 +299,27 @@ async function loadAdminSettings() { } function renderSettings() { - document.getElementById('settings-form').innerHTML = Object.entries(adminState.settings).map(([key, value]) => ` -
- - -
`).join(''); + const labels = { + 'admin_password': '管理密码', + 'default_gpu': '默认GPU', + 'default_version': '默认版本', + '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 `
`; + } + const type = key === 'llm_api_key' ? 'password' : 'text'; + return `
`; + }).join(''); } async function saveSettings() { diff --git a/static/js/main.js b/static/js/main.js index 9fac5e7..d7b280f 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,59 +1,42 @@ // ===== State ===== let state = { - versions: [], - gpus: [], - models: [], - filteredModels: [], - selectedModel: null, - currentVersionId: null, - params: [], - paramValues: {}, - mode: 'gpu', - gpuSlots: [], - showHidden: false, - currentTab: 'common', - paramSearchText: '', + versions: [], gpus: [], models: [], modelsGrouped: {}, + baseModelList: [], filteredBaseModels: [], + currentVersionId: null, params: [], paramValues: {}, + mode: 'gpu', gpuSlots: [], showHidden: false, + currentTab: 'common', paramSearchText: '', + selectedModel: null, defaultQuant: 'Q4_K_M', }; // ===== Init ===== async function init() { await loadVersions(); await loadGpus(); - await loadModels(); - const defaultVer = state.versions.find(v => v.version_tag === 'b10068') || state.versions[0]; - if (defaultVer) { - state.currentVersionId = defaultVer.id; - document.getElementById('version-select').value = defaultVer.id; - await loadParams(defaultVer.id); - } - const defaultGpu = state.gpus.find(g => g.name === 'RTX 3090') || state.gpus[0]; - if (defaultGpu) { - state.gpuSlots = [{ ...defaultGpu }]; - renderGpuSlots(); - } - renderParams(); - generateCommand(); - updateEstimate(); + await loadModelsGrouped(); + const dv = state.versions.find(v => v.version_tag === 'b10068') || state.versions[0]; + if (dv) { state.currentVersionId = dv.id; document.getElementById('version-select').value = dv.id; await loadParams(dv.id); } + const dg = state.gpus.find(g => g.name === 'RTX 3090') || state.gpus[0]; + if (dg) { state.gpuSlots = [{ ...dg }]; renderGpuSlots(); } + renderParams(); generateCommand(); updateEstimate(); } // ===== Load Data ===== async function loadVersions() { - const res = await fetch('/api/versions'); - state.versions = await res.json(); - const sel = document.getElementById('version-select'); - sel.innerHTML = state.versions.map(v => ``).join(''); + const res = await fetch('/api/versions'); state.versions = await res.json(); + document.getElementById('version-select').innerHTML = state.versions.map(v => ``).join(''); } +async function loadGpus() { const res = await fetch('/api/gpus'); state.gpus = await res.json(); } -async function loadGpus() { - const res = await fetch('/api/gpus'); - state.gpus = await res.json(); -} - -async function loadModels() { - const res = await fetch('/api/models'); - state.models = await res.json(); - state.filteredModels = state.models; - renderModelDropdown(); +async function loadModelsGrouped() { + const res = await fetch('/api/models/grouped'); + const data = await res.json(); + state.modelsGrouped = data.models || {}; + state.defaultQuant = data.default_quant || 'Q4_K_M'; + state.baseModelList = Object.keys(state.modelsGrouped).sort(); + state.filteredBaseModels = state.baseModelList; + // Also flat list for backward compat + state.models = Object.values(state.modelsGrouped).flat(); + renderBaseModelDropdown(); } async function loadParams(versionId) { @@ -61,98 +44,67 @@ async function loadParams(versionId) { state.params = await res.json(); state.paramValues = {}; state.params.forEach(p => { - if (p.param_type === 'boolean') { - state.paramValues[p.param_key] = p.default_value === 'true'; - } else { - state.paramValues[p.param_key] = p.default_value || ''; - } + state.paramValues[p.param_key] = p.param_type === 'boolean' ? (p.default_value === 'true') : (p.default_value || ''); }); - renderParams(); - generateCommand(); - updateEstimate(); + renderParams(); generateCommand(); updateEstimate(); } // ===== Version Change ===== -async function onVersionChange() { - const sel = document.getElementById('version-select'); - state.currentVersionId = parseInt(sel.value); - await loadParams(state.currentVersionId); -} +async function onVersionChange() { state.currentVersionId = parseInt(document.getElementById('version-select').value); await loadParams(state.currentVersionId); } // ===== Mode Switch ===== function switchMode(mode) { state.mode = mode; - document.querySelectorAll('.mode-btn').forEach(btn => { - btn.classList.toggle('active', btn.dataset.mode === mode); - }); + document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.mode === mode)); document.getElementById('memory-panel').classList.toggle('hidden', mode !== 'gpu_cpu'); - generateCommand(); - updateEstimate(); + generateCommand(); updateEstimate(); } // ===== GPU Slots ===== function renderGpuSlots() { - const container = document.getElementById('gpu-slots'); - container.innerHTML = state.gpuSlots.map((gpu, i) => { - const options = state.gpus.map(g => - `` - ).join(''); - return ` -
- GPU ${i + 1} - - 显存: ${(gpu.vram_mb / 1024).toFixed(1)} GB - ${state.gpuSlots.length > 1 ? `` : ''} -
- `; + document.getElementById('gpu-slots').innerHTML = state.gpuSlots.map((gpu, i) => { + const opts = state.gpus.map(g => ``).join(''); + return `
GPU ${i+1}显存: ${(gpu.vram_mb/1024).toFixed(1)} GB${state.gpuSlots.length > 1 ? `` : ''}
`; }).join(''); document.getElementById('btn-add-gpu').style.display = state.gpuSlots.length >= 4 ? 'none' : 'block'; 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() { - if (state.gpuSlots.length >= 4) return; - const defaultGpu = state.gpus.find(g => g.name === 'RTX 3090') || state.gpus[0]; - state.gpuSlots.push({ ...defaultGpu }); - renderGpuSlots(); - generateCommand(); -} - -function removeGpuSlot(index) { - state.gpuSlots.splice(index, 1); - renderGpuSlots(); - generateCommand(); -} - -function updateGpuSlot(index, name) { - const gpu = state.gpus.find(g => g.name === name); - if (gpu) state.gpuSlots[index] = { ...gpu }; - renderGpuSlots(); - generateCommand(); -} - -// ===== Model Selection ===== -function filterModels() { +// ===== Model Selection (two-step) ===== +function filterBaseModels() { const text = document.getElementById('model-search').value.toLowerCase(); - state.filteredModels = state.models.filter(m => - m.name.toLowerCase().includes(text) || - (m.quant || '').toLowerCase().includes(text) || - (m.description || '').toLowerCase().includes(text) - ); - renderModelDropdown(); - document.getElementById('model-dropdown').style.display = state.filteredModels.length > 0 ? 'block' : 'none'; + state.filteredBaseModels = state.baseModelList.filter(n => n.toLowerCase().includes(text)); + renderBaseModelDropdown(); + document.getElementById('model-dropdown').style.display = state.filteredBaseModels.length > 0 ? 'block' : 'none'; } -function renderModelDropdown() { - const dd = document.getElementById('model-dropdown'); - dd.innerHTML = state.filteredModels.map(m => ` -
- ${m.name} - ${m.size_gb}GB | ${m.layers}L | ${m.quant || ''} -
+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 `
${n}${quantStr}
`; + }).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); +} + +function renderQuantOptions(baseName, variants, selectedId) { + document.getElementById('quant-options').innerHTML = variants.map(v => ` + `).join(''); } @@ -160,19 +112,20 @@ function selectModel(id) { const m = state.models.find(m => m.id === id); if (!m) return; state.selectedModel = m; - document.getElementById('model-search').value = m.name; - document.getElementById('model-dropdown').style.display = 'none'; + // Update quant buttons active state + const variants = state.modelsGrouped[m.base_model] || []; + renderQuantOptions(m.base_model, variants, id); + // Show model detail document.getElementById('model-selected-info').innerHTML = `
+ 模型: ${m.name} 大小: ${m.size_gb} GB 层数: ${m.layers} - 嵌入维度: ${m.embd} + 嵌入: ${m.embd} KV Heads: ${m.kv_heads} Head Dim: ${m.head_dim} - Attention Heads: ${m.attention_heads} 量化: ${m.quant || 'N/A'} -
- `; + `; updateEstimate(); } @@ -181,25 +134,39 @@ function switchTab(cat) { state.currentTab = cat; state.paramSearchText = ''; document.getElementById('param-search').value = ''; - document.querySelectorAll('.tab-btn').forEach(btn => { - btn.classList.toggle('active', btn.dataset.cat === cat); - }); + document.getElementById('search-hint').textContent = ''; + document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.cat === cat)); + document.getElementById('param-tabs').style.display = ''; renderParams(); } function onParamSearch() { state.paramSearchText = document.getElementById('param-search').value.toLowerCase(); + const hint = document.getElementById('search-hint'); + if (state.paramSearchText) { + const count = state.params.filter(p => + p.param_key.toLowerCase().includes(state.paramSearchText) || + (p.long_flag||'').toLowerCase().includes(state.paramSearchText) || + (p.short_flag||'').toLowerCase().includes(state.paramSearchText) || + (p.description||'').toLowerCase().includes(state.paramSearchText) + ).length; + hint.textContent = `找到 ${count} 个匹配参数`; + document.getElementById('param-tabs').style.display = 'none'; + } else { + hint.textContent = ''; + document.getElementById('param-tabs').style.display = ''; + } renderParams(); } function getFilteredParams() { let params = state.params.filter(p => p.category === state.currentTab); if (state.paramSearchText) { - params = params.filter(p => + params = state.params.filter(p => p.param_key.toLowerCase().includes(state.paramSearchText) || - (p.long_flag || '').toLowerCase().includes(state.paramSearchText) || - (p.short_flag || '').toLowerCase().includes(state.paramSearchText) || - (p.description || '').toLowerCase().includes(state.paramSearchText) + (p.long_flag||'').toLowerCase().includes(state.paramSearchText) || + (p.short_flag||'').toLowerCase().includes(state.paramSearchText) || + (p.description||'').toLowerCase().includes(state.paramSearchText) ); } return params; @@ -207,121 +174,72 @@ function getFilteredParams() { function renderParams() { const container = document.getElementById('param-container'); - const allParams = getFilteredParams(); - - // When searching, show all matching regardless of important/hidden + const all = getFilteredParams(); 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'; return; } - - const importantParams = allParams.filter(p => p.is_important === 1); - const otherParams = allParams.filter(p => p.is_important === 0); - - let html = importantParams.map(p => renderParamItem(p)).join(''); - if (state.showHidden) { - html += otherParams.map(p => renderParamItem(p)).join(''); - } - + const imp = all.filter(p => p.is_important === 1); + const other = all.filter(p => p.is_important === 0); + let html = imp.map(p => renderParamItem(p)).join(''); + if (state.showHidden) html += other.map(p => renderParamItem(p)).join(''); container.innerHTML = html; - - const toggleBtn = document.getElementById('toggle-advanced-btn'); - if (otherParams.length > 0 && !state.paramSearchText) { - toggleBtn.style.display = 'block'; - toggleBtn.textContent = state.showHidden ? '▲ 收起更多参数' : '▼ 显示更多参数'; - } else { - toggleBtn.style.display = 'none'; - } + const btn = document.getElementById('toggle-advanced-btn'); + if (other.length > 0 && !state.paramSearchText) { btn.style.display = 'block'; btn.textContent = state.showHidden ? '▲ 收起更多参数' : '▼ 显示更多参数'; } + else btn.style.display = 'none'; } function renderParamItem(p) { const val = state.paramValues[p.param_key]; - const defaultVal = p.default_value; - const isModified = isParamModified(p, val); - const modifiedClass = isModified ? 'modified' : ''; - const vramBadge = p.affects_vram ? '⚡显存' : ''; + const isMod = isParamModified(p, val); + const mc = isMod ? 'modified' : ''; + const vb = p.affects_vram ? '⚡显存' : ''; const flag = p.short_flag || p.long_flag; - - let inputHtml = ''; + let inp = ''; if (p.param_type === 'boolean') { - inputHtml = ``; + inp = ``; } else if (p.param_type === 'select') { - const options = Array.isArray(p.options) ? p.options : []; - inputHtml = ``; + const opts = Array.isArray(p.options) ? p.options : []; + inp = ``; } else if (p.param_type === 'number') { const step = p.step || 'any'; const min = p.min_value !== null ? `min="${p.min_value}"` : ''; const max = p.max_value !== null ? `max="${p.max_value}"` : ''; - inputHtml = ``; + inp = ``; } else { - inputHtml = ``; + inp = ``; } - - const unitHtml = p.unit ? `${p.unit}` : ''; - - return ` -
- ${flag} - ${vramBadge} - ${p.description} - ${inputHtml} - ${unitHtml} -
- `; + const uh = p.unit ? `${p.unit}` : ''; + return `
${flag}${vb}${p.description}${inp}${uh}
`; } function isParamModified(p, val) { - const defaultVal = p.default_value; - if (p.param_type === 'boolean') { - return (val === true || val === 'true') !== (defaultVal === 'true'); - } - return String(val) !== String(defaultVal); + const dv = p.default_value; + if (p.param_type === 'boolean') return (val===true||val==='true') !== (dv==='true'); + return String(val) !== String(dv); } function setParam(key, value) { state.paramValues[key] = value; - // Only update modified visual state, command, and estimate — do NOT re-render 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'); - } - } - generateCommand(); - updateEstimate(); + 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'); } + generateCommand(); updateEstimate(); } -function toggleHiddenParams() { - state.showHidden = !state.showHidden; - renderParams(); -} +function toggleHiddenParams() { state.showHidden = !state.showHidden; renderParams(); } // ===== VRAM Estimation ===== async function updateEstimate() { const params = collectParamsForEstimate(); - const gpuSelections = state.gpuSlots.map(s => ({ name: s.name, vram_mb: s.vram_mb })); - - const sysMemory = parseFloat(document.getElementById('sys-memory').value) || 0; - const sysMemUnit = document.getElementById('sys-memory-unit').value; - let sysMemoryGb = 0; - if (sysMemUnit === '1') sysMemoryGb = sysMemory; - else if (sysMemUnit === '2') sysMemoryGb = sysMemory / 1024; - - const res = await fetch('/api/estimate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ params, gpu_selections: gpuSelections, mode: state.mode, system_memory_gb: sysMemoryGb }) - }); + const gpuSel = state.gpuSlots.map(s => ({ name: s.name, vram_mb: s.vram_mb })); + const sm = parseFloat(document.getElementById('sys-memory').value) || 0; + const smu = document.getElementById('sys-memory-unit').value; + let sysGb = 0; if (smu === '1') sysGb = sm; else if (smu === '2') sysGb = sm / 1024; + 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(); renderVramDisplay(data); - if (state.mode === 'gpu_cpu') renderRamDisplay(data, sysMemoryGb); + if (state.mode === 'gpu_cpu') renderRamDisplay(data, sysGb); } function collectParamsForEstimate() { @@ -333,124 +251,49 @@ function collectParamsForEstimate() { params._model_kv_heads = state.selectedModel.kv_heads; params._model_head_dim = state.selectedModel.head_dim; params._model_heads = state.selectedModel.attention_heads; - } else { - params._model_size_gb = 0; - params._model_layers = 0; - params._model_embd = 0; - params._model_kv_heads = 0; - params._model_head_dim = 0; - params._model_heads = 0; - } + } else { params._model_size_gb = 0; params._model_layers = 0; params._model_embd = 0; params._model_kv_heads = 0; params._model_head_dim = 0; params._model_heads = 0; } return params; } function renderVramDisplay(data) { - const bar = document.getElementById('vram-bar'); - const label = document.getElementById('vram-label'); - const breakdown = document.getElementById('vram-breakdown'); - - if (!data.total_vram_available_mb || data.total_vram_available_mb === 0) { - bar.style.width = '0%'; - label.textContent = '请选择GPU'; - breakdown.innerHTML = ''; - return; - } - - const percent = data.usage_percent || 0; - bar.style.width = Math.min(percent, 100) + '%'; - bar.className = 'vram-bar' + (percent > 90 ? ' danger' : percent > 75 ? ' warning' : ''); - label.textContent = `VRAM: ${data.total_gb}GB / ${data.total_vram_available_gb}GB (${percent}%)`; - - breakdown.innerHTML = ` -
模型权重${data.weights_gb}GB
-
KV缓存${data.kv_cache_gb}GB
-
计算/开销${(data.compute_mb / 1024).toFixed(2)}GB
-
CUDA开销${(data.cuda_overhead_mb / 1024).toFixed(2)}GB
- `; + 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 = `
模型权重${data.weights_gb}GB
KV缓存${data.kv_cache_gb}GB
计算/开销${(data.compute_mb/1024).toFixed(2)}GB
CUDA开销${(data.cuda_overhead_mb/1024).toFixed(2)}GB
`; } -function renderRamDisplay(data, sysMemoryGb) { - const bar = document.getElementById('ram-bar'); - const label = document.getElementById('ram-label'); - const breakdown = document.getElementById('ram-breakdown'); - - 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 = ` -
CPU模型权重${data.cpu_weights_gb}GB
-
CPU KV缓存${(data.cpu_kv_cache_mb / 1024).toFixed(2)}GB
- `; +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 = `
CPU模型权重${data.cpu_weights_gb}GB
CPU KV缓存${(data.cpu_kv_cache_mb/1024).toFixed(2)}GB
`; } // ===== Natural Language ===== async function parseNaturalLanguage() { const text = document.getElementById('nl-input').value; if (!text.trim()) return; - - const res = await fetch('/api/parse-nl', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text }) - }); + const res = await fetch('/api/parse-nl', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) }); const data = await res.json(); - - let resultHtml = '
解析结果:
'; - + let html = '
解析结果:
'; if (data._gpu_name) { - const gpu = state.gpus.find(g => g.name === data._gpu_name); - if (gpu) { - if (data._gpu_count && data._gpu_count > 1) { - state.gpuSlots = []; - for (let i = 0; i < Math.min(data._gpu_count, 4); i++) state.gpuSlots.push({ ...gpu }); - } else { - state.gpuSlots = [{ ...gpu }]; - } - renderGpuSlots(); - resultHtml += `GPU: ${data._gpu_name}${data._gpu_count > 1 ? ' x' + data._gpu_count : ''}`; - } + const g = state.gpus.find(g => g.name === data._gpu_name); + if (g) { const cnt = data._gpu_count > 1 ? Math.min(data._gpu_count, 4) : 1; state.gpuSlots = []; for (let i = 0; i < cnt; i++) state.gpuSlots.push({ ...g }); renderGpuSlots(); html += `GPU: ${data._gpu_name}${cnt > 1 ? ' x'+cnt : ''}`; } } - - if (data._mode) { - switchMode(data._mode); - resultHtml += `模式: ${data._mode}`; - } - - for (const [key, value] of Object.entries(data)) { - if (key.startsWith('_')) continue; - state.paramValues[key] = value; - resultHtml += `${key}: ${value}`; - } - - document.getElementById('nl-result').innerHTML = resultHtml; - renderParams(); - generateCommand(); - updateEstimate(); + if (data._mode) { switchMode(data._mode); html += `模式: ${data._mode}`; } + for (const [k, v] of Object.entries(data)) { if (k.startsWith('_')) continue; state.paramValues[k] = v; html += `${k}: ${v}`; } + document.getElementById('nl-result').innerHTML = html; + renderParams(); generateCommand(); updateEstimate(); } // ===== Generate Command ===== async function generateCommand() { const params = { ...state.paramValues }; Object.keys(params).forEach(k => { if (k.startsWith('_')) delete params[k]; }); - - const res = await fetch('/api/generate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - version_id: state.currentVersionId, - params, - mode: state.mode, - gpu_selections: state.gpuSlots, - binary: document.getElementById('binary-select').value, - }) - }); + const res = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version_id: state.currentVersionId, params, mode: state.mode, gpu_selections: state.gpuSlots, binary: document.getElementById('binary-select').value }) }); const data = await res.json(); document.getElementById('command-output').textContent = data.command; } @@ -458,24 +301,11 @@ async function generateCommand() { // ===== Copy ===== function copyCommand() { const text = document.getElementById('command-output').textContent; - // Use textarea fallback for non-HTTPS environments const ta = document.createElement('textarea'); - ta.value = text; - ta.style.position = 'fixed'; - ta.style.left = '-9999px'; - document.body.appendChild(ta); - ta.select(); - try { - document.execCommand('copy'); - const btn = document.querySelector('.btn-copy'); - const orig = btn.textContent; - btn.textContent = '✅ 已复制!'; - setTimeout(() => btn.textContent = orig, 2000); - } catch(e) { - alert('复制失败,请手动选择文本复制'); - } + 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('复制失败,请手动选择文本复制'); } document.body.removeChild(ta); } -// ===== Init ===== window.addEventListener('DOMContentLoaded', init);