Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f37952c3f6 | ||
|
|
5169d368bf | ||
|
|
c12c2240a2 |
@@ -626,8 +626,12 @@ def get_models_grouped():
|
||||
def get_public_settings():
|
||||
db = get_db()
|
||||
nl = db.execute("SELECT value FROM settings WHERE key = 'nl_default_text'").fetchone()
|
||||
snl = db.execute("SELECT value FROM settings WHERE key = 'show_nl_section'").fetchone()
|
||||
db.close()
|
||||
return jsonify({'nl_default_text': nl['value'] if nl else ''})
|
||||
return jsonify({
|
||||
'nl_default_text': nl['value'] if nl else '',
|
||||
'show_nl_section': snl['value'] if snl else 'true',
|
||||
})
|
||||
|
||||
|
||||
# ----- Parse Natural Language -----
|
||||
|
||||
@@ -387,6 +387,7 @@ def insert_default_data(conn):
|
||||
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("llm_api_key", ""))
|
||||
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("llm_api_model", ""))
|
||||
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("nl_default_text", "用自然语言描述你想要的配置,支持多行输入。\n例如:\n用RTX 4090跑Llama-3-70B,上下文8192\n温度0.7,开启flash attention\n端口设为8080"))
|
||||
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("show_nl_section", "true"))
|
||||
|
||||
conn.commit()
|
||||
|
||||
|
||||
@@ -209,6 +209,8 @@ header h1 {
|
||||
.top-controls { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 20px; }
|
||||
|
||||
.control-group { display: flex; flex-direction: column; gap: 6px; flex: 1; min-width: 200px; }
|
||||
.control-group.control-group-small { flex: 0.5; min-width: 140px; }
|
||||
.control-group.control-group-large { flex: 2; min-width: 300px; }
|
||||
|
||||
.control-group label { font-size: 0.85em; color: var(--text-dim); font-weight: 500; }
|
||||
|
||||
@@ -378,6 +380,8 @@ header h1 {
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.param-item:hover { border-color: var(--accent2); }
|
||||
.param-item.param-empty { border-color: var(--accent); border-width: 2px; }
|
||||
.param-item.param-changed { border-color: #e9c46a; }
|
||||
.param-item.modified { border-color: var(--accent2); }
|
||||
|
||||
/* keep old .modified for backwards compat but remove red styling */
|
||||
|
||||
+3
-3
@@ -36,14 +36,14 @@
|
||||
<label>llama.cpp 版本</label>
|
||||
<select id="version-select" onchange="onVersionChange()"></select>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="control-group control-group-large">
|
||||
<label>运行模式</label>
|
||||
<div class="mode-switch">
|
||||
<button class="mode-btn active" data-mode="gpu" onclick="switchMode('gpu')">🎮 GPU 模式</button>
|
||||
<button class="mode-btn" data-mode="gpu_cpu" onclick="switchMode('gpu_cpu')">🎮+💻 GPU+CPU 模式</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="control-group control-group-small">
|
||||
<label>二进制程序</label>
|
||||
<select id="binary-select">
|
||||
<option value="llama-server">llama-server</option>
|
||||
@@ -102,7 +102,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Natural Language Input -->
|
||||
<section class="panel">
|
||||
<section class="panel" id="nl-section">
|
||||
<h2>💬 自然语言生成</h2>
|
||||
<div class="nl-input-container">
|
||||
<textarea id="nl-input" rows="3" placeholder="加载中..."></textarea>
|
||||
|
||||
+31
-2
@@ -307,6 +307,7 @@ function renderSettings() {
|
||||
'default_version': '默认版本',
|
||||
'default_mode': '默认模式',
|
||||
'default_quant': '默认量化版本',
|
||||
'show_nl_section': '显示自然语言区域 (true/false)',
|
||||
'nl_default_text': '自然语言默认提示文本',
|
||||
'llm_enabled': '启用LLM解析 (true/false)',
|
||||
'llm_api_url': 'LLM API URL',
|
||||
@@ -314,12 +315,40 @@ function renderSettings() {
|
||||
'llm_api_model': 'LLM 模型名',
|
||||
'llm_system_prompt': 'LLM 系统提示词',
|
||||
};
|
||||
// Keys that use multi-line textarea
|
||||
const textareaKeys = ['nl_default_text', 'llm_system_prompt'];
|
||||
// Keys that use select with live data
|
||||
const gpuSelectKeys = ['default_gpu'];
|
||||
const versionSelectKeys = ['default_version'];
|
||||
const modeSelectKeys = ['default_mode'];
|
||||
// Keys that are boolean-like
|
||||
const boolKeys = ['llm_enabled', 'show_nl_section'];
|
||||
|
||||
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) {
|
||||
// Textarea
|
||||
if (textareaKeys.includes(key)) {
|
||||
return `<div class="setting-item setting-item-wide"><label>${label}</label><textarea rows="4" onchange="adminState.settings['${key}'] = this.value">${value}</textarea></div>`;
|
||||
}
|
||||
// Select with GPU list
|
||||
if (gpuSelectKeys.includes(key)) {
|
||||
const opts = adminState.gpus.map(g => `<option value="${g.name}" ${value===g.name?'selected':''}>${g.name}</option>`).join('');
|
||||
return `<div class="setting-item"><label>${label}</label><select onchange="adminState.settings['${key}'] = this.value">${opts}</select></div>`;
|
||||
}
|
||||
// Select with version list
|
||||
if (versionSelectKeys.includes(key)) {
|
||||
const opts = adminState.versions.map(v => `<option value="${v.version_tag}" ${value===v.version_tag?'selected':''}>${v.version_tag}</option>`).join('');
|
||||
return `<div class="setting-item"><label>${label}</label><select onchange="adminState.settings['${key}'] = this.value">${opts}</select></div>`;
|
||||
}
|
||||
// Select with mode
|
||||
if (modeSelectKeys.includes(key)) {
|
||||
return `<div class="setting-item"><label>${label}</label><select onchange="adminState.settings['${key}'] = this.value"><option value="gpu" ${value==='gpu'?'selected':''}>GPU</option><option value="gpu_cpu" ${value==='gpu_cpu'?'selected':''}>GPU+CPU</option></select></div>`;
|
||||
}
|
||||
// Boolean
|
||||
if (boolKeys.includes(key)) {
|
||||
return `<div class="setting-item"><label>${label}</label><select onchange="adminState.settings['${key}'] = this.value"><option value="true" ${value==='true'?'selected':''}>是</option><option value="false" ${value==='false'?'selected':''}>否</option></select></div>`;
|
||||
}
|
||||
// Password or text
|
||||
const type = key === 'llm_api_key' ? 'password' : 'text';
|
||||
return `<div class="setting-item"><label>${label}</label><input type="${type}" value="${value}" onchange="adminState.settings['${key}'] = this.value"></div>`;
|
||||
}).join('');
|
||||
|
||||
+46
-8
@@ -29,6 +29,11 @@ async function loadNlDefaultText() {
|
||||
if (data.nl_default_text) {
|
||||
ta.placeholder = data.nl_default_text;
|
||||
}
|
||||
// Show/hide natural language section
|
||||
const nlSection = document.getElementById('nl-section');
|
||||
if (nlSection) {
|
||||
nlSection.classList.toggle('hidden', data.show_nl_section !== 'true');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Load Data =====
|
||||
@@ -79,9 +84,9 @@ function renderGpuSlots() {
|
||||
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 dg = state.gpus.find(g => g.name === 'RTX 3090') || state.gpus[0]; state.gpuSlots.push({ ...dg }); renderGpuSlots(); if (state.selectedModel) applyModelDefaults(state.selectedModel); generateCommand(); }
|
||||
function removeGpuSlot(i) { state.gpuSlots.splice(i, 1); renderGpuSlots(); if (state.gpuSlots.length > 1) { if (state.selectedModel) applyModelDefaults(state.selectedModel); } else { state.paramValues['split_mode'] = state.params.find(p=>p.param_key==='split_mode')?.default_value || 'layer'; state.paramValues['tensor_split'] = state.params.find(p=>p.param_key==='tensor_split')?.default_value || ''; } generateCommand(); }
|
||||
function updateGpuSlot(i, name) { const g = state.gpus.find(g => g.name === name); if (g) state.gpuSlots[i] = { ...g }; renderGpuSlots(); if (state.gpuSlots.length > 1 && state.selectedModel) applyModelDefaults(state.selectedModel); generateCommand(); }
|
||||
|
||||
// ===== Model Selection =====
|
||||
function filterBaseModels() {
|
||||
@@ -153,15 +158,29 @@ function applyModelDefaults(m) {
|
||||
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';
|
||||
// Set n_gpu_layers to model's layer count (not 'all', use actual number)
|
||||
if (m.layers && m.layers > 0) {
|
||||
state.paramValues['n_gpu_layers'] = String(m.layers);
|
||||
}
|
||||
// Multi-GPU: set split_mode to tensor, compute tensor_split by VRAM ratio
|
||||
if (state.gpuSlots.length > 1) {
|
||||
state.paramValues['split_mode'] = 'tensor';
|
||||
updateTensorSplit();
|
||||
}
|
||||
// Update param UI if currently visible
|
||||
renderParams();
|
||||
generateCommand();
|
||||
}
|
||||
|
||||
function updateTensorSplit() {
|
||||
if (state.gpuSlots.length <= 1) return;
|
||||
const totalVram = state.gpuSlots.reduce((s, g) => s + (g.vram_mb || 0), 0);
|
||||
if (totalVram > 0) {
|
||||
const ratios = state.gpuSlots.map(g => (g.vram_mb / totalVram).toFixed(2));
|
||||
state.paramValues['tensor_split'] = ratios.join(',');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Parameter Rendering =====
|
||||
function switchTab(cat) {
|
||||
state.currentTab = cat;
|
||||
@@ -230,7 +249,13 @@ function renderParams() {
|
||||
|
||||
function renderParamItem(p) {
|
||||
const val = state.paramValues[p.param_key];
|
||||
const mc = '';
|
||||
// Determine item class: empty-required (red), modified (orange), or normal
|
||||
let itemClass = '';
|
||||
if (p.param_key === 'model' && (!val || val.trim() === '')) {
|
||||
itemClass = 'param-empty';
|
||||
} else if (isParamModified(p, val)) {
|
||||
itemClass = 'param-changed';
|
||||
}
|
||||
const vb = p.affects_vram ? '<span class="vram-badge">⚡显存</span>' : '';
|
||||
// Show both short and long flag
|
||||
const shortFlag = p.short_flag || '';
|
||||
@@ -256,7 +281,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}">${flagHtml}${vb}<span class="param-desc-text">${p.description}</span>${inp}${uh}</div>`;
|
||||
return `<div class="param-item ${itemClass}" data-key="${p.param_key}">${flagHtml}${vb}<span class="param-desc-text">${p.description}</span>${inp}${uh}</div>`;
|
||||
}
|
||||
|
||||
function isParamModified(p, val) {
|
||||
@@ -267,6 +292,19 @@ function isParamModified(p, val) {
|
||||
|
||||
function setParam(key, value) {
|
||||
state.paramValues[key] = value;
|
||||
// Update this item's visual state without full re-render (preserves focus)
|
||||
const item = document.querySelector(`.param-item[data-key="${key}"]`);
|
||||
if (item) {
|
||||
const p = state.params.find(p => p.param_key === key);
|
||||
if (p) {
|
||||
item.classList.remove('param-empty', 'param-changed');
|
||||
if (p.param_key === 'model' && (!value || String(value).trim() === '')) {
|
||||
item.classList.add('param-empty');
|
||||
} else if (isParamModified(p, value)) {
|
||||
item.classList.add('param-changed');
|
||||
}
|
||||
}
|
||||
}
|
||||
// If on 'modified' tab, re-render to update the list
|
||||
if (state.currentTab === 'modified') renderParams();
|
||||
generateCommand(); updateEstimate();
|
||||
|
||||
Reference in New Issue
Block a user