diff --git a/static/css/style.css b/static/css/style.css index ca1341f..2f5b3f3 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -14,6 +14,103 @@ --radius: 8px; } +/* ===== Sticky Memory Bar ===== */ +.sticky-mem-bar { + position: sticky; + top: 0; + z-index: 200; + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 6px 12px; + margin-bottom: 16px; + box-shadow: 0 2px 8px rgba(0,0,0,0.4); +} +.sticky-vram-row, .sticky-ram-row { + display: flex; + align-items: center; + gap: 8px; +} +.sticky-ram-row { margin-top: 4px; } +.sticky-ram-row.hidden { display: none; } +.sticky-label { + font-size: 0.8em; + font-weight: 600; + color: var(--text-dim); + min-width: 60px; +} +.sticky-bar-container { + position: relative; + flex: 1; + height: 22px; + background: var(--bg); + border-radius: 4px; + overflow: hidden; + border: 1px solid var(--border); +} +.sticky-bar { + height: 100%; + background: linear-gradient(90deg, #4ecca3, #e9c46a); + border-radius: 3px; + transition: width 0.3s; + width: 0%; +} +.sticky-bar.warning { background: linear-gradient(90deg, #e9c46a, #f4a261); } +.sticky-bar.danger { background: linear-gradient(90deg, #f4a261, #e94560); } +.ram-sticky-bar { background: linear-gradient(90deg, #4e9cca, #4ecca3); } +.sticky-bar-text { + position: absolute; + top: 50%; left: 50%; + transform: translate(-50%, -50%); + font-size: 0.75em; + font-weight: 600; + color: white; + text-shadow: 1px 1px 2px rgba(0,0,0,0.8); + white-space: nowrap; +} + +/* ===== Inline Memory Config ===== */ +.inline-memory-config { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--border); } +.inline-memory-config.hidden { display: none; } +.memory-input-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } +.memory-input-row label { font-size: 0.85em; color: var(--text-dim); } +.memory-input-row input { + background: var(--bg-input); border: 1px solid var(--border); color: var(--text); + padding: 6px 10px; border-radius: 4px; width: 100px; +} +.memory-unit { font-size: 0.85em; color: var(--text-dim); } + +/* ===== Parameter Items (show long flag) ===== */ +.param-item .param-flag-long { + font-family: monospace; + font-size: 0.8em; + color: var(--text-dim); + background: var(--bg); + padding: 2px 6px; + border-radius: 3px; + min-width: 80px; + text-align: center; + flex-shrink: 0; +} + +/* ===== Command Hints ===== */ +.command-hint { margin-top: 8px; font-size: 0.85em; } +.hint-error { color: var(--accent); padding: 6px 10px; background: rgba(233, 69, 96, 0.1); border-radius: 4px; margin-bottom: 4px; border-left: 3px solid var(--accent); } +.hint-warning { color: #e9c46a; padding: 6px 10px; background: rgba(233, 196, 106, 0.1); border-radius: 4px; margin-bottom: 4px; border-left: 3px solid #e9c46a; } +.hint-info { color: var(--text-dim); padding: 6px 10px; background: rgba(136, 146, 176, 0.1); border-radius: 4px; margin-bottom: 4px; border-left: 3px solid var(--text-dim); } +.hint-ok { color: var(--accent2); padding: 6px 10px; background: rgba(78, 204, 163, 0.1); border-radius: 4px; margin-bottom: 4px; border-left: 3px solid var(--accent2); } + +/* ===== Natural Language Textarea ===== */ +.nl-input-container { display: flex; gap: 8px; align-items: flex-end; } +.nl-input-container textarea { + flex: 1; background: var(--bg-input); border: 1px solid var(--border); color: var(--text); + padding: 10px 14px; border-radius: var(--radius); font-size: 0.95em; resize: vertical; font-family: inherit; + line-height: 1.5; +} +.nl-input-container textarea:focus { outline: none; border-color: var(--accent); } +.nl-input-container button { padding: 10px 24px; background: var(--accent2); color: var(--bg); border: none; border-radius: var(--radius); cursor: pointer; font-weight: 600; flex-shrink: 0; } +} + * { margin: 0; padding: 0; box-sizing: border-box; } body { @@ -234,9 +331,6 @@ header h1 { .detail-item b { color: var(--accent2); } /* ===== Natural Language ===== */ -.nl-input-container { display: flex; gap: 8px; } -.nl-input-container input { flex: 1; background: var(--bg-input); border: 1px solid var(--border); color: var(--text); padding: 10px 14px; border-radius: var(--radius); } -.nl-input-container button { padding: 10px 24px; background: var(--accent2); color: var(--bg); border: none; border-radius: var(--radius); cursor: pointer; font-weight: 600; } .nl-result { margin-top: 10px; font-size: 0.9em; color: var(--text-dim); } .parsed-param { display: inline-block; margin: 2px 4px; padding: 2px 8px; background: var(--bg-input); border-radius: 4px; border: 1px solid var(--border); } .parsed-param .key { color: var(--accent2); } diff --git a/static/index.html b/static/index.html index 49b653b..6fcbc34 100644 --- a/static/index.html +++ b/static/index.html @@ -12,6 +12,24 @@

🦙 llama.cpp 命令生成器

+ +
+
+ 🎮 VRAM + +
+ +
+
@@ -34,11 +52,13 @@
- +

🖥️ GPU 配置

+ +
@@ -46,33 +66,27 @@
-
- -

📦 选择模型

-
@@ -80,12 +94,10 @@
- -
@@ -93,7 +105,7 @@

💬 自然语言生成

- +
@@ -123,6 +135,7 @@

📋 生成命令

请在上方配置参数...
+
diff --git a/static/js/main.js b/static/js/main.js index d7b280f..a99a77e 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -6,6 +6,7 @@ let state = { mode: 'gpu', gpuSlots: [], showHidden: false, currentTab: 'common', paramSearchText: '', selectedModel: null, defaultQuant: 'Q4_K_M', + lastEstimate: null, }; // ===== Init ===== @@ -34,7 +35,6 @@ async function loadModelsGrouped() { state.defaultQuant = data.default_quant || 'Q4_K_M'; state.baseModelList = Object.keys(state.modelsGrouped).sort(); state.filteredBaseModels = state.baseModelList; - // Also flat list for backward compat state.models = Object.values(state.modelsGrouped).flat(); renderBaseModelDropdown(); } @@ -49,14 +49,14 @@ async function loadParams(versionId) { renderParams(); generateCommand(); updateEstimate(); } -// ===== Version Change ===== async function onVersionChange() { state.currentVersionId = parseInt(document.getElementById('version-select').value); await loadParams(state.currentVersionId); } -// ===== Mode Switch ===== function switchMode(mode) { state.mode = mode; document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.mode === mode)); - document.getElementById('memory-panel').classList.toggle('hidden', mode !== 'gpu_cpu'); + const showRam = mode === 'gpu_cpu'; + document.getElementById('inline-memory-config').classList.toggle('hidden', !showRam); + document.getElementById('sticky-ram-row').classList.toggle('hidden', !showRam); generateCommand(); updateEstimate(); } @@ -73,49 +73,40 @@ function addGpuSlot() { if (state.gpuSlots.length >= 4) return; const dg = state function removeGpuSlot(i) { state.gpuSlots.splice(i, 1); renderGpuSlots(); generateCommand(); } function updateGpuSlot(i, name) { const g = state.gpus.find(g => g.name === name); if (g) state.gpuSlots[i] = { ...g }; renderGpuSlots(); generateCommand(); } -// ===== Model Selection (two-step) ===== +// ===== Model Selection ===== function filterBaseModels() { const text = document.getElementById('model-search').value.toLowerCase(); state.filteredBaseModels = state.baseModelList.filter(n => n.toLowerCase().includes(text)); renderBaseModelDropdown(); document.getElementById('model-dropdown').style.display = state.filteredBaseModels.length > 0 ? 'block' : 'none'; } - 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}
`; + const qs = quants.map(q => q.quant).filter(Boolean).join(', '); + return `
${n}${qs}
`; }).join(''); } - function selectBaseModel(baseName) { document.getElementById('model-search').value = baseName; document.getElementById('model-dropdown').style.display = 'none'; const variants = state.modelsGrouped[baseName] || []; if (variants.length === 0) return; - // Show quant step document.getElementById('quant-step').classList.remove('hidden'); - // Auto-select default quant - const defaultVariant = variants.find(v => v.quant === state.defaultQuant) || variants[0]; - renderQuantOptions(baseName, variants, defaultVariant.id); - selectModel(defaultVariant.id); + const dv = variants.find(v => v.quant === state.defaultQuant) || variants[0]; + renderQuantOptions(baseName, variants, dv.id); + selectModel(dv.id); } - function renderQuantOptions(baseName, variants, selectedId) { document.getElementById('quant-options').innerHTML = variants.map(v => ` `).join(''); } - function selectModel(id) { const m = state.models.find(m => m.id === id); if (!m) return; state.selectedModel = m; - // Update quant buttons active state - const variants = state.modelsGrouped[m.base_model] || []; - renderQuantOptions(m.base_model, variants, id); - // Show model detail + renderQuantOptions(m.base_model, state.modelsGrouped[m.base_model] || [], id); document.getElementById('model-selected-info').innerHTML = `
模型: ${m.name} @@ -195,7 +186,15 @@ function renderParamItem(p) { const isMod = isParamModified(p, val); const mc = isMod ? 'modified' : ''; const vb = p.affects_vram ? '⚡显存' : ''; - 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 = `${shortFlag}${longFlag}`; + } else { + flagHtml = `${longFlag}`; + } let inp = ''; if (p.param_type === 'boolean') { inp = ``; @@ -211,7 +210,7 @@ function renderParamItem(p) { inp = ``; } const uh = p.unit ? `${p.unit}` : ''; - return `
${flag}${vb}${p.description}${inp}${uh}
`; + return `
${flagHtml}${vb}${p.description}${inp}${uh}
`; } function isParamModified(p, val) { @@ -234,12 +233,16 @@ async function updateEstimate() { const params = collectParamsForEstimate(); const gpuSel = state.gpuSlots.map(s => ({ name: s.name, vram_mb: s.vram_mb })); const sm = parseFloat(document.getElementById('sys-memory').value) || 0; - const smu = document.getElementById('sys-memory-unit').value; - let sysGb = 0; if (smu === '1') sysGb = sm; else if (smu === '2') sysGb = sm / 1024; + // No unit select anymore - always GB, 0 = unlimited + const sysGb = sm > 0 ? sm : 0; + const isUnlimited = sm === 0; const res = await fetch('/api/estimate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ params, gpu_selections: gpuSel, mode: state.mode, system_memory_gb: sysGb }) }); const data = await res.json(); + state.lastEstimate = data; renderVramDisplay(data); - if (state.mode === 'gpu_cpu') renderRamDisplay(data, sysGb); + if (state.mode === 'gpu_cpu') renderRamDisplay(data, sysGb, isUnlimited); + updateStickyBar(data, sysGb, isUnlimited); + generateCommandHint(data, sysGb, isUnlimited); } function collectParamsForEstimate() { @@ -265,13 +268,92 @@ function renderVramDisplay(data) { 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, sysGb) { +function renderRamDisplay(data, sysGb, isUnlimited) { 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}%)`; } + 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}%)`; + } bd.innerHTML = `
CPU模型权重${data.cpu_weights_gb}GB
CPU KV缓存${(data.cpu_kv_cache_mb/1024).toFixed(2)}GB
`; } +// ===== 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 => `
${h.text}
`).join(''); +} + // ===== Natural Language ===== async function parseNaturalLanguage() { const text = document.getElementById('nl-input').value;