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) => `
  • ${renderInlineMarkdown(item)}
  • `).join('')}`); listItems = []; listType = ''; }; const flushCodeBlock = () => { if (!inCodeBlock) return; html.push(`
    ${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(`${renderInlineMarkdown(headingMatch[2])}`); continue; } if (unorderedMatch || orderedMatch) { flushParagraph(); flushQuote(); const nextType = unorderedMatch ? 'ul' : 'ol'; const itemText = unorderedMatch ? unorderedMatch[1] : orderedMatch[1]; if (listType && listType !== nextType) flushList(); listType = nextType; listItems.push(itemText); continue; } flushList(); if (quoteMatch) { flushParagraph(); quoteLines.push(quoteMatch[1]); continue; } flushQuote(); if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) { flushParagraph(); flushQuote(); html.push('
    '); continue; } paragraph.push(line); } flushParagraph(); flushList(); flushQuote(); flushCodeBlock(); return html.join('') || '

    '; }; const renderReleaseNotes = (source) => { if (!source.trim()) return `

    ${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 = ` `; document.body.appendChild(overlay); const close = () => { overlay.classList.remove('open'); overlay.setAttribute('aria-hidden', 'true'); }; overlay.addEventListener('click', (event) => { if (event.target === overlay) close(); }); overlay.querySelector('#admin-version-modal-close')?.addEventListener('click', close); document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && overlay.classList.contains('open')) close(); }); return overlay; }; const renderVersionModal = (overlay = ensureVersionModal()) => { const title = overlay.querySelector('#admin-version-modal-title'); const badge = overlay.querySelector('#admin-version-modal-badge'); const status = overlay.querySelector('#admin-version-modal-status'); const currentLabel = overlay.querySelector('#admin-version-modal-current-label'); const currentValue = overlay.querySelector('#admin-version-modal-current'); const latestLabel = overlay.querySelector('#admin-version-modal-latest-label'); const latestValue = overlay.querySelector('#admin-version-modal-latest'); const publishedLabel = overlay.querySelector('#admin-version-modal-published-label'); const publishedValue = overlay.querySelector('#admin-version-modal-published'); const notes = overlay.querySelector('#admin-version-modal-notes'); const link = overlay.querySelector('#admin-version-modal-link'); const refresh = overlay.querySelector('#admin-version-modal-refresh'); const close = overlay.querySelector('#admin-version-modal-close'); const latestVersion = String(updateInfo?.latest_version || appVersion || '').trim(); const currentVersion = String(appVersion || updateInfo?.current_version || '').trim(); const releaseUrl = String(updateInfo?.release_url || '').trim(); const releaseNotes = String(updateInfo?.release_notes || '').trim(); if (title) title.textContent = text('header.versionDialogTitle', 'Version'); if (currentLabel) currentLabel.textContent = `${text('header.versionCurrent', 'Current')}:`; if (currentValue) currentValue.textContent = currentVersion ? `v${currentVersion}` : '-'; if (latestLabel) latestLabel.textContent = `${text('header.versionLatest', 'Latest')}:`; if (latestValue) latestValue.textContent = latestVersion ? `v${latestVersion}` : '-'; if (publishedLabel) publishedLabel.textContent = `${text('header.versionPublishedAt', 'Published')}:`; if (publishedValue) publishedValue.textContent = formatDateTime(updateInfo?.published_at); if (badge) { badge.hidden = true; badge.textContent = ''; badge.className = 'admin-version-modal-badge'; } if (status) { status.textContent = ''; status.className = 'admin-version-modal-status is-hidden'; } if (badge) { if (updateStatus === 'loading') { badge.hidden = false; badge.textContent = text('header.versionChecking', 'Checking for updates...'); badge.className = 'admin-version-modal-badge is-muted'; } else if (updateStatus === 'error' || !updateInfo || updateInfo.status === 'error') { badge.hidden = false; badge.textContent = text('header.versionUnavailable', 'Unable to check for updates right now.'); badge.className = 'admin-version-modal-badge is-muted'; } else if (updateInfo.update_available) { badge.hidden = false; badge.textContent = text('header.versionUpdateAvailable', 'A new version is available.'); badge.className = 'admin-version-modal-badge is-update'; } else { badge.hidden = false; badge.textContent = text('header.versionUpToDate', 'You are already on the latest version.'); badge.className = 'admin-version-modal-badge is-current'; } } if (notes) { if (updateStatus === 'loading' || updateStatus === 'error' || !updateInfo || updateInfo.status === 'error') { notes.hidden = true; notes.innerHTML = ''; } else { notes.hidden = false; notes.innerHTML = renderReleaseNotes(releaseNotes); } } if (link instanceof HTMLAnchorElement) { if (releaseUrl) { link.hidden = false; link.style.display = 'inline-flex'; link.href = releaseUrl; link.textContent = text('header.versionOpenRelease', 'Open Release'); } else { link.hidden = true; link.style.display = 'none'; link.removeAttribute('href'); link.textContent = ''; } } if (refresh instanceof HTMLButtonElement) { refresh.textContent = text('header.versionRefresh', 'Check Now'); refresh.disabled = updateStatus === 'loading'; } if (close instanceof HTMLButtonElement) { close.textContent = text('header.versionClose', 'Close'); } overlay.classList.add('open'); overlay.setAttribute('aria-hidden', 'false'); }; const openVersionModal = async () => { const overlay = ensureVersionModal(); updateStatus = 'loading'; renderVersionModal(overlay); try { await refreshUpdate(false); } finally { applyVersion(); if (overlay.classList.contains('open')) { renderVersionModal(overlay); } } }; 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; node.classList.toggle('has-update', Boolean(updateInfo?.update_available)); node.setAttribute('role', 'button'); node.setAttribute('tabindex', '0'); node.onclick = () => { void openVersionModal(); }; node.onkeydown = (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); void openVersionModal(); } }; }; await loadVersion(); try { const cachedHtml = window.__grok2apiAdminHeaderHtml || readSessionCache(HEADER_HTML_CACHE_KEY); if (cachedHtml) { mount.innerHTML = cachedHtml; } else { const res = await fetch('/static/admin/header.html'); if (!res.ok) throw new Error('header unavailable'); const html = await res.text(); mount.innerHTML = html; window.__grok2apiAdminHeaderHtml = html; writeSessionCache(HEADER_HTML_CACHE_KEY, html); } } catch { mount.innerHTML = `
    `; } 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?.(); const versionModal = ensureVersionModal(); versionModal.querySelector('#admin-version-modal-refresh')?.addEventListener('click', async () => { renderVersionModal(versionModal); try { await refreshUpdate(true); } finally { applyVersion(); if (versionModal.classList.contains('open')) { renderVersionModal(versionModal); } } }); };