Add config.json support with auto-populate and reset features

- Added ConfigParser module for parsing Hugging Face config files
- Added model name display above form
- Added file upload for config.json (accepts only .json files)
- Added reset button to clear all fields
- Added error indicators (!) with language-aware messages for missing fields
- Auto-populates fields: num_hidden_layers, num_key_value_heads, head_dim,
  num_attention_heads, max_position_embeddings, full_attention_interval
- Sets defaults for optional fields: parallel=1, model_size=0
- Auto-calculates after successful config upload
- Default quantization set to f16
This commit is contained in:
Arseniy Romenskiy 2026-04-12 01:12:53 +03:00
parent c471e1d0a9
commit b8352ebd17
3 changed files with 290 additions and 1 deletions

View file

@ -300,6 +300,49 @@ html[lang="en"] .tooltip-icon:hover::after {
text-align: left;
}
.model-name {
text-align: center;
font-weight: bold;
margin-bottom: 15px;
color: #0000FF;
}
.error-icon {
display: none;
font-size: 14px;
font-weight: bold;
line-height: 1;
cursor: help;
user-select: none;
}
.error-icon:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background-color: #FF0000;
color: #FFFFFF;
padding: 8px 12px;
font-size: 11px;
white-space: pre-wrap;
max-width: 300px;
z-index: 1000;
line-height: 1.4;
text-align: center;
}
#reset-btn {
background-color: #333333;
width: auto;
padding: 8px 16px;
}
#reset-btn:hover {
background-color: #555555;
}
@media (max-width: 600px) {
.container {
padding: 15px;

View file

@ -13,6 +13,9 @@
<button id="lang-toggle" class="lang-btn">EN</button>
</div>
<!-- Model name display -->
<div id="model-name-display" class="model-name"></div>
<form id="calculator-form">
<div class="form-group">
<label for="context-length" data-key="context_length_label">Длина Контекста (токены)</label>
@ -79,6 +82,14 @@
<span class="tooltip-icon" data-tooltip-ru="Интервал слоев для полного внимания. Слои считают полный KV кеш каждые N слоев. Уменьшает эффективное количество слоев" data-tooltip-en="Interval for full attention layers. Layers compute full KV cache every N layers. Reduces effective layer count"></span>
</div>
<div class="form-group" style="display: flex; align-items: center; gap: 10px;">
<label for="config-file" data-key="config_file_label" style="margin-bottom: 0; white-space: nowrap;">Config.json</label>
<input type="file" id="config-file" accept=".json" style="flex: 1; margin-bottom: 0;">
<button type="button" id="reset-btn" data-key="reset_btn" style="margin-bottom: 0;">Сбросить</button>
</div>
<span class="tooltip-icon" data-tooltip-ru="Загрузите config.json из модели. Автоматически заполнит поля и запустит расчет" data-tooltip-en="Upload model config.json. Auto-fills fields and runs calculation"></span>
<span class="error-icon" style="display:none; color: #FF0000; cursor: help; margin-left: 5px;">!</span>
<div class="checkbox-group">
<input type="checkbox" id="asymmetric">
<label for="asymmetric" data-key="asymmetric_label">Асимметричный Контекст</label>

237
js/app.js
View file

@ -29,6 +29,10 @@
asymmetric_label: 'Асимметричный Контекст',
v_type_label: 'Квантование V кеша',
calculate_btn: 'Рассчитать',
reset_btn: 'Сбросить',
config_file_label: 'Config.json',
config_file_ru: 'Загрузите config.json из модели. Автоматически заполнит поля и запустит расчет',
config_file_en: 'Upload model config.json. Auto-fills fields and runs calculation',
example_text: 'Пример: context=8192, layers=32, kv_heads=32, head_size=128, model_size=7 GB, parallel=1',
results_title: 'Результаты',
k_cache: 'K кеш:',
@ -64,6 +68,10 @@
asymmetric_label: 'Asymmetric Context',
v_type_label: 'V cache quantization',
calculate_btn: 'Calculate',
reset_btn: 'Reset',
config_file_label: 'Config.json',
config_file_ru: 'Загрузите config.json из модели. Автоматически заполнит поля и запустит расчет',
config_file_en: 'Upload model config.json. Auto-fills fields and runs calculation',
example_text: 'Example: context=8192, layers=32, kv_heads=32, head_size=128, model_size=7 GB, parallel=1',
results_title: 'Results',
k_cache: 'K cache:',
@ -80,6 +88,55 @@
}
};
// ConfigParser module
const ConfigParser = {
defaults: { parallel: 1, model_size: 0 },
parse(config) {
const result = {
modelName: config.model_type || 'Unknown',
fields: {},
errors: {},
warnings: {}
};
const textConfig = config.text_config || config;
// Required fields
if (config.num_hidden_layers) {
result.fields['num-layers'] = config.num_hidden_layers;
}
if (config.num_key_value_heads) {
result.fields['kv-heads'] = config.num_key_value_heads;
}
if (textConfig.head_dim) {
result.fields['head-size'] = textConfig.head_dim;
} else if (textConfig.hidden_size && textConfig.num_attention_heads) {
result.fields['head-size'] = Math.round(textConfig.hidden_size / textConfig.num_attention_heads);
}
if (config.num_attention_heads) {
result.fields['num-heads'] = config.num_attention_heads;
}
if (textConfig.max_position_embeddings) {
result.fields['context-length'] = textConfig.max_position_embeddings;
}
if (textConfig.full_attention_interval) {
result.fields['full-attention'] = textConfig.full_attention_interval;
}
// Optional fields with defaults
result.fields['parallel'] = config.parallel || this.defaults.parallel;
result.fields['model-size'] = config.model_size_gb || this.defaults.model_size;
return result;
}
};
// Tooltip translations
const tooltips = {
ru: {
@ -121,6 +178,9 @@
const resultsContainer = document.getElementById('results');
const exampleText = document.getElementById('example-text');
const langToggleBtn = document.getElementById('lang-toggle');
const configFileInput = document.getElementById('config-file');
const resetBtn = document.getElementById('reset-btn');
const modelNameDisplay = document.getElementById('model-name-display');
// Initialize
function init() {
@ -140,6 +200,16 @@
// Language toggle
langToggleBtn.addEventListener('click', toggleLanguage);
// Config file upload
if (configFileInput) {
configFileInput.addEventListener('change', handleConfigUpload);
}
// Reset button
if (resetBtn) {
resetBtn.addEventListener('click', handleReset);
}
}
// Toggle language
@ -181,6 +251,23 @@
currentLang === 'ru' ? 'Опционально, для общего объема памяти' : 'Optional, for total memory';
document.getElementById('full-attention').placeholder =
currentLang === 'ru' ? 'Опционально' : 'Optional';
// Update config field translations
const configLabel = document.querySelector('#config-file + label');
if (configLabel) {
configLabel.textContent = translations[currentLang].config_file_label;
}
const configTooltip = document.querySelector('#config-file + .tooltip-icon');
if (configTooltip) {
configTooltip.setAttribute('data-tooltip-ru', translations[currentLang].config_file_ru);
configTooltip.setAttribute('data-tooltip-en', translations[currentLang].config_file_en);
}
// Re-evaluate errors
if (window.configErrors && Object.keys(window.configErrors).length > 0) {
showConfigErrors();
}
}
// Update tooltips
@ -326,6 +413,148 @@
};
}
// Config file upload handler
function handleConfigUpload(event) {
const file = event.target.files[0];
if (!file) return;
// Only accept .json files
if (!file.name.toLowerCase().endsWith('.json')) {
showConfigError('config-file', currentLang === 'ru'
? 'Неверный формат файла. Только .json файлы.'
: 'Invalid file format. Only .json files allowed.');
return;
}
const reader = new FileReader();
reader.onload = function(e) {
try {
const config = JSON.parse(e.target.result);
populateFromConfig(config);
} catch (err) {
showConfigError('config-file', currentLang === 'ru'
? 'Неверный JSON формат.'
: 'Invalid JSON format.');
}
};
reader.readAsText(file);
}
// Populate form from config
function populateFromConfig(config) {
// Set default quantization
document.getElementById('k-type').value = 'f16';
document.getElementById('v-type').value = 'f16';
// Parse config using ConfigParser
const parsed = ConfigParser.parse(config);
// Clear previous errors
window.configErrors = {};
// Display model name
modelNameDisplay.textContent = parsed.modelName;
// Set field values
Object.keys(parsed.fields).forEach(fieldId => {
const input = document.getElementById(fieldId);
if (input) {
input.value = parsed.fields[fieldId];
}
});
// Show warnings for default values
if (config.parallel === undefined) {
showConfigError('parallel', currentLang === 'ru'
? 'Не найден параметр parallel. Использовано значение по умолчанию: 1.'
: 'parallel not found. Default value used: 1.');
}
if (config.model_size_gb === undefined) {
showConfigError('model-size', currentLang === 'ru'
? 'Не найден параметр model_size_gb. Использовано значение по умолчанию: 0.'
: 'model_size_gb not found. Default value used: 0.');
}
// Show errors for missing required fields
if (!config.num_hidden_layers) {
showConfigError('num-layers', currentLang === 'ru'
? 'Не найден параметр num_hidden_layers. Важно для расчёта количества слоёв в KV кеше.'
: 'num_hidden_layers not found. Important for calculating KV cache layers.');
}
if (!config.num_key_value_heads) {
showConfigError('kv-heads', currentLang === 'ru'
? 'Не найден параметр num_key_value_heads. Важно для расчёта голов внимания.'
: 'num_key_value_heads not found. Important for attention head calculation.');
}
const textConfig = config.text_config || config;
if (!textConfig.head_dim && !(textConfig.hidden_size && textConfig.num_attention_heads)) {
showConfigError('head-size', currentLang === 'ru'
? 'Не найден параметр head_dim. Важно для размера каждой головы.'
: 'head_dim not found. Important for head size calculation.');
}
// Clear and show errors
clearAllTooltips();
showConfigErrors();
// Auto-calculate
handleCalculate();
}
// Reset button handler
function handleReset() {
// Clear all form fields
document.getElementById('context-length').value = '';
document.getElementById('kv-heads').value = '';
document.getElementById('head-size').value = '';
document.getElementById('num-heads').value = '';
document.getElementById('num-layers').value = '';
document.getElementById('model-size').value = '';
document.getElementById('parallel').value = '1';
document.getElementById('full-attention').value = '';
// Reset quantization
document.getElementById('k-type').value = 'KV';
document.getElementById('v-type').value = 'f16';
document.getElementById('asymmetric').checked = false;
document.getElementById('asymmetric-controls').classList.remove('visible');
// Clear model name
modelNameDisplay.textContent = '';
// Clear errors
window.configErrors = {};
clearAllTooltips();
}
// Show config error
function showConfigError(field, message) {
window.configErrors[field] = message;
}
// Clear all tooltips
function clearAllTooltips() {
document.querySelectorAll('.tooltip-icon').forEach(icon => {
icon.removeAttribute('data-tooltip-ru');
icon.removeAttribute('data-tooltip-en');
});
document.querySelectorAll('.error-icon').forEach(icon => {
icon.style.display = 'none';
});
}
// Show config errors
function showConfigErrors() {
Object.keys(window.configErrors).forEach(fieldId => {
const errorIcon = document.querySelector(`#${fieldId} ~ .error-icon`);
if (errorIcon) {
errorIcon.style.display = 'inline-block';
errorIcon.setAttribute('data-tooltip-ru', window.configErrors[fieldId]);
errorIcon.setAttribute('data-tooltip-en', window.configErrors[fieldId]);
}
});
}
// Expose functions globally for testing
window.CalculatorApp = {
collectParams,
@ -334,7 +563,13 @@
updateExampleText,
toggleLanguage,
updateText,
updateTooltips
updateTooltips,
handleConfigUpload,
populateFromConfig,
handleReset,
showConfigError,
clearAllTooltips,
showConfigErrors
};
// Initialize when DOM is ready