Files
llama-cmd-gen/static/js/main.js
T
hz4th_coder 5bfb74480b fix: v1.1.0 - 修复5个问题
1. 首页移除后台链接,后台需密码登录
2. 参数输入不再丢失焦点(停止oninput重建DOM)
3. 每个参数单行布局 + 搜索功能
4. 复制命令使用execCommand兼容非HTTPS
5. 模型选择器替代手动输入,26个预设模型,后台可增删改

- 新增 models 数据表
- 新增 /api/models 公开读取接口
- 新增 /api/admin/models CRUD 接口
- 新增 admin 登录/登出/检查接口
- admin 页面加登录界面
- admin 新增模型管理标签页
- CORS 支持 credentials
2026-07-19 18:49:05 +08:00

482 lines
18 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: [],
models: [],
filteredModels: [],
selectedModel: null,
currentVersionId: null,
params: [],
paramValues: {},
mode: 'gpu',
gpuSlots: [],
showHidden: false,
currentTab: 'common',
paramSearchText: '',
};
// ===== Init =====
async function init() {
await loadVersions();
await loadGpus();
await loadModels();
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);
}
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 loadModels() {
const res = await fetch('/api/models');
state.models = await res.json();
state.filteredModels = state.models;
renderModelDropdown();
}
async function loadParams(versionId) {
const res = await fetch(`/api/versions/${versionId}/params`);
state.params = await res.json();
state.paramValues = {};
state.params.forEach(p => {
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);
});
document.getElementById('memory-panel').classList.toggle('hidden', mode !== 'gpu_cpu');
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('');
document.getElementById('btn-add-gpu').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();
}
// ===== Model Selection =====
function filterModels() {
const text = document.getElementById('model-search').value.toLowerCase();
state.filteredModels = state.models.filter(m =>
m.name.toLowerCase().includes(text) ||
(m.quant || '').toLowerCase().includes(text) ||
(m.description || '').toLowerCase().includes(text)
);
renderModelDropdown();
document.getElementById('model-dropdown').style.display = state.filteredModels.length > 0 ? 'block' : 'none';
}
function renderModelDropdown() {
const dd = document.getElementById('model-dropdown');
dd.innerHTML = state.filteredModels.map(m => `
<div class="model-option" onclick="selectModel(${m.id})">
<span class="model-name">${m.name}</span>
<span class="model-meta">${m.size_gb}GB | ${m.layers}L | ${m.quant || ''}</span>
</div>
`).join('');
}
function selectModel(id) {
const m = state.models.find(m => m.id === id);
if (!m) return;
state.selectedModel = m;
document.getElementById('model-search').value = m.name;
document.getElementById('model-dropdown').style.display = 'none';
document.getElementById('model-selected-info').innerHTML = `
<div class="model-detail">
<span class="detail-item"><b>大小:</b> ${m.size_gb} GB</span>
<span class="detail-item"><b>层数:</b> ${m.layers}</span>
<span class="detail-item"><b>嵌入维度:</b> ${m.embd}</span>
<span class="detail-item"><b>KV Heads:</b> ${m.kv_heads}</span>
<span class="detail-item"><b>Head Dim:</b> ${m.head_dim}</span>
<span class="detail-item"><b>Attention Heads:</b> ${m.attention_heads}</span>
<span class="detail-item"><b>量化:</b> ${m.quant || 'N/A'}</span>
</div>
`;
updateEstimate();
}
// ===== Parameter Rendering =====
function switchTab(cat) {
state.currentTab = cat;
state.paramSearchText = '';
document.getElementById('param-search').value = '';
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.cat === cat);
});
renderParams();
}
function onParamSearch() {
state.paramSearchText = document.getElementById('param-search').value.toLowerCase();
renderParams();
}
function getFilteredParams() {
let params = state.params.filter(p => p.category === state.currentTab);
if (state.paramSearchText) {
params = params.filter(p =>
p.param_key.toLowerCase().includes(state.paramSearchText) ||
(p.long_flag || '').toLowerCase().includes(state.paramSearchText) ||
(p.short_flag || '').toLowerCase().includes(state.paramSearchText) ||
(p.description || '').toLowerCase().includes(state.paramSearchText)
);
}
return params;
}
function renderParams() {
const container = document.getElementById('param-container');
const allParams = getFilteredParams();
// When searching, show all matching regardless of important/hidden
if (state.paramSearchText) {
container.innerHTML = allParams.map(p => renderParamItem(p)).join('');
document.getElementById('toggle-advanced-btn').style.display = 'none';
return;
}
const importantParams = allParams.filter(p => p.is_important === 1);
const otherParams = allParams.filter(p => p.is_important === 0);
let html = importantParams.map(p => renderParamItem(p)).join('');
if (state.showHidden) {
html += otherParams.map(p => renderParamItem(p)).join('');
}
container.innerHTML = html;
const toggleBtn = document.getElementById('toggle-advanced-btn');
if (otherParams.length > 0 && !state.paramSearchText) {
toggleBtn.style.display = 'block';
toggleBtn.textContent = state.showHidden ? '▲ 收起更多参数' : '▼ 显示更多参数';
} else {
toggleBtn.style.display = 'none';
}
}
function renderParamItem(p) {
const val = state.paramValues[p.param_key];
const defaultVal = p.default_value;
const isModified = isParamModified(p, val);
const modifiedClass = isModified ? 'modified' : '';
const vramBadge = p.affects_vram ? '<span class="vram-badge">⚡显存</span>' : '';
const flag = p.short_flag || p.long_flag;
let inputHtml = '';
if (p.param_type === 'boolean') {
inputHtml = `<input type="checkbox" ${val ? 'checked' : ''} onchange="setParam('${p.param_key}', this.checked)">`;
} 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)" data-key="${p.param_key}">`;
} else {
inputHtml = `<input type="text" value="${val}" onchange="setParam('${p.param_key}', this.value)" data-key="${p.param_key}">`;
}
const unitHtml = p.unit ? `<span class="param-unit">${p.unit}</span>` : '';
return `
<div class="param-item ${modifiedClass}" data-key="${p.param_key}">
<span class="param-flag">${flag}</span>
${vramBadge}
<span class="param-desc-text">${p.description}</span>
${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;
// Only update modified visual state, command, and estimate — do NOT re-render
const item = document.querySelector(`.param-item[data-key="${key}"]`);
if (item) {
const p = state.params.find(p => p.param_key === key);
if (p && isParamModified(p, value)) {
item.classList.add('modified');
} else {
item.classList.remove('modified');
}
}
generateCommand();
updateEstimate();
}
function toggleHiddenParams() {
state.showHidden = !state.showHidden;
renderParams();
}
// ===== 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 };
if (state.selectedModel) {
params._model_size_gb = state.selectedModel.size_gb;
params._model_layers = state.selectedModel.layers;
params._model_embd = state.selectedModel.embd;
params._model_kv_heads = state.selectedModel.kv_heads;
params._model_head_dim = state.selectedModel.head_dim;
params._model_heads = state.selectedModel.attention_heads;
} else {
params._model_size_gb = 0;
params._model_layers = 0;
params._model_embd = 0;
params._model_kv_heads = 0;
params._model_head_dim = 0;
params._model_heads = 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) + '%';
bar.className = 'vram-bar' + (percent > 90 ? ' danger' : percent > 75 ? ' warning' : '');
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) + '%';
bar.className = 'vram-bar ram-bar' + (percent > 90 ? ' danger' : percent > 75 ? ' warning' : '');
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>
`;
}
// ===== 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();
let resultHtml = '<div>解析结果:</div>';
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>`;
}
}
if (data._mode) {
switchMode(data._mode);
resultHtml += `<span class="parsed-param"><span class="key">模式:</span> <span class="value">${data._mode}</span></span>`;
}
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 };
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;
// Use textarea fallback for non-HTTPS environments
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
const btn = document.querySelector('.btn-copy');
const orig = btn.textContent;
btn.textContent = '✅ 已复制!';
setTimeout(() => btn.textContent = orig, 2000);
} catch(e) {
alert('复制失败,请手动选择文本复制');
}
document.body.removeChild(ta);
}
// ===== Init =====
window.addEventListener('DOMContentLoaded', init);