grok2api / app /statics /js /webui-header.js
FUCAT's picture
Deploy grok2api to HF Spaces (Docker)
7e55e53
Raw
History Blame Contribute Delete
9.02 kB
window.renderWebuiHeader = async function renderWebuiHeader() {
const mount = document.getElementById('webui-header');
if (!mount || mount.children.length) return;
const scriptVersion = (() => {
try {
const script = document.querySelector('script[src*="/static/js/webui-header.js"]');
if (!script) return 'v1';
return new URL(script.src, window.location.href).searchParams.get('v') || 'v1';
} catch {
return 'v1';
}
})();
const HEADER_HTML_CACHE_KEY = `grok2api.webui_header_html.${scriptVersion}`;
const META_VERSION_CACHE_KEY = `grok2api.meta_version.${scriptVersion}`;
let appVersion = '';
const readSessionCache = (key) => {
try {
return sessionStorage.getItem(key) || '';
} catch {
return '';
}
};
const writeSessionCache = (key, value) => {
if (!value) return;
try {
sessionStorage.setItem(key, value);
} catch {}
};
const languageCodes = {
zh: 'CN',
en: 'EN',
ja: 'JA',
es: 'ES',
de: 'DE',
fr: 'FR',
};
const initLanguageMenu = () => {
const menu = mount.querySelector('#hd-lang-menu');
const trigger = mount.querySelector('#hd-lang-trigger');
const code = mount.querySelector('#hd-lang-code');
const options = Array.from(mount.querySelectorAll('.admin-lang-option'));
if (!menu || !trigger || !code || !options.length) return;
const close = () => {
menu.classList.remove('open');
trigger.setAttribute('aria-expanded', 'false');
};
const sync = () => {
const current = window.I18n?.getLang?.() || localStorage.getItem('grok2api_lang') || 'zh';
code.textContent = languageCodes[current] || current.toUpperCase();
options.forEach((option) => {
option.classList.toggle('active', option.dataset.lang === current);
});
};
trigger.addEventListener('click', (event) => {
event.stopPropagation();
const open = !menu.classList.contains('open');
menu.classList.toggle('open', open);
trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
});
options.forEach((option) => {
option.addEventListener('click', () => {
const lang = option.dataset.lang;
if (!lang) return;
close();
if (window.I18n?.setLang) {
I18n.setLang(lang);
} else {
localStorage.setItem('grok2api_lang', lang);
location.reload();
}
});
});
document.addEventListener('click', (event) => {
const target = event.target;
if (!(target instanceof Node) || !menu.contains(target)) close();
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') close();
});
sync();
return sync;
};
const applyHeaderI18n = () => {
if (window.I18n?.apply) I18n.apply(mount);
const trigger = mount.querySelector('#hd-lang-trigger');
if (trigger) {
const label = window.t ? t('header.languageLabel') : 'Language';
trigger.title = label;
trigger.setAttribute('aria-label', label);
}
const logout = mount.querySelector('#hd-logout');
if (logout) {
const hidden = mount.dataset.hideLogout === 'true';
logout.hidden = hidden;
if (!hidden) {
const label = window.t ? t('header.logout') : 'Logout';
logout.title = label;
logout.setAttribute('aria-label', label);
}
}
};
const loadVersion = async () => {
const cachedVersion = window.__grok2apiMetaVersion || readSessionCache(META_VERSION_CACHE_KEY);
if (cachedVersion) {
appVersion = String(cachedVersion).trim();
window.__grok2apiMetaVersion = appVersion;
return;
}
try {
const res = await fetch('/meta');
if (!res.ok) throw new Error('meta unavailable');
const data = await res.json();
appVersion = String(data?.version || '').trim();
window.__grok2apiMetaVersion = appVersion;
writeSessionCache(META_VERSION_CACHE_KEY, appVersion);
} catch {
appVersion = '';
}
};
const applyVersion = () => {
const right = mount.querySelector('.admin-header-right');
if (!right) return;
let node = mount.querySelector('#hd-version');
if (!appVersion) {
node?.remove();
return;
}
if (!node) {
node = document.createElement('span');
node.id = 'hd-version';
node.className = 'admin-header-version';
right.insertBefore(node, right.firstChild);
}
const value = `v${appVersion}`;
node.textContent = value;
node.title = value;
};
await loadVersion();
try {
const cachedHtml = window.__grok2apiWebuiHeaderHtml || readSessionCache(HEADER_HTML_CACHE_KEY);
if (cachedHtml) {
mount.innerHTML = cachedHtml;
} else {
const res = await fetch('/static/webui/header.html');
if (!res.ok) throw new Error('header unavailable');
const html = await res.text();
mount.innerHTML = html;
window.__grok2apiWebuiHeaderHtml = html;
writeSessionCache(HEADER_HTML_CACHE_KEY, html);
}
} catch {
mount.innerHTML = `
<header class="admin-header webui-header-bar">
<div class="admin-header-inner webui-header-inner">
<div class="admin-brand-wrap">
<a href="https://github.com/chenyme/grok2api" target="_blank" rel="noopener" class="admin-brand-link">
<span class="admin-brand">Grok2API</span>
</a>
<a href="https://blog.cheny.me/" target="_blank" rel="noopener" class="admin-username" id="hd-user">@Chenyme</a>
</div>
<nav class="admin-nav">
<a href="/webui/chat" class="admin-nav-link" data-nav="/webui/chat" data-i18n="webui.header.chat">聊天</a>
<a href="/webui/masonry" class="admin-nav-link" data-nav="/webui/masonry" data-i18n="webui.header.masonry">Masonry</a>
<a href="/webui/chatkit" class="admin-nav-link" data-nav="/webui/chatkit" data-i18n="webui.header.chatkit">ChatKit</a>
</nav>
<div class="admin-header-right">
<div class="admin-lang-menu" id="hd-lang-menu">
<button type="button" class="btn admin-header-control admin-lang-trigger" id="hd-lang-trigger" aria-label="Language" aria-haspopup="menu" aria-expanded="false">
<span class="admin-lang-trigger-code" id="hd-lang-code">CN</span>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="m7 10 5 5 5-5"/>
</svg>
</button>
<div class="admin-lang-popover" id="hd-lang-popover" role="menu" aria-labelledby="hd-lang-trigger">
<button type="button" class="admin-lang-option" data-lang="zh" role="menuitem">
<span class="admin-lang-option-code">CN</span>
<span class="admin-lang-option-name">简体中文</span>
</button>
<button type="button" class="admin-lang-option" data-lang="en" role="menuitem">
<span class="admin-lang-option-code">EN</span>
<span class="admin-lang-option-name">English</span>
</button>
<button type="button" class="admin-lang-option" data-lang="ja" role="menuitem">
<span class="admin-lang-option-code">JA</span>
<span class="admin-lang-option-name">日本語</span>
</button>
<button type="button" class="admin-lang-option" data-lang="es" role="menuitem">
<span class="admin-lang-option-code">ES</span>
<span class="admin-lang-option-name">Español</span>
</button>
<button type="button" class="admin-lang-option" data-lang="de" role="menuitem">
<span class="admin-lang-option-code">DE</span>
<span class="admin-lang-option-name">Deutsch</span>
</button>
<button type="button" class="admin-lang-option" data-lang="fr" role="menuitem">
<span class="admin-lang-option-code">FR</span>
<span class="admin-lang-option-name">Français</span>
</button>
</div>
</div>
<button onclick="webuiLogout()" class="btn admin-header-control admin-header-icon-btn" id="hd-logout" aria-label="Logout" title="Logout">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<path d="M16 17l5-5-5-5"/>
<path d="M21 12H9"/>
</svg>
</button>
</div>
</div>
</header>`;
}
const active = mount.dataset.active || location.pathname;
mount.querySelectorAll('[data-nav]').forEach((link) => {
link.classList.toggle('active', link.dataset.nav === active);
});
const syncLanguageMenu = initLanguageMenu();
applyHeaderI18n();
applyVersion();
syncLanguageMenu?.();
};