feat: v1.3.0 - 五项优化

1. 显存/内存进度条置顶显示 (sticky)
   - 滚动页面时始终可见 VRAM 和内存占用情况
   - GPU+CPU 模式时内存条也同步显示在 sticky 区域

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

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

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

5. 自然语言输入改为多行 textarea
   - 支持多行输入
   - placeholder 说明可以自然对话描述需求
This commit is contained in:
2026-07-19 23:35:20 +08:00
parent 6691f37e45
commit 3c27a205e5
3 changed files with 243 additions and 54 deletions
+97 -3
View File
@@ -14,6 +14,103 @@
--radius: 8px; --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; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { body {
@@ -234,9 +331,6 @@ header h1 {
.detail-item b { color: var(--accent2); } .detail-item b { color: var(--accent2); }
/* ===== Natural Language ===== */ /* ===== 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); } .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 { 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); } .parsed-param .key { color: var(--accent2); }
+36 -23
View File
@@ -12,6 +12,24 @@
<h1>🦙 llama.cpp 命令生成器</h1> <h1>🦙 llama.cpp 命令生成器</h1>
</header> </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 --> <!-- Version & Mode Selection -->
<section class="top-controls"> <section class="top-controls">
<div class="control-group"> <div class="control-group">
@@ -34,11 +52,13 @@
</div> </div>
</section> </section>
<!-- GPU Configuration --> <!-- GPU Configuration + System Memory (merged) -->
<section class="panel" id="gpu-panel"> <section class="panel" id="gpu-panel">
<h2>🖥️ GPU 配置</h2> <h2>🖥️ GPU 配置</h2>
<div id="gpu-slots"></div> <div id="gpu-slots"></div>
<button class="btn-add" id="btn-add-gpu" onclick="addGpuSlot()">+ 添加显卡</button> <button class="btn-add" id="btn-add-gpu" onclick="addGpuSlot()">+ 添加显卡</button>
<!-- VRAM Display -->
<div class="vram-display" id="vram-display"> <div class="vram-display" id="vram-display">
<div class="vram-bar-container"> <div class="vram-bar-container">
<div class="vram-bar" id="vram-bar"></div> <div class="vram-bar" id="vram-bar"></div>
@@ -46,33 +66,27 @@
</div> </div>
<div class="vram-breakdown" id="vram-breakdown"></div> <div class="vram-breakdown" id="vram-breakdown"></div>
</div> </div>
</section>
<!-- System Memory (for GPU+CPU mode) --> <!-- System Memory (inline, only for GPU+CPU mode) -->
<section class="panel hidden" id="memory-panel"> <div class="inline-memory-config hidden" id="inline-memory-config">
<h2>💾 系统内存配置</h2> <div class="memory-input-row">
<div class="control-group"> <label>系统内存上限</label>
<label>系统内存上限</label> <input type="number" id="sys-memory" value="0" min="0" step="1" onchange="updateEstimate()" placeholder="0 = 无限制">
<input type="number" id="sys-memory" value="0" min="0" step="1" onchange="updateEstimate()"> <span class="memory-unit">GB</span>
<select id="sys-memory-unit" onchange="updateEstimate()"> </div>
<option value="0">无限制</option> <div class="vram-display" id="ram-display">
<option value="1">GB</option> <div class="vram-bar-container">
<option value="2">MB</option> <div class="vram-bar ram-bar" id="ram-bar"></div>
</select> <div class="vram-label" id="ram-label">内存: 等待配置...</div>
</div> </div>
<div class="vram-display" id="ram-display"> <div class="vram-breakdown" id="ram-breakdown"></div>
<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>
<div class="vram-breakdown" id="ram-breakdown"></div>
</div> </div>
</section> </section>
<!-- Model Selection (two-step) --> <!-- Model Selection (two-step) -->
<section class="panel"> <section class="panel">
<h2>📦 选择模型</h2> <h2>📦 选择模型</h2>
<!-- Step 1: Select base model -->
<div class="model-step"> <div class="model-step">
<label class="model-step-label">第1步:选择模型</label> <label class="model-step-label">第1步:选择模型</label>
<div class="model-select-container"> <div class="model-select-container">
@@ -80,12 +94,10 @@
<div class="model-dropdown" id="model-dropdown"></div> <div class="model-dropdown" id="model-dropdown"></div>
</div> </div>
</div> </div>
<!-- Step 2: Select quant -->
<div class="model-step hidden" id="quant-step"> <div class="model-step hidden" id="quant-step">
<label class="model-step-label">第2步:选择量化版本</label> <label class="model-step-label">第2步:选择量化版本</label>
<div id="quant-options"></div> <div id="quant-options"></div>
</div> </div>
<!-- Selected model info -->
<div id="model-selected-info" class="model-selected-info"></div> <div id="model-selected-info" class="model-selected-info"></div>
</section> </section>
@@ -93,7 +105,7 @@
<section class="panel"> <section class="panel">
<h2>💬 自然语言生成</h2> <h2>💬 自然语言生成</h2>
<div class="nl-input-container"> <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="用自然语言描述你想要的配置,支持多行输入。&#10;例如:&#10;用RTX 4090跑Llama-3-70B上下文8192&#10;温度0.7,开启flash attention&#10;端口设为8080"></textarea>
<button onclick="parseNaturalLanguage()">解析</button> <button onclick="parseNaturalLanguage()">解析</button>
</div> </div>
<div id="nl-result" class="nl-result"></div> <div id="nl-result" class="nl-result"></div>
@@ -123,6 +135,7 @@
<section class="panel command-panel"> <section class="panel command-panel">
<h2>📋 生成命令</h2> <h2>📋 生成命令</h2>
<div class="command-output" id="command-output">请在上方配置参数...</div> <div class="command-output" id="command-output">请在上方配置参数...</div>
<div class="command-hint" id="command-hint"></div>
<div class="command-actions"> <div class="command-actions">
<button onclick="copyCommand()" class="btn-copy">📋 复制命令</button> <button onclick="copyCommand()" class="btn-copy">📋 复制命令</button>
<button onclick="generateCommand()" class="btn-gen">🔄 重新生成</button> <button onclick="generateCommand()" class="btn-gen">🔄 重新生成</button>
+110 -28
View File
@@ -6,6 +6,7 @@ let state = {
mode: 'gpu', gpuSlots: [], showHidden: false, mode: 'gpu', gpuSlots: [], showHidden: false,
currentTab: 'common', paramSearchText: '', currentTab: 'common', paramSearchText: '',
selectedModel: null, defaultQuant: 'Q4_K_M', selectedModel: null, defaultQuant: 'Q4_K_M',
lastEstimate: null,
}; };
// ===== Init ===== // ===== Init =====
@@ -34,7 +35,6 @@ async function loadModelsGrouped() {
state.defaultQuant = data.default_quant || 'Q4_K_M'; state.defaultQuant = data.default_quant || 'Q4_K_M';
state.baseModelList = Object.keys(state.modelsGrouped).sort(); state.baseModelList = Object.keys(state.modelsGrouped).sort();
state.filteredBaseModels = state.baseModelList; state.filteredBaseModels = state.baseModelList;
// Also flat list for backward compat
state.models = Object.values(state.modelsGrouped).flat(); state.models = Object.values(state.modelsGrouped).flat();
renderBaseModelDropdown(); renderBaseModelDropdown();
} }
@@ -49,14 +49,14 @@ async function loadParams(versionId) {
renderParams(); generateCommand(); updateEstimate(); renderParams(); generateCommand(); updateEstimate();
} }
// ===== Version Change =====
async function onVersionChange() { state.currentVersionId = parseInt(document.getElementById('version-select').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) { function switchMode(mode) {
state.mode = 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'); 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(); generateCommand(); updateEstimate();
} }
@@ -73,49 +73,40 @@ function addGpuSlot() { if (state.gpuSlots.length >= 4) return; const dg = state
function removeGpuSlot(i) { state.gpuSlots.splice(i, 1); 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 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() { function filterBaseModels() {
const text = document.getElementById('model-search').value.toLowerCase(); const text = document.getElementById('model-search').value.toLowerCase();
state.filteredBaseModels = state.baseModelList.filter(n => n.toLowerCase().includes(text)); state.filteredBaseModels = state.baseModelList.filter(n => n.toLowerCase().includes(text));
renderBaseModelDropdown(); renderBaseModelDropdown();
document.getElementById('model-dropdown').style.display = state.filteredBaseModels.length > 0 ? 'block' : 'none'; document.getElementById('model-dropdown').style.display = state.filteredBaseModels.length > 0 ? 'block' : 'none';
} }
function renderBaseModelDropdown() { function renderBaseModelDropdown() {
document.getElementById('model-dropdown').innerHTML = state.filteredBaseModels.map(n => { document.getElementById('model-dropdown').innerHTML = state.filteredBaseModels.map(n => {
const quants = state.modelsGrouped[n] || []; const quants = state.modelsGrouped[n] || [];
const quantStr = quants.map(q => q.quant).filter(Boolean).join(', '); 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">${quantStr}</span></div>`; return `<div class="model-option" onclick="selectBaseModel('${n}')"><span class="model-name">${n}</span><span class="model-meta">${qs}</span></div>`;
}).join(''); }).join('');
} }
function selectBaseModel(baseName) { function selectBaseModel(baseName) {
document.getElementById('model-search').value = baseName; document.getElementById('model-search').value = baseName;
document.getElementById('model-dropdown').style.display = 'none'; document.getElementById('model-dropdown').style.display = 'none';
const variants = state.modelsGrouped[baseName] || []; const variants = state.modelsGrouped[baseName] || [];
if (variants.length === 0) return; if (variants.length === 0) return;
// Show quant step
document.getElementById('quant-step').classList.remove('hidden'); document.getElementById('quant-step').classList.remove('hidden');
// Auto-select default quant const dv = variants.find(v => v.quant === state.defaultQuant) || variants[0];
const defaultVariant = variants.find(v => v.quant === state.defaultQuant) || variants[0]; renderQuantOptions(baseName, variants, dv.id);
renderQuantOptions(baseName, variants, defaultVariant.id); selectModel(dv.id);
selectModel(defaultVariant.id);
} }
function renderQuantOptions(baseName, variants, selectedId) { function renderQuantOptions(baseName, variants, selectedId) {
document.getElementById('quant-options').innerHTML = variants.map(v => ` 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> <button class="quant-btn ${v.id === selectedId ? 'active' : ''}" onclick="selectModel(${v.id})">${v.quant || 'FP16'}<span class="quant-size">${v.size_gb}GB</span></button>
`).join(''); `).join('');
} }
function selectModel(id) { function selectModel(id) {
const m = state.models.find(m => m.id === id); const m = state.models.find(m => m.id === id);
if (!m) return; if (!m) return;
state.selectedModel = m; state.selectedModel = m;
// Update quant buttons active state renderQuantOptions(m.base_model, state.modelsGrouped[m.base_model] || [], id);
const variants = state.modelsGrouped[m.base_model] || [];
renderQuantOptions(m.base_model, variants, id);
// Show model detail
document.getElementById('model-selected-info').innerHTML = ` document.getElementById('model-selected-info').innerHTML = `
<div class="model-detail"> <div class="model-detail">
<span class="detail-item"><b>模型:</b> ${m.name}</span> <span class="detail-item"><b>模型:</b> ${m.name}</span>
@@ -195,7 +186,15 @@ function renderParamItem(p) {
const isMod = isParamModified(p, val); const isMod = isParamModified(p, val);
const mc = isMod ? 'modified' : ''; const mc = isMod ? 'modified' : '';
const vb = p.affects_vram ? '<span class="vram-badge">⚡显存</span>' : ''; 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 = ''; let inp = '';
if (p.param_type === 'boolean') { if (p.param_type === 'boolean') {
inp = `<input type="checkbox" ${val ? 'checked' : ''} onchange="setParam('${p.param_key}', this.checked)">`; inp = `<input type="checkbox" ${val ? 'checked' : ''} onchange="setParam('${p.param_key}', this.checked)">`;
@@ -211,7 +210,7 @@ function renderParamItem(p) {
inp = `<input type="text" value="${val}" onchange="setParam('${p.param_key}', this.value)" data-key="${p.param_key}">`; inp = `<input type="text" value="${val}" onchange="setParam('${p.param_key}', this.value)" data-key="${p.param_key}">`;
} }
const uh = p.unit ? `<span class="param-unit">${p.unit}</span>` : ''; const 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) { function isParamModified(p, val) {
@@ -234,12 +233,16 @@ async function updateEstimate() {
const params = collectParamsForEstimate(); const params = collectParamsForEstimate();
const gpuSel = state.gpuSlots.map(s => ({ name: s.name, vram_mb: s.vram_mb })); const gpuSel = state.gpuSlots.map(s => ({ name: s.name, vram_mb: s.vram_mb }));
const sm = parseFloat(document.getElementById('sys-memory').value) || 0; const sm = parseFloat(document.getElementById('sys-memory').value) || 0;
const smu = document.getElementById('sys-memory-unit').value; // No unit select anymore - always GB, 0 = unlimited
let sysGb = 0; if (smu === '1') sysGb = sm; else if (smu === '2') sysGb = sm / 1024; 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 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(); const data = await res.json();
state.lastEstimate = data;
renderVramDisplay(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() { function collectParamsForEstimate() {
@@ -265,13 +268,92 @@ function renderVramDisplay(data) {
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>`; 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>`;
} }
function renderRamDisplay(data, sysGb) { function renderRamDisplay(data, sysGb, isUnlimited) {
const bar = document.getElementById('ram-bar'), label = document.getElementById('ram-label'), bd = document.getElementById('ram-breakdown'); 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 (无限)`; } if (isUnlimited) { 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}%)`; } 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>`; 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>`;
} }
// ===== 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 ===== // ===== Natural Language =====
async function parseNaturalLanguage() { async function parseNaturalLanguage() {
const text = document.getElementById('nl-input').value; const text = document.getElementById('nl-input').value;