Files
llama-cmd-gen/static/js/main.js
T
hz4th_coder a407032b4d feat: llama.cpp 命令生成器 v1.0.0
- 支持多版本 llama.cpp 参数 (b6310, b10068)
- GPU 模式 / GPU+CPU 模式
- 多 GPU 支持 (最多4张)
- 实时显存/内存估算
- 自然语言解析生成命令
- 参数分级显示 (重要/隐藏)
- 仅输出非默认值参数
- 后台管理 (GPU/版本/参数 CRUD)
- 模型预设 (7B-70B)
2026-07-19 18:08:36 +08:00

502 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ===== State =====
let state = {
versions: [],
gpus: [],
currentVersionId: null,
params: [], // param definitions for current version
paramValues: {}, // user-selected values
mode: 'gpu',
gpuSlots: [], // [{name, vram_mb, ...}]
showHidden: false,
currentTab: 'common',
binary: 'llama-server',
};
// ===== Init =====
async function init() {
await loadVersions();
await loadGpus();
// Set default version
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);
}
// Default GPU: RTX 3090
const defaultGpu = state.gpus.find(g => g.name === 'RTX 3090') || state.gpus[0];
if (defaultGpu) {
state.gpuSlots = [{ ...defaultGpu }];
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 => `<option value="${v.id}">${v.version_tag} - ${v.description}</option>`).join('');
}
async function loadGpus() {
const res = await fetch('/api/gpus');
state.gpus = await res.json();
}
async function loadParams(versionId) {
const res = await fetch(`/api/versions/${versionId}/params`);
state.params = await res.json();
// Initialize param values with defaults
state.paramValues = {};
state.params.forEach(p => {
if (p.param_type === 'number') {
state.paramValues[p.param_key] = p.default_value;
} else if (p.param_type === 'boolean') {
state.paramValues[p.param_key] = p.default_value === 'true';
} else {
state.paramValues[p.param_key] = p.default_value || '';
}
});
renderParams();
generateCommand();
updateEstimate();
}
// ===== Version Change =====
async function onVersionChange() {
const sel = document.getElementById('version-select');
state.currentVersionId = parseInt(sel.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);
});
const memPanel = document.getElementById('memory-panel');
const gpuPanel = document.getElementById('gpu-panel');
if (mode === 'gpu_cpu') {
memPanel.classList.remove('hidden');
} else {
memPanel.classList.add('hidden');
}
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 =>
`<option value="${g.name}" ${g.name === gpu.name ? 'selected' : ''}>${g.name} (${(g.vram_mb / 1024).toFixed(0)}GB)</option>`
).join('');
return `
<div class="gpu-slot">
<span class="gpu-index">GPU ${i + 1}</span>
<select onchange="updateGpuSlot(${i}, this.value)">
<option value="">-- 选择GPU --</option>
${options}
</select>
<span class="vram-info">显存: ${(gpu.vram_mb / 1024).toFixed(1)} GB</span>
${state.gpuSlots.length > 1 ? `<button class="btn-remove" onclick="removeGpuSlot(${i})">×</button>` : ''}
</div>
`;
}).join('');
const addBtn = document.getElementById('btn-add-gpu');
addBtn.style.display = state.gpuSlots.length >= 4 ? 'none' : 'block';
updateEstimate();
}
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();
}
// ===== Parameter Rendering =====
function switchTab(cat) {
state.currentTab = cat;
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.cat === cat);
});
renderParams();
}
function renderParams() {
const container = document.getElementById('param-container');
const params = state.params.filter(p => p.category === state.currentTab);
const importantParams = params.filter(p => p.is_important === 1);
const otherParams = params.filter(p => p.is_important === 0);
let html = '';
// Important params always visible
importantParams.forEach(p => html += renderParamItem(p, false));
// Other params
otherParams.forEach(p => {
const hidden = !state.showHidden;
html += renderParamItem(p, hidden);
});
container.innerHTML = html;
// Show/hide toggle button
const toggleBtn = document.getElementById('toggle-advanced-btn');
if (otherParams.length > 0) {
toggleBtn.style.display = 'block';
toggleBtn.textContent = state.showHidden ? '▲ 收起更多参数' : '▼ 显示更多参数';
} else {
toggleBtn.style.display = 'none';
}
}
function renderParamItem(p, hidden) {
const val = state.paramValues[p.param_key];
const defaultVal = p.default_value;
const isModified = isParamModified(p, val);
const hiddenClass = hidden ? 'hidden-param' : '';
const modifiedClass = isModified ? 'modified' : '';
let inputHtml = '';
if (p.param_type === 'boolean') {
inputHtml = `
<div class="param-checkbox">
<input type="checkbox" ${val ? 'checked' : ''}
onchange="setParam('${p.param_key}', this.checked)">
<span>${val ? '开启' : '关闭'}</span>
</div>
`;
} else if (p.param_type === 'select') {
const options = Array.isArray(p.options) ? p.options : [];
inputHtml = `
<select onchange="setParam('${p.param_key}', this.value)">
${options.map(o => `<option value="${o}" ${String(val) === String(o) ? 'selected' : ''}>${o}</option>`).join('')}
</select>
`;
} 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 = `
<input type="number" value="${val}" step="${step}" ${min} ${max}
onchange="setParam('${p.param_key}', this.value)"
oninput="onParamInput('${p.param_key}', this.value)">
`;
} else {
inputHtml = `
<input type="text" value="${val}"
onchange="setParam('${p.param_key}', this.value)"
oninput="onParamInput('${p.param_key}', this.value)">
`;
}
const unitHtml = p.unit ? `<span style="color:var(--text-dim);font-size:0.8em;">${p.unit}</span>` : '';
const vramBadge = p.affects_vram ? '<span style="color:var(--accent);font-size:0.7em;">⚡显存</span>' : '';
return `
<div class="param-item ${hiddenClass} ${modifiedClass}" data-key="${p.param_key}">
<div class="param-label">
<span class="param-flag">${p.short_flag || p.long_flag}</span>
${vramBadge}
${p.description}
</div>
<div class="param-desc">${p.description}</div>
${inputHtml}
${unitHtml}
</div>
`;
}
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);
}
function setParam(key, value) {
state.paramValues[key] = value;
renderParams();
generateCommand();
updateEstimate();
}
let inputTimer = null;
function onParamInput(key, value) {
state.paramValues[key] = value;
// Debounce update
if (inputTimer) clearTimeout(inputTimer);
inputTimer = setTimeout(() => {
renderParams();
generateCommand();
updateEstimate();
}, 300);
}
function toggleHiddenParams() {
state.showHidden = !state.showHidden;
renderParams();
}
// ===== Model Presets =====
function setModelPreset(type) {
const presets = {
'7b': { size: 4.1, layers: 32, embd: 4096, kv_heads: 32, head_dim: 128, heads: 32 },
'13b': { size: 7.4, layers: 40, embd: 5120, kv_heads: 40, head_dim: 128, heads: 40 },
'34b': { size: 19.5, layers: 48, embd: 8192, kv_heads: 8, head_dim: 128, heads: 64 },
'70b': { size: 38.5, layers: 80, embd: 8192, kv_heads: 8, head_dim: 128, heads: 64 },
'70b-q8': { size: 74.0, layers: 80, embd: 8192, kv_heads: 8, head_dim: 128, heads: 64 },
'70b-fp16': { size: 138.0, layers: 80, embd: 8192, kv_heads: 8, head_dim: 128, heads: 64 },
};
const p = presets[type];
if (p) {
document.getElementById('model-size-gb').value = p.size;
document.getElementById('model-layers').value = p.layers;
document.getElementById('model-embd').value = p.embd;
document.getElementById('model-kv-heads').value = p.kv_heads;
document.getElementById('model-head-dim').value = p.head_dim;
document.getElementById('model-heads').value = p.heads;
updateEstimate();
}
}
// ===== 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 data = await res.json();
renderVramDisplay(data);
if (state.mode === 'gpu_cpu') {
renderRamDisplay(data, sysMemoryGb);
}
}
function collectParamsForEstimate() {
const params = { ...state.paramValues };
params._model_size_gb = parseFloat(document.getElementById('model-size-gb').value) || 0;
params._model_layers = parseInt(document.getElementById('model-layers').value) || 0;
params._model_embd = parseInt(document.getElementById('model-embd').value) || 0;
params._model_kv_heads = parseInt(document.getElementById('model-kv-heads').value) || 0;
params._model_head_dim = parseInt(document.getElementById('model-head-dim').value) || 0;
params._model_heads = parseInt(document.getElementById('model-heads').value) || 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) + '%';
if (percent > 90) {
bar.className = 'vram-bar danger';
} else if (percent > 75) {
bar.className = 'vram-bar warning';
} else {
bar.className = 'vram-bar';
}
label.textContent = `VRAM: ${data.total_gb}GB / ${data.total_vram_available_gb}GB (${percent}%)`;
breakdown.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, 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) + '%';
if (percent > 90) {
bar.className = 'vram-bar ram-bar danger';
} else if (percent > 75) {
bar.className = 'vram-bar ram-bar warning';
} else {
bar.className = 'vram-bar ram-bar';
}
label.textContent = `内存: ${data.cpu_total_gb}GB / ${sysMemoryGb}GB (${percent}%)`;
}
breakdown.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>
<div class="breakdown-item">
<span class="label">开销</span>
<span class="value">~0.49GB</span>
</div>
`;
}
// ===== 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 data = await res.json();
// Apply parsed values
let resultHtml = '<div>解析结果:</div>';
// Handle GPU
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 += `<span class="parsed-param"><span class="key">GPU:</span> <span class="value">${data._gpu_name}${data._gpu_count > 1 ? ' x' + data._gpu_count : ''}</span></span>`;
}
}
// Handle mode
if (data._mode) {
switchMode(data._mode);
resultHtml += `<span class="parsed-param"><span class="key">模式:</span> <span class="value">${data._mode}</span></span>`;
}
// Apply other params
for (const [key, value] of Object.entries(data)) {
if (key.startsWith('_')) continue;
state.paramValues[key] = value;
resultHtml += `<span class="parsed-param"><span class="key">${key}:</span> <span class="value">${value}</span></span>`;
}
document.getElementById('nl-result').innerHTML = resultHtml;
renderParams();
generateCommand();
updateEstimate();
}
// ===== Generate Command =====
async function generateCommand() {
const params = { ...state.paramValues };
// Remove model info params
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 data = await res.json();
document.getElementById('command-output').textContent = data.command;
}
// ===== Copy =====
function copyCommand() {
const text = document.getElementById('command-output').textContent;
navigator.clipboard.writeText(text).then(() => {
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = '✅ 已复制!';
setTimeout(() => btn.textContent = originalText, 2000);
});
}
// ===== Init on Load =====
window.addEventListener('DOMContentLoaded', init);