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:
+97
-3
@@ -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
@@ -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="用自然语言描述你想要的配置,支持多行输入。 例如: 用RTX 4090跑Llama-3-70B,上下文8192 温度0.7,开启flash attention 端口设为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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user