window.renderAdminHeader = async function renderAdminHeader() {
const mount = document.getElementById('admin-header');
if (!mount || mount.children.length) return;
const scriptVersion = (() => {
try {
const script = document.querySelector('script[src*="/static/js/admin-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.admin_header_html.${scriptVersion}`;
const META_VERSION_CACHE_KEY = `grok2api.meta_version.${scriptVersion}`;
let appVersion = '';
let updateInfo = null;
let updateStatus = 'idle';
let updatePromise = null;
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 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 refreshUpdate = async (force = false) => {
if (updatePromise) return updatePromise;
if (force) updateInfo = null;
updateStatus = 'loading';
updatePromise = (async () => {
try {
const path = force ? '/meta/update?force=true' : '/meta/update';
const res = await fetch(path, { cache: 'no-store' });
if (!res.ok) throw new Error('update unavailable');
const data = await res.json();
updateInfo = data && typeof data === 'object' ? data : null;
updateStatus = 'ready';
} catch {
updateInfo = null;
updateStatus = 'error';
}
})().finally(() => {
updatePromise = null;
});
return updatePromise;
};
const text = (key, fallback, params) => {
if (typeof window.t !== 'function') return fallback;
const value = t(key, params);
return value === key ? fallback : value;
};
const formatDateTime = (value) => {
if (!value) return '-';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return String(value);
return date.toLocaleString();
};
const escapeHtml = (value) => String(value)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
const sanitizeUrl = (value) => {
try {
const url = new URL(value, window.location.origin);
return ['http:', 'https:', 'mailto:'].includes(url.protocol) ? url.href : '';
} catch {
return '';
}
};
const sanitizeRenderedHtml = (html) => {
const template = document.createElement('template');
template.innerHTML = html;
const blockedTags = new Set(['script', 'style', 'iframe', 'object', 'embed', 'link', 'meta']);
const walk = (node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
const el = node;
const tag = el.tagName.toLowerCase();
if (blockedTags.has(tag)) {
el.remove();
return;
}
Array.from(el.attributes).forEach((attr) => {
const name = attr.name.toLowerCase();
const value = attr.value || '';
if (name.startsWith('on')) {
el.removeAttribute(attr.name);
return;
}
if ((name === 'href' || name === 'src') && !sanitizeUrl(value)) {
el.removeAttribute(attr.name);
return;
}
if (name === 'target') {
el.setAttribute('target', '_blank');
}
});
Array.from(el.children).forEach((child) => walk(child));
};
Array.from(template.content.children).forEach((child) => walk(child));
return template.innerHTML;
};
const renderInlineMarkdown = (source) => {
let html = escapeHtml(source);
html = html.replace(/`([^`]+)`/g, (_, code) => `${code}`);
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => {
const safeHref = sanitizeUrl(href.trim());
const safeLabel = escapeHtml(label.trim() || href.trim());
return safeHref
? `${safeLabel}`
: safeLabel;
});
html = html.replace(/(^|[\s(>])((https?:\/\/|mailto:)[^\s<]+)/g, (_, prefix, rawUrl) => {
const safeHref = sanitizeUrl(rawUrl.trim());
if (!safeHref) return `${prefix}${rawUrl}`;
return `${prefix}${escapeHtml(rawUrl)}`;
});
html = html.replace(/\*\*([^*]+)\*\*/g, '$1');
html = html.replace(/(^|[^\*])\*([^*]+)\*/g, '$1$2');
return html;
};
const renderMarkdown = (source) => {
const lines = String(source || '').replace(/\r\n?/g, '\n').split('\n');
const html = [];
const paragraph = [];
let listType = '';
let listItems = [];
let inCodeBlock = false;
let codeLines = [];
let quoteLines = [];
const flushParagraph = () => {
if (!paragraph.length) return;
html.push(`
${renderInlineMarkdown(paragraph.map((line) => line.trim()).join(' '))}
`); paragraph.length = 0; }; const flushList = () => { if (!listItems.length) return; html.push(`<${listType}>${listItems.map((item) => `${escapeHtml(codeLines.join('\n'))}`);
inCodeBlock = false;
codeLines = [];
};
const flushQuote = () => {
if (!quoteLines.length) return;
html.push(`${renderInlineMarkdown(quoteLines.map((line) => line.trim()).join(' '))}`); quoteLines = []; }; for (const line of lines) { if (line.startsWith('```')) { flushParagraph(); flushList(); flushQuote(); if (inCodeBlock) { flushCodeBlock(); } else { inCodeBlock = true; codeLines = []; } continue; } if (inCodeBlock) { codeLines.push(line); continue; } const trimmed = line.trim(); const headingMatch = trimmed.match(/^(#{1,6})\s+(.*)$/); const unorderedMatch = trimmed.match(/^[-*+]\s+(.*)$/); const orderedMatch = trimmed.match(/^\d+\.\s+(.*)$/); const quoteMatch = trimmed.match(/^>\s?(.*)$/); if (!trimmed) { flushParagraph(); flushList(); flushQuote(); continue; } if (headingMatch) { flushParagraph(); flushList(); flushQuote(); const level = headingMatch[1].length; html.push(`
${escapeHtml(text('header.versionNotesEmpty', 'No release notes available.'))}
`; if (window.marked && typeof window.marked.parse === 'function') { return sanitizeRenderedHtml(window.marked.parse(source, { async: false, breaks: false, gfm: true, })); } return renderMarkdown(source); }; const ensureVersionModal = () => { let overlay = document.getElementById('admin-version-modal'); if (overlay) return overlay; overlay = document.createElement('div'); overlay.id = 'admin-version-modal'; overlay.className = 'modal-overlay'; overlay.innerHTML = `