paper-burner/js/ui/ui-model-panels.js

591 lines
30 KiB
JavaScript
Raw Permalink 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.

// js/ui/ui-model-panels.js
// 提供各个模型信息面板的渲染逻辑与 DeepLX 辅助工具。
(function(global) {
'use strict';
const DEEPLX_DEFAULT_ENDPOINT_TEMPLATE = 'https://api.deeplx.org/<api-key>/translate';
const DEEPLX_LANGUAGE_ORDER = ['BG','ZH','CS','DA','NL','EN','ET','FI','FR','DE','EL','HU','IT','JA','LV','LT','PL','PT','RO','RU'];
const DEEPLX_FALLBACK_LANG_DISPLAY = {
'BG': { zh: '保加利亚语', native: 'Български' },
'ZH': { zh: '中文', native: '中文' },
'CS': { zh: '捷克语', native: 'Česky' },
'DA': { zh: '丹麦语', native: 'Dansk' },
'NL': { zh: '荷兰语', native: 'Nederlands' },
'EN': { zh: '英语', native: 'English' },
'ET': { zh: '爱沙尼亚语', native: 'Eesti' },
'FI': { zh: '芬兰语', native: 'Suomi' },
'FR': { zh: '法语', native: 'Français' },
'DE': { zh: '德语', native: 'Deutsch' },
'EL': { zh: '希腊语', native: 'Ελληνικά' },
'HU': { zh: '匈牙利语', native: 'Magyar' },
'IT': { zh: '意大利语', native: 'Italiano' },
'JA': { zh: '日语', native: '日本語' },
'LV': { zh: '拉脱维亚语', native: 'Latviešu' },
'LT': { zh: '立陶宛语', native: 'Lietuvių' },
'PL': { zh: '波兰语', native: 'Polski' },
'PT': { zh: '葡萄牙语', native: 'Português' },
'RO': { zh: '罗马尼亚语', native: 'Română' },
'RU': { zh: '俄语', native: 'Русский' }
};
if (typeof global !== 'undefined') {
global.getDeeplxLangDisplay = function(code) {
const displayMap = (typeof global.DEEPLX_LANG_DISPLAY === 'object' && global.DEEPLX_LANG_DISPLAY) || {};
return displayMap[code] || DEEPLX_FALLBACK_LANG_DISPLAY[code] || null;
};
}
function getDeeplxEndpointTemplate() {
const cfg = typeof global.loadModelConfig === 'function' ? (global.loadModelConfig('deeplx') || {}) : {};
if (cfg && typeof cfg.endpointTemplate === 'string' && cfg.endpointTemplate.trim()) {
return cfg.endpointTemplate.trim();
}
if (cfg && typeof cfg.apiBaseUrlTemplate === 'string' && cfg.apiBaseUrlTemplate.trim()) {
return cfg.apiBaseUrlTemplate.trim();
}
if (cfg && typeof cfg.apiBaseUrl === 'string' && cfg.apiBaseUrl.trim()) {
const base = cfg.apiBaseUrl.trim();
return base.endsWith('/') ? `${base}<api-key>/translate` : `${base}/<api-key>/translate`;
}
return DEEPLX_DEFAULT_ENDPOINT_TEMPLATE;
}
function setupDeeplxEndpointInput(inputEl, resetBtn) {
if (!inputEl) return;
const template = getDeeplxEndpointTemplate();
inputEl.value = template;
inputEl.placeholder = DEEPLX_DEFAULT_ENDPOINT_TEMPLATE;
if (inputEl.dataset.bound === 'true') {
return;
}
const saveTemplate = (value, notify = true) => {
const sanitized = value && value.trim() ? value.trim() : DEEPLX_DEFAULT_ENDPOINT_TEMPLATE;
if (typeof global.saveModelConfig === 'function') {
const existing = typeof global.loadModelConfig === 'function' ? (global.loadModelConfig('deeplx') || {}) : {};
global.saveModelConfig('deeplx', { ...existing, endpointTemplate: sanitized });
}
if (notify && typeof global.showNotification === 'function') {
global.showNotification('DeepLX 接口模板已保存', 'success');
}
inputEl.value = sanitized;
};
inputEl.addEventListener('blur', () => saveTemplate(inputEl.value, true));
inputEl.addEventListener('keydown', (evt) => {
if (evt.key === 'Enter') {
evt.preventDefault();
inputEl.blur();
}
});
if (resetBtn) {
resetBtn.addEventListener('click', () => {
inputEl.value = DEEPLX_DEFAULT_ENDPOINT_TEMPLATE;
saveTemplate(DEEPLX_DEFAULT_ENDPOINT_TEMPLATE, true);
});
}
inputEl.dataset.bound = 'true';
}
function renderGeminiInfoPanel() {
const keys = typeof global.loadModelKeys === 'function' ? (global.loadModelKeys('gemini') || []) : [];
const usableKeys = keys.filter(k => k.status !== 'invalid' && k.value);
const keysHint = document.getElementById('geminiKeysCountHint');
if (keysHint) keysHint.textContent = usableKeys.length > 0 ? `可用Key: ${usableKeys.length}` : '无可用 Key';
const selectEl = document.getElementById('geminiModelSelect');
const hintEl = document.getElementById('geminiModelHint');
const searchBtn = document.getElementById('geminiModelSearchBtn');
const cfg = typeof global.loadModelConfig === 'function' ? (global.loadModelConfig('gemini') || {}) : {};
const preferred = cfg.preferredModelId || cfg.modelId || '';
if (selectEl && selectEl.dataset.initialized !== 'true') {
selectEl.innerHTML = '';
if (preferred) {
const opt = document.createElement('option');
opt.value = preferred; opt.textContent = `${preferred}(当前)`;
selectEl.appendChild(opt);
} else {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = '(未设置,点击检测获取列表)';
selectEl.appendChild(opt);
}
selectEl.dataset.initialized = 'true';
}
if (selectEl) {
selectEl.onchange = function() {
const val = this.value;
if (!val) return;
if (typeof global.saveModelConfig === 'function') global.saveModelConfig('gemini', { preferredModelId: val });
if (typeof global.showNotification === 'function') global.showNotification(`Gemini 默认模型已设为 ${val}`, 'success');
};
if (!global.getModelSearchCache('gemini').length) {
global.setModelSearchCache('gemini', global.buildSearchItemsFromSelect(selectEl));
}
}
global.registerModelSearchIntegration({
key: 'gemini',
selectEl,
buttonEl: searchBtn,
title: '选择 Gemini 模型',
placeholder: '搜索模型 ID 或名称...',
emptyMessage: '未找到匹配的 Gemini 模型',
onEmpty: () => {
const detectBtn = document.getElementById('geminiDetectBtn');
if (detectBtn && !detectBtn.disabled) {
detectBtn.click();
} else if (typeof global.showNotification === 'function') {
global.showNotification('请先检测可用模型', 'info');
}
return true;
}
});
const detectBtn = document.getElementById('geminiDetectBtn');
if (!detectBtn) return;
detectBtn.onclick = async function() {
if (usableKeys.length === 0) {
if (typeof global.showNotification === 'function') global.showNotification('请先在“模型与Key管理”中添加 Gemini 的 API Key', 'warning');
return;
}
detectBtn.disabled = true;
detectBtn.textContent = '检测中...';
try {
const apiKey = usableKeys[0].value.trim();
const resp = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`);
if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
const data = await resp.json();
const items = Array.isArray(data.models || data.data) ? (data.models || data.data) : [];
if (!selectEl) return;
if (items.length === 0) {
selectEl.innerHTML = '<option value="">(未返回模型列表)</option>';
if (hintEl) hintEl.textContent = '未返回模型列表。';
global.setModelSearchCache('gemini', []);
} else {
selectEl.innerHTML = '';
const seen = new Set();
const normalized = [];
items.forEach(m => {
const fullName = m && (m.name || m.id || '');
if (!fullName) return;
const id = fullName.includes('/') ? fullName.split('/').pop() : fullName;
if (!id || seen.has(id)) return;
seen.add(id);
const display = m.displayName && m.displayName !== id ? m.displayName : '';
const description = display || m.description || '';
normalized.push({ value: id, label: id, description });
const opt = document.createElement('option');
opt.value = id;
opt.textContent = id;
if (preferred && id === preferred) opt.selected = true;
selectEl.appendChild(opt);
});
if (preferred && !seen.has(preferred)) {
const opt = document.createElement('option');
opt.value = preferred;
opt.textContent = `${preferred}(当前)`;
opt.selected = true;
selectEl.insertBefore(opt, selectEl.firstChild);
normalized.unshift({ value: preferred, label: preferred, description: '' });
}
global.setModelSearchCache('gemini', normalized);
if (hintEl) hintEl.textContent = '从列表中选择默认模型。';
}
} catch (e) {
console.error(e);
if (hintEl) hintEl.textContent = `检测失败:${e.message}`;
global.setModelSearchCache('gemini', []);
} finally {
detectBtn.disabled = false;
detectBtn.textContent = '检测可用模型';
}
};
}
function renderDeepseekInfoPanel() {
const keys = typeof global.loadModelKeys === 'function' ? (global.loadModelKeys('deepseek') || []) : [];
const usableKeys = keys.filter(k => k.status !== 'invalid' && k.value);
const keysHint = document.getElementById('deepseekKeysCountHint');
if (keysHint) keysHint.textContent = usableKeys.length > 0 ? `可用Key: ${usableKeys.length}` : '无可用 Key';
const selectEl = document.getElementById('deepseekModelSelect');
const hintEl = document.getElementById('deepseekModelHint');
const searchBtn = document.getElementById('deepseekModelSearchBtn');
const cfg = typeof global.loadModelConfig === 'function' ? (global.loadModelConfig('deepseek') || {}) : {};
const preferred = cfg.preferredModelId || cfg.modelId || '';
if (selectEl && selectEl.options.length === 0) {
const opt = document.createElement('option');
opt.value = preferred;
opt.textContent = preferred ? `${preferred}(当前)` : '(未设置,点击检测获取列表)';
selectEl.appendChild(opt);
}
if (selectEl) {
selectEl.onchange = function(){
if (!this.value) return;
if (typeof global.saveModelConfig === 'function') global.saveModelConfig('deepseek', { preferredModelId: this.value });
if (typeof global.showNotification === 'function') global.showNotification(`DeepSeek 默认模型已设为 ${this.value}`, 'success');
};
if (!global.getModelSearchCache('deepseek').length) {
global.setModelSearchCache('deepseek', global.buildSearchItemsFromSelect(selectEl));
}
}
global.registerModelSearchIntegration({
key: 'deepseek',
selectEl,
buttonEl: searchBtn,
title: '选择 DeepSeek 模型',
placeholder: '搜索模型 ID...',
emptyMessage: '未找到匹配的 DeepSeek 模型',
onEmpty: () => {
const detectBtn = document.getElementById('deepseekDetectBtn');
if (detectBtn && !detectBtn.disabled) {
detectBtn.click();
} else if (typeof global.showNotification === 'function') {
global.showNotification('请先检测可用模型', 'info');
}
return true;
}
});
const detectBtn = document.getElementById('deepseekDetectBtn');
if (!detectBtn) return;
detectBtn.onclick = async function(){
if (usableKeys.length === 0) {
if (typeof global.showNotification === 'function') global.showNotification('请先在Key管理中添加 DeepSeek Key','warning');
return;
}
detectBtn.disabled = true;
detectBtn.textContent = '检测中...';
try {
const apiKey = usableKeys[0].value.trim();
const resp = await fetch('https://api.deepseek.com/v1/models', { headers: { 'Authorization': `Bearer ${apiKey}` } });
if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
const data = await resp.json();
const items = Array.isArray(data.data) ? data.data : [];
if (items.length === 0) {
if (hintEl) hintEl.textContent = '未返回模型列表';
if (selectEl) selectEl.innerHTML = '<option value="">(未返回模型列表)</option>';
global.setModelSearchCache('deepseek', []);
return;
}
if (selectEl) {
selectEl.innerHTML='';
}
const normalized = [];
items.forEach(m => {
const id = m.id;
if (!id) return;
const opt = document.createElement('option');
opt.value = id;
opt.textContent = id;
if (preferred && id === preferred) opt.selected = true;
if (selectEl) selectEl.appendChild(opt);
normalized.push({ value: id, label: id });
});
if (preferred && !normalized.some(item => item.value === preferred)) {
const opt = document.createElement('option');
opt.value = preferred;
opt.textContent = `${preferred}(当前)`;
opt.selected = true;
if (selectEl) selectEl.insertBefore(opt, selectEl.firstChild);
normalized.unshift({ value: preferred, label: preferred });
}
global.setModelSearchCache('deepseek', normalized);
if (hintEl) hintEl.textContent = '从列表中选择默认模型。';
} catch (e) {
if (hintEl) hintEl.textContent = `检测失败:${e.message}`;
global.setModelSearchCache('deepseek', []);
} finally {
detectBtn.disabled = false;
detectBtn.textContent = '检测可用模型';
}
};
}
function renderDeeplxInfoPanel() {
const keys = typeof global.loadModelKeys === 'function' ? (global.loadModelKeys('deeplx') || []) : [];
const usableKeys = keys.filter(k => k.status !== 'invalid' && k.value);
const keysHint = document.getElementById('deeplxKeysCountHint');
if (keysHint) {
keysHint.textContent = usableKeys.length > 0 ? `可用Key: ${usableKeys.length}` : '无可用 Key';
}
const inputEl = document.getElementById('deeplxEndpointTemplateInput');
const resetBtn = document.getElementById('deeplxEndpointResetBtn');
setupDeeplxEndpointInput(inputEl, resetBtn);
const tableEl = document.getElementById('deeplxLangTable');
if (tableEl && tableEl.dataset.initialized !== 'true') {
const rows = DEEPLX_LANGUAGE_ORDER.map(function(code) {
const info = (typeof global.getDeeplxLangDisplay === 'function')
? global.getDeeplxLangDisplay(code)
: (global.DEEPLX_LANG_DISPLAY && global.DEEPLX_LANG_DISPLAY[code]) || DEEPLX_FALLBACK_LANG_DISPLAY[code] || {};
const zhName = info.zh || '--';
const nativeName = info.native || '';
return `<li class="py-0.5 flex justify-between"><code>${code}</code><span class="flex-1 px-2 text-left">${zhName}</span><span>${nativeName}</span></li>`;
}).join('');
tableEl.innerHTML = `
<details class="bg-slate-100 border border-slate-200 rounded-md px-3 py-2">
<summary class="cursor-pointer text-sm text-slate-700 select-none">常用语言代码对照(点击展开)</summary>
<ul class="mt-2 text-xs text-slate-700 space-y-1">${rows}</ul>
</details>`;
tableEl.dataset.initialized = 'true';
}
if (typeof global.updateDeeplxTargetLangHint === 'function') {
global.updateDeeplxTargetLangHint();
}
}
function renderTongyiInfoPanel() {
let keys = [];
if (typeof global.loadModelKeys === 'function') {
keys = global.loadModelKeys('tongyi') || [];
}
const usableKeys = keys.filter(k => k.status !== 'invalid' && k.value);
const keysHint = document.getElementById('tongyiKeysCountHint');
if (keysHint) keysHint.textContent = usableKeys.length > 0 ? `可用Key: ${usableKeys.length}` : '无可用 Key';
const selectEl = document.getElementById('tongyiModelSelect');
const hintEl = document.getElementById('tongyiModelHint');
const searchBtn = document.getElementById('tongyiModelSearchBtn');
const cfg = typeof global.loadModelConfig === 'function' ? (global.loadModelConfig('tongyi') || {}) : {};
const preferred = cfg.preferredModelId || cfg.modelId || '';
if (selectEl && selectEl.options.length === 0) {
const opt = document.createElement('option');
opt.value = preferred;
opt.textContent = preferred ? `${preferred}(当前)` : '(未设置,请手动输入或在设置中选择)';
selectEl.appendChild(opt);
}
if (selectEl) {
selectEl.onchange = function(){
if (!this.value) return;
if (typeof global.saveModelConfig === 'function') global.saveModelConfig('tongyi', { preferredModelId: this.value });
if (typeof global.showNotification === 'function') global.showNotification(`通义 默认模型已设为 ${this.value}`, 'success');
};
if (!global.getModelSearchCache('tongyi').length) {
global.setModelSearchCache('tongyi', global.buildSearchItemsFromSelect(selectEl));
}
}
global.registerModelSearchIntegration({
key: 'tongyi',
selectEl,
buttonEl: searchBtn,
title: '选择通义模型',
placeholder: '搜索模型 ID...',
emptyMessage: '未找到匹配的通义模型',
onEmpty: () => {
const detectBtn = document.getElementById('tongyiDetectBtn');
if (detectBtn && !detectBtn.disabled) {
detectBtn.click();
} else if (typeof global.showNotification === 'function') {
global.showNotification('请先检测可用模型', 'info');
}
return true;
}
});
const detectBtn = document.getElementById('tongyiDetectBtn');
if (!detectBtn) return;
detectBtn.onclick = async function(){
if (usableKeys.length === 0) {
if (typeof global.showNotification === 'function') global.showNotification('请先在Key管理中添加 通义 Key','warning');
return;
}
detectBtn.disabled = true;
detectBtn.textContent='检测中...';
try {
const apiKey = usableKeys[0].value.trim();
const resp = await fetch('https://dashscope.aliyuncs.com/compatible-mode/v1/models', { headers: { 'Authorization': `Bearer ${apiKey}` } });
if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
const data = await resp.json();
const items = Array.isArray(data.data) ? data.data : (Array.isArray(data.models) ? data.models : (Array.isArray(data?.data?.models) ? data.data.models : []));
if (!items || items.length === 0) {
if (hintEl) hintEl.textContent = '未返回模型列表';
if (selectEl) selectEl.innerHTML = '<option value="">(未返回模型列表)</option>';
global.setModelSearchCache('tongyi', []);
return;
}
if (selectEl) selectEl.innerHTML = '';
const normalized = [];
items.forEach(m => {
const id = m.model || m.id || m.name;
if (!id) return;
const opt = document.createElement('option');
opt.value = id;
opt.textContent = id;
if (preferred && id === preferred) opt.selected = true;
if (selectEl) selectEl.appendChild(opt);
normalized.push({ value: id, label: id });
});
if (preferred && !normalized.some(item => item.value === preferred)) {
const opt = document.createElement('option');
opt.value = preferred;
opt.textContent = `${preferred}(当前)`;
opt.selected = true;
if (selectEl) selectEl.insertBefore(opt, selectEl.firstChild);
normalized.unshift({ value: preferred, label: preferred });
}
global.setModelSearchCache('tongyi', normalized);
if (hintEl) hintEl.textContent = '从列表中选择默认模型。';
} catch (e) {
if (hintEl) hintEl.textContent = `检测失败:${e.message}`;
global.setModelSearchCache('tongyi', []);
} finally {
detectBtn.disabled = false;
detectBtn.textContent = '检测可用模型';
}
};
}
function renderVolcanoInfoPanel() {
let keys = [];
if (typeof global.loadModelKeys === 'function') {
keys = global.loadModelKeys('volcano') || [];
}
const usableKeys = keys.filter(k => k.status !== 'invalid' && k.value);
const keysHint = document.getElementById('volcanoKeysCountHint');
if (keysHint) keysHint.textContent = usableKeys.length > 0 ? `可用Key: ${usableKeys.length}` : '无可用 Key';
const selectEl = document.getElementById('volcanoModelSelect');
const hintEl = document.getElementById('volcanoModelHint');
const cfg = typeof global.loadModelConfig === 'function' ? (global.loadModelConfig('volcano') || {}) : {};
const preferred = cfg.preferredModelId || cfg.modelId || '';
if (selectEl) {
const seenValues = new Set();
Array.from(selectEl.options).forEach(opt => {
const key = opt.value || '__empty__';
if (seenValues.has(key) && key !== '__empty__') {
opt.remove();
} else {
seenValues.add(key);
}
});
if (selectEl.options.length === 0) {
const opt = document.createElement('option');
opt.value = preferred;
opt.textContent = preferred ? `${preferred}(当前)` : '(未设置,请手动输入或在设置中选择)';
selectEl.appendChild(opt);
}
selectEl.onchange = function(){
if (!this.value) return;
if (typeof global.saveModelConfig === 'function') {
global.saveModelConfig('volcano', { preferredModelId: this.value });
}
Array.from(selectEl.options).forEach(opt => {
if (opt.value === this.value) {
opt.textContent = `${opt.value}(当前)`;
} else if (opt.value) {
opt.textContent = opt.value;
} else {
opt.textContent = '(未设置,请手动输入或在设置中选择)';
}
});
if (typeof global.showNotification === 'function') {
global.showNotification(`火山 默认模型已设为 ${this.value}`, 'success');
}
global.setModelSearchCache('volcano', global.buildSearchItemsFromSelect(selectEl));
};
}
const detectBtn = document.getElementById('volcanoDetectBtn');
if (detectBtn) {
detectBtn.style.display = 'none';
if (hintEl) hintEl.textContent = '请手动输入模型ID或在设置中选择。';
}
if (selectEl) {
const parent = selectEl.parentElement;
if (parent) {
const legacyInputs = parent.querySelectorAll('#volcanoManualInput');
legacyInputs.forEach((inputEl, idx) => {
const wrap = inputEl.closest('.pbx-volcano-manual-wrap') || inputEl.parentElement;
if (idx === 0) {
if (wrap) wrap.classList.add('pbx-volcano-manual-wrap');
} else if (wrap) {
wrap.remove();
} else {
inputEl.remove();
}
});
let manualWrap = parent.querySelector('.pbx-volcano-manual-wrap');
if (!manualWrap) {
manualWrap = document.createElement('div');
manualWrap.className = 'mt-2 flex flex-col sm:flex-row sm:items-center gap-2 pbx-volcano-manual-wrap';
const inputEl = document.createElement('input');
inputEl.id = 'volcanoManualInput';
inputEl.type = 'text';
inputEl.placeholder = '例如doubao-1-5-pro-32k-250115';
inputEl.className = 'w-full sm:flex-grow px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-1 focus:ring-blue-500 focus:border-blue-500';
const saveBtn = document.createElement('button');
saveBtn.id = 'volcanoSaveModelBtn';
saveBtn.className = 'px-3 py-1.5 bg-blue-600 text-white rounded text-xs hover:bg-blue-700 transition-colors';
saveBtn.textContent = '保存为默认';
manualWrap.appendChild(inputEl);
manualWrap.appendChild(saveBtn);
parent.appendChild(manualWrap);
}
const inputEl = manualWrap.querySelector('#volcanoManualInput');
const saveBtn = manualWrap.querySelector('#volcanoSaveModelBtn');
if (inputEl) inputEl.value = preferred || '';
if (saveBtn && inputEl) {
saveBtn.onclick = function() {
const val = (inputEl.value || '').trim();
if (!val) {
if (typeof global.showNotification === 'function') global.showNotification('请输入模型ID', 'warning');
return;
}
if (typeof global.saveModelConfig === 'function') {
global.saveModelConfig('volcano', { preferredModelId: val });
}
let matchedOption = Array.from(selectEl.options).find(opt => opt.value === val);
if (!matchedOption) {
matchedOption = document.createElement('option');
matchedOption.value = val;
selectEl.appendChild(matchedOption);
}
Array.from(selectEl.options).forEach(opt => {
if (opt.value === val) {
opt.textContent = `${val}(当前)`;
} else if (opt.value) {
opt.textContent = opt.value;
} else {
opt.textContent = '(未设置,请手动输入或在设置中选择)';
}
});
selectEl.value = val;
if (typeof global.showNotification === 'function') global.showNotification(`火山 默认模型已设为 ${val}`, 'success');
};
}
}
}
}
global.DEEPLX_DEFAULT_ENDPOINT_TEMPLATE = DEEPLX_DEFAULT_ENDPOINT_TEMPLATE;
global.getDeeplxEndpointTemplate = getDeeplxEndpointTemplate;
global.setupDeeplxEndpointInput = setupDeeplxEndpointInput;
global.renderGeminiInfoPanel = renderGeminiInfoPanel;
global.renderDeepseekInfoPanel = renderDeepseekInfoPanel;
global.renderDeeplxInfoPanel = renderDeeplxInfoPanel;
global.renderTongyiInfoPanel = renderTongyiInfoPanel;
global.renderVolcanoInfoPanel = renderVolcanoInfoPanel;
})(window);