- Call updateTooltips() after clearAllTooltips() to restore tooltip attributes - Add error icons to all input fields (context-length, kv-heads, head-size, num-heads, num-layers, parallel, full-attention, hf-model) - Update showConfigErrors() to handle hf-model field correctly
671 lines
27 KiB
JavaScript
671 lines
27 KiB
JavaScript
/**
|
||
* LLM Context Memory Calculator - Main Application
|
||
* DOM manipulation and event handling
|
||
*/
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
// Translation dictionary
|
||
const translations = {
|
||
ru: {
|
||
title: 'Калькулятор Памяти Контекста LLM',
|
||
context_length_label: 'Длина Контекста (токены)',
|
||
k_type_label: 'Квантование K кеша',
|
||
kv_cache: 'KV кеш',
|
||
q8_0: 'q8_0',
|
||
q4_0: 'q4_0',
|
||
q4_1: 'q4_1',
|
||
iq4_nl: 'iq4_nl',
|
||
q5_0: 'q5_0',
|
||
q5_1: 'q5_1',
|
||
kv_heads_label: 'Головок KV',
|
||
head_size_label: 'Размер Головки',
|
||
num_heads_label: 'Количество Головок',
|
||
num_layers_label: 'Количество Слов',
|
||
model_size_label: 'Размер Модели (GB)',
|
||
parallel_label: 'Параллелизм (np)',
|
||
full_attention_label: 'Интервал Полного Внимания',
|
||
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 кеш:',
|
||
v_cache: 'V кеш:',
|
||
total_kv: 'Total KV:',
|
||
total: 'Total:',
|
||
errors_header: 'Ошибки:',
|
||
error_context_length: 'Длина контекста должна быть положительным числом',
|
||
error_kv_heads: 'Количество KV головок должно быть положительным числом',
|
||
error_head_size: 'Размер головы должен быть положительным числом',
|
||
error_num_layers: 'Количество слоев должно быть положительным числом',
|
||
error_full_attention: 'Интервал полного внимания должен быть положительным числом',
|
||
error_model_size: 'Размер модели должен быть положительным числом'
|
||
},
|
||
en: {
|
||
title: 'LLM Context Memory Calculator',
|
||
context_length_label: 'Context Length (tokens)',
|
||
k_type_label: 'K cache quantization',
|
||
kv_cache: 'KV cache',
|
||
q8_0: 'q8_0',
|
||
q4_0: 'q4_0',
|
||
q4_1: 'q4_1',
|
||
iq4_nl: 'iq4_nl',
|
||
q5_0: 'q5_0',
|
||
q5_1: 'q5_1',
|
||
kv_heads_label: 'KV Heads',
|
||
head_size_label: 'Head Size',
|
||
num_heads_label: 'Number of Heads',
|
||
num_layers_label: 'Number of Layers',
|
||
model_size_label: 'Model Size (GB)',
|
||
parallel_label: 'Parallel (np)',
|
||
full_attention_label: 'Full Attention Interval',
|
||
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',
|
||
hf_model_label: 'Model (HuggingFace)',
|
||
hf_model_ru: 'Введите имя модели',
|
||
hf_model_en: 'Please enter a model name',
|
||
hf_fetch_btn: 'Fetch',
|
||
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:',
|
||
v_cache: 'V cache:',
|
||
total_kv: 'Total KV:',
|
||
total: 'Total:',
|
||
errors_header: 'Errors:',
|
||
error_context_length: 'Context length must be a positive number',
|
||
error_kv_heads: 'KV heads must be a positive number',
|
||
error_head_size: 'Head size must be a positive number',
|
||
error_num_layers: 'Number of layers must be a positive number',
|
||
error_full_attention: 'Full attention interval must be a positive number',
|
||
error_model_size: 'Model size must be a positive number'
|
||
}
|
||
};
|
||
|
||
// 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 - check both top level and text_config
|
||
if (config.num_hidden_layers) {
|
||
result.fields['num-layers'] = config.num_hidden_layers;
|
||
} else if (textConfig.num_hidden_layers) {
|
||
result.fields['num-layers'] = textConfig.num_hidden_layers;
|
||
}
|
||
|
||
if (config.num_key_value_heads) {
|
||
result.fields['kv-heads'] = config.num_key_value_heads;
|
||
} else if (textConfig.num_key_value_heads) {
|
||
result.fields['kv-heads'] = textConfig.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;
|
||
} else if (textConfig.num_attention_heads) {
|
||
result.fields['num-heads'] = textConfig.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: {
|
||
context_length: 'Количество токенов в контексте. Влияет линейно на размер KV кеша',
|
||
k_type: 'Тип квантования для тензоров ключей внимания. Меньшее значение = меньше памяти, но потенциально выше ошибка квантования',
|
||
kv_heads: 'Количество голов внимания для K и V тензоров. Обычно num_key_value_heads в конфигурации модели',
|
||
head_size: 'Размер каждой головы внимания (head_dim). Влияет линейно на размер KV кеша',
|
||
num_heads: 'Общее количество голов внимания. Используется только для информации и примеров',
|
||
num_layers: 'Количество слоев модели. Каждый слой имеет свой KV кеш',
|
||
model_size: 'Размер весов модели в гигабайтах. Используется для расчета общего объема памяти (KV кеш + веса)',
|
||
parallel: 'Количество параллельных последовательностей для декодирования (-np/--parallel в llama.cpp). Увеличивает KV кеш пропорционально',
|
||
full_attention: 'Интервал слоев для полного внимания. Слои считают полный KV кеш каждые N слоев. Уменьшает эффективное количество слоев',
|
||
v_type: 'Тип квантования для тензоров значений внимания. Можно выбрать отдельно от K кеша'
|
||
},
|
||
en: {
|
||
context_length: 'Number of tokens in context. Affects KV cache size linearly',
|
||
k_type: 'Quantization type for attention key tensors. Lower value = less memory, but potentially higher quantization error',
|
||
kv_heads: 'Number of attention heads for K and V tensors. Usually num_key_value_heads in model config',
|
||
head_size: 'Size of each attention head (head_dim). Affects KV cache size linearly',
|
||
num_heads: 'Total number of attention heads. Used for display and examples only',
|
||
num_layers: 'Number of model layers. Each layer has its own KV cache',
|
||
model_size: 'Model weights size in gigabytes. Used to calculate total memory (KV cache + weights)',
|
||
parallel: 'Number of parallel sequences for decoding (-np/--parallel in llama.cpp). Increases KV cache proportionally',
|
||
full_attention: 'Interval for full attention layers. Layers compute full KV cache every N layers. Reduces effective layer count',
|
||
v_type: 'Quantization type for attention value tensors. Can be selected separately from K cache'
|
||
}
|
||
};
|
||
|
||
// Current language
|
||
let currentLang = 'ru';
|
||
|
||
// DOM Elements
|
||
const form = document.getElementById('calculator-form');
|
||
const kTypeSelect = document.getElementById('k-type');
|
||
const vTypeSelect = document.getElementById('v-type');
|
||
const asymmetricCheckbox = document.getElementById('asymmetric');
|
||
const asymmetricControls = document.getElementById('asymmetric-controls');
|
||
const calculateBtn = document.getElementById('calculate-btn');
|
||
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');
|
||
const hfModelInput = document.getElementById('hf-model');
|
||
const hfFetchBtn = document.getElementById('hf-fetch-btn');
|
||
const loadingIndicator = document.getElementById('loading-indicator');
|
||
|
||
// Initialize
|
||
function init() {
|
||
setupEventListeners();
|
||
updateText();
|
||
updateTooltips();
|
||
updateExampleText();
|
||
}
|
||
|
||
// Set up event listeners
|
||
function setupEventListeners() {
|
||
asymmetricCheckbox.addEventListener('change', toggleAsymmetric);
|
||
calculateBtn.addEventListener('click', handleCalculate);
|
||
|
||
// Auto-calculate on input change
|
||
form.addEventListener('input', debounce(handleCalculate, 500));
|
||
|
||
// Language toggle
|
||
langToggleBtn.addEventListener('click', toggleLanguage);
|
||
|
||
// Config file upload
|
||
if (configFileInput) {
|
||
configFileInput.addEventListener('change', handleConfigUpload);
|
||
}
|
||
|
||
// Reset button
|
||
if (resetBtn) {
|
||
resetBtn.addEventListener('click', handleReset);
|
||
}
|
||
|
||
// HF fetch button
|
||
if (hfFetchBtn) {
|
||
hfFetchBtn.addEventListener('click', handleHfFetch);
|
||
}
|
||
}
|
||
|
||
// Toggle language
|
||
function toggleLanguage() {
|
||
currentLang = currentLang === 'ru' ? 'en' : 'ru';
|
||
langToggleBtn.textContent = currentLang === 'ru' ? 'EN' : 'RU';
|
||
document.documentElement.lang = currentLang;
|
||
updateText();
|
||
updateTooltips();
|
||
updateExampleText();
|
||
}
|
||
|
||
// Update all translatable text
|
||
function updateText() {
|
||
// Translate elements with data-key attribute
|
||
document.querySelectorAll('[data-key]').forEach(el => {
|
||
const key = el.getAttribute('data-key');
|
||
if (translations[currentLang][key]) {
|
||
if (el.tagName === 'TITLE') {
|
||
document.title = translations[currentLang][key];
|
||
} else {
|
||
el.textContent = translations[currentLang][key];
|
||
}
|
||
}
|
||
});
|
||
|
||
// Update placeholder text
|
||
document.getElementById('context-length').placeholder =
|
||
currentLang === 'ru' ? 'Введите длину контекста' : 'Enter context length';
|
||
document.getElementById('kv-heads').placeholder =
|
||
currentLang === 'ru' ? 'Введите количество головок KV' : 'Enter KV heads';
|
||
document.getElementById('head-size').placeholder =
|
||
currentLang === 'ru' ? 'Введите размер головы' : 'Enter head size';
|
||
document.getElementById('num-heads').placeholder =
|
||
currentLang === 'ru' ? 'Введите количество головок' : 'Enter number of heads';
|
||
document.getElementById('num-layers').placeholder =
|
||
currentLang === 'ru' ? 'Введите количество слоев' : 'Enter number of layers';
|
||
document.getElementById('model-size').placeholder =
|
||
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);
|
||
}
|
||
|
||
// Update HF model field translation
|
||
const hfModelLabel = document.querySelector('#hf-model + label');
|
||
if (hfModelLabel) {
|
||
hfModelLabel.textContent = translations[currentLang].hf_model_label;
|
||
}
|
||
|
||
// Re-evaluate errors
|
||
if (window.configErrors && Object.keys(window.configErrors).length > 0) {
|
||
showConfigErrors();
|
||
}
|
||
}
|
||
|
||
// Update tooltips
|
||
function updateTooltips() {
|
||
const tooltipMap = {
|
||
'context-length': 'context_length',
|
||
'k-type': 'k_type',
|
||
'kv-heads': 'kv_heads',
|
||
'head-size': 'head_size',
|
||
'num-heads': 'num_heads',
|
||
'num-layers': 'num_layers',
|
||
'model-size': 'model_size',
|
||
'parallel': 'parallel',
|
||
'full-attention': 'full_attention',
|
||
'v-type': 'v_type'
|
||
};
|
||
|
||
Object.keys(tooltipMap).forEach(fieldId => {
|
||
const tooltipIcon = document.querySelector(`#${fieldId} + .tooltip-icon`);
|
||
if (tooltipIcon) {
|
||
const key = tooltipMap[fieldId];
|
||
const ruText = tooltips.ru[key];
|
||
const enText = tooltips.en[key];
|
||
|
||
if (currentLang === 'ru') {
|
||
tooltipIcon.setAttribute('data-tooltip-ru', ruText);
|
||
tooltipIcon.removeAttribute('data-tooltip-en');
|
||
} else {
|
||
tooltipIcon.setAttribute('data-tooltip-en', enText);
|
||
tooltipIcon.removeAttribute('data-tooltip-ru');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Update example text
|
||
function updateExampleText() {
|
||
const kvHeads = parseInt(document.getElementById('kv-heads').value) || 32;
|
||
const headSize = parseInt(document.getElementById('head-size').value) || 128;
|
||
const layers = parseInt(document.getElementById('num-layers').value) || 32;
|
||
const modelSize = document.getElementById('model-size').value || 7;
|
||
const context = parseInt(document.getElementById('context-length').value) || 8192;
|
||
const parallel = parseInt(document.getElementById('parallel').value) || 1;
|
||
|
||
exampleText.textContent = translations[currentLang].example_text
|
||
.replace('8192', context)
|
||
.replace('32', layers)
|
||
.replace('kv_heads=32', `kv_heads=${kvHeads}`)
|
||
.replace('head_size=128', `head_size=${headSize}`)
|
||
.replace('7 GB', `${modelSize} GB`)
|
||
.replace('parallel=1', `parallel=${parallel}`);
|
||
}
|
||
|
||
// Toggle asymmetric context controls
|
||
function toggleAsymmetric() {
|
||
if (asymmetricCheckbox.checked) {
|
||
asymmetricControls.classList.add('visible');
|
||
const kvOption = kTypeSelect.querySelector('option[value="KV"]');
|
||
if (kvOption) {
|
||
kvOption.value = 'K';
|
||
kvOption.textContent = currentLang === 'ru' ? 'K кеш' : 'K cache';
|
||
}
|
||
} else {
|
||
asymmetricControls.classList.remove('visible');
|
||
const kOption = kTypeSelect.querySelector('option[value="K"]');
|
||
if (kOption) {
|
||
kOption.value = 'KV';
|
||
kOption.textContent = translations[currentLang].kv_cache;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Handle calculate button click
|
||
function handleCalculate() {
|
||
const params = collectParams();
|
||
const validation = validateParams(params, currentLang);
|
||
|
||
if (!validation.valid) {
|
||
displayErrors(validation.errors);
|
||
return;
|
||
}
|
||
|
||
const results = calculateMemory(params);
|
||
displayResults(results);
|
||
}
|
||
|
||
// Collect parameters from form
|
||
function collectParams() {
|
||
return {
|
||
contextLength: parseInt(document.getElementById('context-length').value),
|
||
kType: asymmetricCheckbox.checked
|
||
? kTypeSelect.value === 'K' ? 'KV' : kTypeSelect.value
|
||
: 'KV',
|
||
vType: asymmetricCheckbox.checked ? vTypeSelect.value : null,
|
||
kvHeads: parseInt(document.getElementById('kv-heads').value),
|
||
headSize: parseInt(document.getElementById('head-size').value),
|
||
numLayers: parseInt(document.getElementById('num-layers').value),
|
||
modelSizeGB: parseFloat(document.getElementById('model-size').value) || null,
|
||
parallel: parseInt(document.getElementById('parallel').value) || 1,
|
||
fullAttentionInterval: document.getElementById('full-attention').value
|
||
? parseInt(document.getElementById('full-attention').value)
|
||
: null
|
||
};
|
||
}
|
||
|
||
// Display calculation results
|
||
function displayResults(results) {
|
||
let html = '<div class="results"><h2>' + translations[currentLang].results_title + '</h2>';
|
||
|
||
html += `<div class="result-item"><span class="label">${translations[currentLang].k_cache}</span><span class="value">${results.kCache.formatted}</span></div>`;
|
||
html += `<div class="result-item"><span class="label">${translations[currentLang].v_cache}</span><span class="value">${results.vCache.formatted}</span></div>`;
|
||
html += `<div class="result-item"><span class="label">${translations[currentLang].total_kv}</span><span class="value">${results.totalKVCache.formatted}</span></div>`;
|
||
|
||
if (results.totalMemory) {
|
||
html += `<div class="result-item total"><span class="label">${translations[currentLang].total}</span><span class="value">${results.totalMemory.formatted}</span></div>`;
|
||
}
|
||
|
||
html += '</div>';
|
||
|
||
resultsContainer.innerHTML = html;
|
||
}
|
||
|
||
// Display validation errors
|
||
function displayErrors(errors) {
|
||
let html = '<div class="error-message"><strong>' + translations[currentLang].errors_header + '</strong><ul>';
|
||
errors.forEach(error => {
|
||
html += `<li>${error}</li>`;
|
||
});
|
||
html += '</ul></div>';
|
||
resultsContainer.innerHTML = html;
|
||
}
|
||
|
||
// Debounce function for performance
|
||
function debounce(func, wait) {
|
||
let timeout;
|
||
return function executedFunction(...args) {
|
||
const later = () => {
|
||
clearTimeout(timeout);
|
||
func(...args);
|
||
};
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(later, wait);
|
||
};
|
||
}
|
||
|
||
// 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';
|
||
|
||
// Get text_config if available
|
||
const textConfig = config.text_config || config;
|
||
|
||
// 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 && (!textConfig || textConfig.parallel === undefined)) {
|
||
showConfigError('parallel', currentLang === 'ru'
|
||
? 'Не найден параметр parallel. Использовано значение по умолчанию: 1.'
|
||
: 'parallel not found. Default value used: 1.');
|
||
}
|
||
if (config.model_size_gb === undefined && (!textConfig || textConfig.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 && (!textConfig || !textConfig.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 && (!textConfig || !textConfig.num_key_value_heads)) {
|
||
showConfigError('kv-heads', currentLang === 'ru'
|
||
? 'Не найден параметр num_key_value_heads. Важно для расчёта голов внимания.'
|
||
: 'num_key_value_heads not found. Important for attention head calculation.');
|
||
}
|
||
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 = '';
|
||
hfModelInput.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();
|
||
}
|
||
|
||
// HuggingFace fetch function
|
||
async function handleHfFetch() {
|
||
const modelName = hfModelInput.value.trim();
|
||
if (!modelName) {
|
||
showConfigError('hf-model', currentLang === 'ru'
|
||
? 'Введите имя модели'
|
||
: 'Please enter a model name');
|
||
return;
|
||
}
|
||
|
||
// Show loading state
|
||
loadingIndicator.textContent = currentLang === 'ru'
|
||
? 'Загрузка config из HuggingFace...'
|
||
: 'Fetching config from HuggingFace...';
|
||
loadingIndicator.style.display = 'block';
|
||
|
||
const url = `https://huggingface.co/${modelName}/resolve/main/config.json`;
|
||
|
||
try {
|
||
const response = await fetch(url);
|
||
if (!response.ok) {
|
||
if (response.status === 404) {
|
||
showConfigError('hf-model', currentLang === 'ru'
|
||
? 'Модель не найдена'
|
||
: 'Model not found');
|
||
} else {
|
||
showConfigError('hf-model', currentLang === 'ru'
|
||
? `Ошибка загрузки: ${response.status}`
|
||
: `Error loading: ${response.status}`);
|
||
}
|
||
loadingIndicator.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
const config = await response.json();
|
||
|
||
// Clear errors
|
||
window.configErrors = {};
|
||
clearAllTooltips();
|
||
|
||
// Populate from config
|
||
populateFromConfig(config);
|
||
|
||
// Update tooltips after clearing them
|
||
updateTooltips();
|
||
|
||
// Hide loading and set focus
|
||
loadingIndicator.style.display = 'none';
|
||
hfModelInput.focus();
|
||
|
||
} catch (err) {
|
||
loadingIndicator.style.display = 'none';
|
||
showConfigError('hf-model', currentLang === 'ru'
|
||
? `Не удалось загрузить config: ${err.message}`
|
||
: `Failed to load config: ${err.message}`);
|
||
}
|
||
}
|
||
|
||
// 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 => {
|
||
let errorIcon;
|
||
if (fieldId === 'hf-model') {
|
||
errorIcon = document.querySelector('#hf-model + .error-icon');
|
||
} else {
|
||
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,
|
||
handleCalculate,
|
||
toggleAsymmetric,
|
||
updateExampleText,
|
||
toggleLanguage,
|
||
updateText,
|
||
updateTooltips,
|
||
handleConfigUpload,
|
||
populateFromConfig,
|
||
handleReset,
|
||
showConfigError,
|
||
clearAllTooltips,
|
||
showConfigErrors
|
||
};
|
||
|
||
// Initialize when DOM is ready
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|