aipackage/cezen-portal/branding.js

806 lines
38 KiB
JavaScript
Raw 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.

/**
* branding.js — Nexus One AI White-Label Branding + Tier Feature Gating
* Fetches /api/settings/branding (public, no auth) and:
* 1. Applies org name, logo, accent color, and page title across all pages.
* 2. Locks nav items that require a higher tier, showing an upgrade prompt.
* Inject BEFORE </body> on every portal page.
*/
(function () {
'use strict';
// ── Tier hierarchy ──────────────────────────────────────────────────────────
var TIER_RANK = { starter: 0, basic: 1, pro: 2, max: 3 };
// ── Feature → minimum tier required ────────────────────────────────────────
// Keyed by partial href match (filename). Value = minimum slug to access.
var FEATURE_TIERS = {
// Basic-only features (not available on Starter)
'analytics.html': 'basic',
'api-keys.html': 'basic',
'benchmark.html': 'basic',
'chat-multi.html': 'basic',
'feedback.html': 'basic',
'notifications.html': 'basic',
'prompt-studio.html': 'basic',
'model-compare.html': 'basic',
// Pro-only features
'agents.html': 'pro',
'api-playground.html':'pro',
'chatrooms.html': 'pro',
'connectors.html': 'pro',
'evals.html': 'pro',
'guardrails.html': 'pro',
'meeting.html': 'pro',
'rag-quality.html': 'pro',
'router.html': 'pro',
'schedules.html': 'pro',
'teams.html': 'pro',
'training.html': 'pro',
'workflows.html': 'pro',
};
// Human-readable tier names for the upgrade prompt (Server S/M/L/Max are the
// commercial labels for the same starter/basic/pro/max ranks used above —
// slugs and TIER_RANK are unchanged for backward compatibility).
var TIER_NAMES = {
starter: 'Server S',
basic: 'Server M',
pro: 'Server L',
max: 'Server Max',
};
var PRODUCT_VOCABULARY = Object.freeze({
companyAttribution: 'Powered by Cezen',
workstation: 'Nexus One AI Workstation',
server: 'Nexus One AI Server',
tiers: Object.freeze({ starter: 'Server S', basic: 'Server M', pro: 'Server L', max: 'Server Max' }),
capabilityReasons: Object.freeze({
licensed: 'Included in the current license',
not_licensed: 'Not included in the current license',
not_provisioned: 'Licensed but not installed on this appliance',
unsupported_hardware: 'Not supported by the detected hardware',
degraded: 'Temporarily degraded',
unavailable: 'Currently unavailable'
})
});
window.NexusOneVocabulary = PRODUCT_VOCABULARY;
var SERVICE_PORTS = {
'3001': { name: 'open-webui', label: 'Open WebUI' },
'11434': { name: 'ollama', label: 'Ollama' },
'8000': { name: 'chromadb', label: 'ChromaDB' },
'8888': { name: 'jupyter', label: 'Jupyter' },
'8080': { name: 'cezen-api', label: 'Nexus API' },
};
// ── Upgrade modal (injected once into the DOM) ──────────────────────────────
function ensureModal() {
if (document.getElementById('cezen-upgrade-modal')) return;
var modal = document.createElement('div');
modal.id = 'cezen-upgrade-modal';
modal.innerHTML = [
'<div id="cezen-upgrade-backdrop"></div>',
'<div id="cezen-upgrade-box">',
' <div id="cezen-upgrade-icon"><span class="icon-svg icon-2xl" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="10" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span></div>',
' <h3 id="cezen-upgrade-title">Feature Locked</h3>',
' <p id="cezen-upgrade-body"></p>',
' <div id="cezen-upgrade-actions">',
' <a href="mailto:sales@cezentech.com" id="cezen-upgrade-cta">Contact Sales to Upgrade</a>',
' <button id="cezen-upgrade-close">Close</button>',
' </div>',
'</div>',
].join('');
document.body.appendChild(modal);
document.getElementById('cezen-upgrade-close').onclick = closeModal;
document.getElementById('cezen-upgrade-backdrop').onclick = closeModal;
}
function openModal(featureName, requiredTier) {
ensureModal();
var name = TIER_NAMES[requiredTier] || requiredTier;
document.getElementById('cezen-upgrade-title').textContent = featureName + ' — Upgrade Required';
document.getElementById('cezen-upgrade-body').textContent =
'This feature is available on the ' + name + ' tier and above. ' +
'Contact Cezentech to upgrade your Nexus One AI package.';
document.getElementById('cezen-upgrade-modal').classList.add('cezen-modal-open');
}
function openServiceModal(label) {
ensureModal();
document.getElementById('cezen-upgrade-title').textContent = label + ' is not running';
document.getElementById('cezen-upgrade-body').textContent =
'This service is not currently available on this appliance. Check the home page service status, or ask the administrator to enable and start the service for this tier.';
document.getElementById('cezen-upgrade-cta').style.display = 'none';
document.getElementById('cezen-upgrade-modal').classList.add('cezen-modal-open');
}
function closeModal() {
var m = document.getElementById('cezen-upgrade-modal');
if (m) m.classList.remove('cezen-modal-open');
var cta = document.getElementById('cezen-upgrade-cta');
if (cta) cta.style.display = '';
}
function currentBaseForPort(port) {
var protocol = location.protocol === 'https:' ? 'https:' : 'http:';
var host = location.hostname || 'ai.local';
return protocol + '//' + host + ':' + port;
}
function rewriteAiLocalText(value) {
if (!value || value.indexOf('ai.local') === -1) return value;
return value
.replace(/https?:\/\/ai\.local:3001/g, currentBaseForPort('3001'))
.replace(/https?:\/\/ai\.local:11434/g, currentBaseForPort('11434'))
.replace(/https?:\/\/ai\.local:8000/g, currentBaseForPort('8000'))
.replace(/https?:\/\/ai\.local:8888/g, currentBaseForPort('8888'))
.replace(/https?:\/\/ai\.local:8080/g, currentBaseForPort('8080'));
}
function normalizeApplianceLinks() {
document.querySelectorAll('a[href]').forEach(function (link) {
var href = link.getAttribute('href') || '';
if (href.indexOf('ai.local') !== -1) link.setAttribute('href', rewriteAiLocalText(href));
});
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
acceptNode: function (node) {
var tag = node.parentNode && node.parentNode.tagName;
if (tag === 'SCRIPT' || tag === 'STYLE') return NodeFilter.FILTER_REJECT;
return node.nodeValue.indexOf('ai.local') !== -1 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
});
var textNodes = [];
while (walker.nextNode()) textNodes.push(walker.currentNode);
textNodes.forEach(function (node) { node.nodeValue = rewriteAiLocalText(node.nodeValue); });
}
function serviceForHref(href) {
try {
var u = new URL(href, location.href);
return SERVICE_PORTS[u.port || (u.protocol === 'https:' ? '443' : '80')];
} catch (e) {
return null;
}
}
function applyServiceAvailability() {
fetch('/api/services', { credentials: 'include' })
.then(function (r) { return r.ok ? r.json() : []; })
.then(function (services) {
var online = {};
services.forEach(function (svc) { online[svc.name] = !!svc.ok; });
document.querySelectorAll('a[href]').forEach(function (link) {
var svc = serviceForHref(link.getAttribute('href') || '');
if (!svc || online[svc.name]) return;
if (link.classList.contains('cezen-service-unavailable')) return;
link.classList.add('cezen-service-unavailable');
link.setAttribute('title', svc.label + ' is not running on this appliance');
link.addEventListener('click', function (e) {
e.preventDefault();
openServiceModal(svc.label);
});
});
})
.catch(function () { /* Public pages or offline backend: leave links unchanged. */ });
}
// ── Apply tier locks to all nav links ──────────────────────────────────────
function applyTierGating(tierSlug) {
var rank = TIER_RANK[tierSlug] !== undefined ? TIER_RANK[tierSlug] : 1;
document.querySelectorAll('a[href]').forEach(function (link) {
var href = link.getAttribute('href') || '';
var file = href.split('/').pop().split('?')[0];
var minTier = FEATURE_TIERS[file];
if (!minTier) return;
var minRank = TIER_RANK[minTier] !== undefined ? TIER_RANK[minTier] : 99;
if (rank >= minRank) return; // user has access — do nothing
// Mark the link as locked
if (link.classList.contains('cezen-locked')) return; // already processed
link.classList.add('cezen-locked');
// Add lock badge after the link text (if not already there)
var badge = document.createElement('span');
badge.className = 'cezen-lock-badge';
badge.setAttribute('title', 'Requires ' + TIER_NAMES[minTier] + ' tier');
badge.innerHTML = '<span class="icon-svg icon-xs" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="10" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span>';
link.appendChild(badge);
// Intercept click — show upgrade modal instead of navigating
var featureName = (link.textContent || file).trim();
link.addEventListener('click', function (e) {
e.preventDefault();
openModal(featureName, minTier);
});
});
}
// ── Main branding apply ─────────────────────────────────────────────────────
function applyBranding(b) {
var orgName = b.org_name || 'Nexus One AI';
var stackName = b.stack_name || 'Nexus One AI';
var logoUrl = b.logo_url || '';
var accent = b.accent_color || '#0D9488';
var footer = b.footer_text || 'Powered by Cezen';
var tier = b.tier_label || 'Basic Tier';
var tierSlug = b.tier_slug || 'basic';
ensureApplianceNavLink();
ensureAboutNavLink();
normalizeApplianceLinks();
applyServiceAvailability();
// ── Accent color CSS variable ───────────────────────────────────────────
document.documentElement.style.setProperty('--accent', accent);
document.documentElement.style.setProperty('--accent-dark', shadeColor(accent, -20));
// ── Page <title> ────────────────────────────────────────────────────────
if (document.title) {
document.title = document.title
.replace(/Cezen AI Suite/gi, stackName)
.replace(/Cezen AI/gi, orgName)
.replace(/Nexus One AI/gi, stackName);
}
// ── data-brand text nodes ───────────────────────────────────────────────
document.querySelectorAll('[data-brand="org"]').forEach(function (el) {
el.textContent = orgName;
});
document.querySelectorAll('[data-brand="stack"]').forEach(function (el) {
el.textContent = stackName;
});
document.querySelectorAll('[data-brand="tier"]').forEach(function (el) {
el.textContent = tier;
// Set slug for CSS tier-colour targeting
if (el.classList.contains('nav-tier-badge')) {
el.setAttribute('data-tier-slug', tierSlug);
}
});
document.querySelectorAll('[data-brand="footer"]').forEach(function (el) {
el.textContent = footer;
});
// ── Replace hardcoded text strings ─────────────────────────────────────
replaceText('.sidebar-brand-text, .brand-name, .navbar-brand', 'Cezen AI Suite', stackName);
replaceText('.sidebar-brand-text, .brand-name, .navbar-brand', 'Cezen AI', orgName);
replaceText('.sidebar-brand-text, .brand-name, .navbar-brand', 'Nexus One AI', orgName);
replaceText('.tier-badge, .tier-label', 'Basic Tier', tier);
replaceText('.tier-badge, .tier-label', 'Starter Tier', tier);
replaceText('footer, .footer-text', 'Powered by Cezen', footer);
// ── Logo ────────────────────────────────────────────────────────────────
if (logoUrl) {
var navLogoSlot = document.getElementById('nav-org-logo');
if (navLogoSlot) {
var navImg = document.createElement('img');
navImg.src = logoUrl;
navImg.alt = orgName + ' Logo';
navLogoSlot.innerHTML = '';
navLogoSlot.appendChild(navImg);
}
document.querySelectorAll('.brand-logo, [data-brand="logo"]').forEach(function (el) {
if (el.tagName === 'IMG') {
el.src = logoUrl;
el.alt = orgName + ' Logo';
} else {
var img = document.createElement('img');
img.src = logoUrl;
img.alt = orgName + ' Logo';
img.style.cssText = 'max-height:36px;width:auto;object-fit:contain;';
el.innerHTML = '';
el.appendChild(img);
}
});
}
// ── Accent <style> block ────────────────────────────────────────────────
var style = document.getElementById('cezen-brand-style');
if (!style) {
style = document.createElement('style');
style.id = 'cezen-brand-style';
document.head.appendChild(style);
}
style.textContent = [
':root { --accent: ' + accent + '; --accent-dark: ' + shadeColor(accent, -20) + '; }',
'a.brand-accent { color: ' + accent + ' !important; }',
/* btn-primary intentionally excluded — brand colour is fixed in style.css */
'.btn-accent { background: ' + accent + ' !important; border-color: ' + accent + ' !important; }',
'.btn-accent:hover { background: ' + shadeColor(accent, -20) + ' !important; }',
'.sidebar-nav .nav-link.active { border-left-color: ' + accent + ' !important; color: ' + accent + ' !important; }',
'.progress-bar { background-color: ' + accent + ' !important; }',
].join('\n');
// ── Tier feature gating ─────────────────────────────────────────────────
applyTierGating(tierSlug);
injectTierGatingStyles();
}
// ── Tier gating CSS (injected once) ────────────────────────────────────────
function injectTierGatingStyles() {
if (document.getElementById('cezen-tier-style')) return;
var s = document.createElement('style');
s.id = 'cezen-tier-style';
s.textContent = [
/* Locked nav links */
'a.cezen-locked {',
' opacity: 0.55;',
' cursor: not-allowed !important;',
' position: relative;',
'}',
'a.cezen-locked:hover { opacity: 0.7; }',
'a.cezen-service-unavailable {',
' opacity: 0.58;',
' cursor: not-allowed !important;',
'}',
'a.cezen-service-unavailable::after {',
' content: " offline";',
' display: inline-block;',
' margin-left: 6px;',
' color: #dc2626;',
' font-size: 0.75em;',
' font-weight: 700;',
'}',
/* Lock badge */
'.cezen-lock-badge {',
' font-size: 0.65em;',
' margin-left: 5px;',
' vertical-align: middle;',
' pointer-events: none;',
'}',
/* Upgrade modal backdrop */
'#cezen-upgrade-modal {',
' display: none;',
' position: fixed;',
' inset: 0;',
' z-index: 99999;',
' align-items: center;',
' justify-content: center;',
'}',
'#cezen-upgrade-modal.cezen-modal-open { display: flex; }',
'#cezen-upgrade-backdrop {',
' position: absolute;',
' inset: 0;',
' background: rgba(15,23,42,0.65);',
' backdrop-filter: blur(3px);',
'}',
/* Modal box */
'#cezen-upgrade-box {',
' position: relative;',
' background: var(--surface, #fff);',
' border-radius: var(--radius-lg, 14px);',
' padding: 40px 36px 32px;',
' max-width: 420px;',
' width: 90%;',
' text-align: center;',
' box-shadow: var(--shadow-lg, 0 24px 60px rgba(0,0,0,0.22));',
' animation: cezen-modal-in 0.2s ease;',
'}',
'@keyframes cezen-modal-in {',
' from { opacity:0; transform: scale(0.92) translateY(12px); }',
' to { opacity:1; transform: scale(1) translateY(0); }',
'}',
'#cezen-upgrade-icon { color: var(--brand, #0D9488); margin-bottom: 10px; display: flex; justify-content: center; }',
'#cezen-upgrade-title {',
' font-size: 1.15rem;',
' font-weight: 700;',
' color: var(--text-primary, #0f172a);',
' margin: 0 0 10px;',
'}',
'#cezen-upgrade-body {',
' font-size: 0.9rem;',
' color: var(--text-secondary, #475569);',
' line-height: 1.55;',
' margin: 0 0 24px;',
'}',
'#cezen-upgrade-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }',
'#cezen-upgrade-cta {',
' display: inline-block;',
' background: var(--accent, #0D9488);',
' color: #fff !important;',
' padding: 9px 20px;',
' border-radius: var(--radius-md, 7px);',
' font-size: 0.875rem;',
' font-weight: 600;',
' text-decoration: none;',
' transition: background 0.18s;',
'}',
'#cezen-upgrade-cta:hover { background: var(--accent-dark, #0b7a6f); }',
'#cezen-upgrade-close {',
' background: var(--bg, #f1f5f9);',
' border: none;',
' border-radius: var(--radius-md, 7px);',
' padding: 9px 20px;',
' font-size: 0.875rem;',
' color: var(--text-secondary, #334155);',
' cursor: pointer;',
' transition: background 0.18s;',
'}',
'#cezen-upgrade-close:hover { background: var(--border, #e2e8f0); }',
].join('\n');
document.head.appendChild(s);
}
function replaceText(selector, from, to) {
try {
document.querySelectorAll(selector).forEach(function (el) {
if (el.childNodes.length === 1 && el.childNodes[0].nodeType === Node.TEXT_NODE) {
el.textContent = el.textContent.replace(
new RegExp(from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), to
);
}
});
} catch (e) { /* selector may not exist */ }
}
function ensureApplianceNavLink() {
if (document.querySelector('a[href="appliance.html"]')) return;
document.querySelectorAll('.nav-drop-cat').forEach(function (cat) {
if ((cat.textContent || '').trim().toUpperCase() !== 'SYSTEM /') return;
var next = cat.nextElementSibling;
var link = document.createElement('a');
link.href = 'appliance.html';
link.textContent = 'Appliance Ops';
if (location.pathname.endsWith('/appliance.html')) link.className = 'active';
cat.parentNode.insertBefore(link, next || null);
});
}
// Adds "About" to the end of the SYSTEM nav group on every page (except
// about.html itself, which already has it hardcoded and marked active).
function ensureAboutNavLink() {
if (document.querySelector('a[href="about.html"]')) return;
document.querySelectorAll('.nav-drop-cat').forEach(function (cat) {
if ((cat.textContent || '').trim().toUpperCase() !== 'SYSTEM /') return;
var sib = cat.nextElementSibling;
while (sib && sib.tagName === 'A') { sib = sib.nextElementSibling; }
var link = document.createElement('a');
link.href = 'about.html';
link.textContent = 'About';
cat.parentNode.insertBefore(link, sib || null);
});
}
// ── License status labels (window.licenseStatusLabel) ──────────────────────
// Raw codes from the backend's license evaluation (valid/missing/expired/
// invalid_signature/not_yet_valid/machine_mismatch) aren't meant for display
// — translate them the same way /api/health does, everywhere the portal
// shows a license status to a user.
var LICENSE_STATUS_LABELS = {
valid: 'Valid',
missing: 'No license installed (field-staging mode)',
expired: 'Expired — contact support@cezentech.com',
invalid_signature: 'Invalid signature — re-upload a valid signed license',
not_yet_valid: 'Not yet in its valid date range',
machine_mismatch: 'Bound to different hardware — contact support@cezentech.com',
};
window.licenseStatusLabel = function (status) {
return LICENSE_STATUS_LABELS[status] || (status || 'Unknown');
};
function shadeColor(hex, pct) {
var num = parseInt(hex.replace('#', ''), 16);
var r = Math.min(255, Math.max(0, (num >> 16) + Math.round(2.55 * pct)));
var g = Math.min(255, Math.max(0, ((num >> 8) & 0xff) + Math.round(2.55 * pct)));
var b = Math.min(255, Math.max(0, (num & 0xff) + Math.round(2.55 * pct)));
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
// ── Shared toast notifications (window.showToast) ──────────────────────────
// Single source of truth for transient messages — replaces the assorted
// per-page toast divs and native alert() calls. See style.css for
// #cezen-toast-stack / .cezen-toast styling.
var TOAST_ICONS = {
success: '<svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>',
error: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>',
warn: '<svg viewBox="0 0 24 24"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>',
info: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
};
function ensureToastStack() {
var stack = document.getElementById('cezen-toast-stack');
if (!stack) {
stack = document.createElement('div');
stack.id = 'cezen-toast-stack';
document.body.appendChild(stack);
}
return stack;
}
window.showToast = function (message, type, opts) {
type = TOAST_ICONS[type] ? type : 'info';
opts = opts || {};
var duration = opts.duration !== undefined ? opts.duration : (type === 'error' ? 6000 : 4000);
var stack = ensureToastStack();
var el = document.createElement('div');
el.className = 'cezen-toast ' + type;
el.innerHTML =
'<span class="icon-svg icon-sm" aria-hidden="true">' + TOAST_ICONS[type] + '</span>' +
'<span class="cezen-toast-body"></span>' +
'<button class="cezen-toast-close" aria-label="Dismiss">×</button>';
el.querySelector('.cezen-toast-body').textContent = message;
function remove() {
el.classList.add('leaving');
setTimeout(function () { if (el.parentNode) el.remove(); }, 160);
}
el.querySelector('.cezen-toast-close').onclick = remove;
stack.appendChild(el);
if (duration > 0) setTimeout(remove, duration);
return el;
};
// ── Shared confirmation modal (window.showConfirm) ──────────────────────────
// Replaces native confirm() with a styled, on-brand modal.
// Usage: showConfirm('Delete user?', 'This cannot be undone.', {danger:true})
// .then(function (ok) { if (ok) { ...proceed... } });
function ensureConfirmModal() {
var modal = document.getElementById('cezen-confirm-modal');
if (modal) return modal;
modal = document.createElement('div');
modal.id = 'cezen-confirm-modal';
modal.innerHTML =
'<div id="cezen-confirm-backdrop"></div>' +
'<div id="cezen-confirm-box">' +
' <div id="cezen-confirm-title"></div>' +
' <div id="cezen-confirm-body"></div>' +
' <div id="cezen-confirm-actions">' +
' <button id="cezen-confirm-cancel" class="btn-secondary">Cancel</button>' +
' <button id="cezen-confirm-ok" class="btn-primary">Confirm</button>' +
' </div>' +
'</div>';
document.body.appendChild(modal);
return modal;
}
window.showConfirm = function (title, body, opts) {
opts = opts || {};
return new Promise(function (resolve) {
var modal = ensureConfirmModal();
modal.querySelector('#cezen-confirm-title').textContent = title || 'Are you sure?';
modal.querySelector('#cezen-confirm-body').textContent = body || '';
var okBtn = modal.querySelector('#cezen-confirm-ok');
var cancelBtn = modal.querySelector('#cezen-confirm-cancel');
okBtn.textContent = opts.confirmLabel || 'Confirm';
cancelBtn.textContent = opts.cancelLabel || 'Cancel';
okBtn.className = opts.danger ? 'btn-danger' : 'btn-primary';
var backdrop = modal.querySelector('#cezen-confirm-backdrop');
function close(result) {
modal.classList.remove('cezen-modal-open');
okBtn.onclick = null; cancelBtn.onclick = null; backdrop.onclick = null;
resolve(result);
}
okBtn.onclick = function () { close(true); };
cancelBtn.onclick = function () { close(false); };
backdrop.onclick = function () { close(false); };
modal.classList.add('cezen-modal-open');
});
};
// ── Global command palette (window search / Ctrl+K) ─────────────────────────
// No global or cross-entity search existed anywhere in the portal — only a
// handful of pages had a local filter box. This adds a lightweight, static
// "jump to" index of every page in the product, reachable from any screen
// via Ctrl/Cmd+K or the search icon in the top nav. It's navigation search,
// not a live database query — no new API surface, matches the rest of
// branding.js's pattern of injecting shared UI once per page load.
var CMDK_PAGES = [
{ label: 'Home', href: 'index.html', cat: 'General' },
{ label: 'Quick Start', href: 'quickstart.html', cat: 'General' },
{ label: 'Prompt Library', href: 'prompts.html', cat: 'General' },
{ label: 'Use Cases', href: 'usecases.html', cat: 'General' },
{ label: 'Model Library', href: 'models.html', cat: 'Learn' },
{ label: 'Troubleshooting', href: 'troubleshooting.html', cat: 'Support' },
{ label: 'FAQ', href: 'faq.html', cat: 'Support' },
{ label: 'Glossary', href: 'glossary.html', cat: 'More' },
{ label: "What's New", href: 'whats-new.html', cat: 'More' },
{ label: 'Security & Privacy', href: 'security.html', cat: 'Docs' },
{ label: 'Admin Guide', href: 'admin.html', cat: 'Docs' },
{ label: 'Dashboard', href: 'dashboard.html', cat: 'Monitor' },
{ label: 'Usage Analytics', href: 'analytics.html', cat: 'Monitor' },
{ label: 'Governance & Audit', href: 'audit.html', cat: 'Monitor' },
{ label: 'Health Center', href: 'health.html', cat: 'Monitor', keywords: 'status services gpu database' },
{ label: 'Feedback & Ratings', href: 'feedback.html', cat: 'Monitor' },
{ label: 'Users', href: 'users.html', cat: 'Manage', keywords: 'user management accounts' },
{ label: 'Teams', href: 'teams.html', cat: 'Manage' },
{ label: 'Model Catalog', href: 'models-admin.html', cat: 'Manage', keywords: 'models install pull upload' },
{ label: 'Training', href: 'training.html', cat: 'Manage', keywords: 'fine-tune qlora' },
{ label: 'Knowledge Base', href: 'knowledge.html', cat: 'Manage', keywords: 'rag documents collections kb' },
{ label: 'API Keys', href: 'apikeys.html', cat: 'Tools' },
{ label: 'Benchmarking', href: 'benchmark.html', cat: 'Tools', keywords: 'model benchmark speed tokens' },
{ label: 'Model Compare', href: 'model-compare.html', cat: 'Tools' },
{ label: 'API Playground', href: 'api-playground.html', cat: 'Tools' },
{ label: 'Guardrails', href: 'guardrails.html', cat: 'Tools', keywords: 'content filters pii keyword' },
{ label: 'RAG Quality', href: 'rag-quality.html', cat: 'Tools' },
{ label: 'Model Router', href: 'router.html', cat: 'Tools' },
{ label: 'MCP & Enterprise Connectors', href: 'connectors.html', cat: 'Tools', keywords: 'database folder connector integration' },
{ label: 'Console', href: 'console.html', cat: 'System' },
{ label: 'Settings', href: 'settings.html', cat: 'System' },
{ label: 'Appliance Ops', href: 'appliance.html', cat: 'System' },
{ label: 'Document Intelligence', href: 'documents.html', cat: 'AI Tools' },
{ label: 'AI Workspace', href: 'chat-multi.html', cat: 'AI Tools', keywords: 'multimodal chat' },
{ label: 'Prompt Studio', href: 'prompt-studio.html', cat: 'AI Tools' },
{ label: 'Meeting Assistant', href: 'meeting.html', cat: 'AI Tools' },
{ label: 'Agent Studio', href: 'agents.html', cat: 'AI Tools', keywords: 'agents automation' },
{ label: 'Scheduled Jobs', href: 'schedules.html', cat: 'AI Tools', keywords: 'cron automation jobs' },
{ label: 'Workflow Designer', href: 'workflows.html', cat: 'AI Tools', keywords: 'automation pipeline' },
{ label: 'AI Eval Suite', href: 'evals.html', cat: 'AI Tools', keywords: 'evaluation test suite' },
{ label: 'Chat Rooms', href: 'chatrooms.html', cat: 'AI Tools' },
{ label: 'Notifications', href: 'notifications.html', cat: 'General' },
];
function ensureCmdPalette() {
var modal = document.getElementById('cezen-cmdk');
if (modal) return modal;
modal = document.createElement('div');
modal.id = 'cezen-cmdk';
modal.innerHTML =
'<div id="cezen-cmdk-backdrop"></div>' +
'<div id="cezen-cmdk-box">' +
' <div id="cezen-cmdk-input-wrap">' +
' <span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg></span>' +
' <input id="cezen-cmdk-input" type="text" placeholder="Jump to a page…" autocomplete="off">' +
' <span id="cezen-cmdk-hint">Esc</span>' +
' </div>' +
' <div id="cezen-cmdk-results"></div>' +
'</div>';
document.body.appendChild(modal);
var input = modal.querySelector('#cezen-cmdk-input');
modal.querySelector('#cezen-cmdk-backdrop').onclick = closeCmdPalette;
input.addEventListener('input', function () { renderCmdResults(input.value); });
input.addEventListener('keydown', function (e) {
var results = modal.querySelectorAll('.cezen-cmdk-item');
if (!results.length) return;
var activeIdx = -1;
results.forEach(function (r, i) { if (r.classList.contains('active')) activeIdx = i; });
if (e.key === 'ArrowDown') {
e.preventDefault();
activeIdx = (activeIdx + 1) % results.length;
results.forEach(function (r, i) { r.classList.toggle('active', i === activeIdx); });
results[activeIdx].scrollIntoView({ block: 'nearest' });
} else if (e.key === 'ArrowUp') {
e.preventDefault();
activeIdx = activeIdx <= 0 ? results.length - 1 : activeIdx - 1;
results.forEach(function (r, i) { r.classList.toggle('active', i === activeIdx); });
results[activeIdx].scrollIntoView({ block: 'nearest' });
} else if (e.key === 'Enter') {
e.preventDefault();
var target = modal.querySelector('.cezen-cmdk-item.active') || results[0];
if (target) target.click();
}
});
return modal;
}
function renderCmdResults(query) {
var modal = document.getElementById('cezen-cmdk');
var resultsEl = modal.querySelector('#cezen-cmdk-results');
var q = (query || '').trim().toLowerCase();
var matches = CMDK_PAGES.filter(function (p) {
if (!q) return true;
var hay = (p.label + ' ' + p.cat + ' ' + (p.keywords || '')).toLowerCase();
return hay.indexOf(q) !== -1;
}).slice(0, 8);
if (!matches.length) {
resultsEl.innerHTML = '<div id="cezen-cmdk-empty">No pages match "' + escapeHtml(query) + '".</div>';
return;
}
resultsEl.innerHTML = matches.map(function (p, i) {
return '<a class="cezen-cmdk-item' + (i === 0 ? ' active' : '') + '" href="' + p.href + '">' +
'<span class="cezen-cmdk-item-label">' + escapeHtml(p.label) + '</span>' +
'<span class="cezen-cmdk-item-cat">' + escapeHtml(p.cat) + '</span></a>';
}).join('');
}
function escapeHtml(s) {
return (s || '').replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
function openCmdPalette() {
var modal = ensureCmdPalette();
modal.classList.add('cezen-cmdk-open');
var input = modal.querySelector('#cezen-cmdk-input');
input.value = '';
renderCmdResults('');
setTimeout(function () { input.focus(); }, 0);
}
function closeCmdPalette() {
var modal = document.getElementById('cezen-cmdk');
if (modal) modal.classList.remove('cezen-cmdk-open');
}
function injectCmdPaletteTrigger() {
if (document.querySelector('.nav-search-btn')) return;
var topnav = document.querySelector('.topnav');
if (!topnav) return; // page has no shared topnav (e.g. login) — skip
var navMenu = topnav.querySelector(':scope > nav');
if (!navMenu) return;
var btn = document.createElement('button');
btn.className = 'nav-icon-btn nav-search-btn';
btn.type = 'button';
btn.setAttribute('aria-label', 'Search (Ctrl+K)');
btn.title = 'Search (Ctrl+K)';
btn.innerHTML = '<span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg></span>';
btn.onclick = openCmdPalette;
topnav.insertBefore(btn, navMenu.nextSibling);
}
// Two topnav markup variants exist across the portal: some pages wrap the
// logo/tier-badge/notification-bell trio in a .nav-right div (in that DOM
// order), others place the same three elements as flat topnav children in
// the REVERSE order (bell, badge, logo). Relying on CSS `order` to reconcile
// this is fragile because .nav-right is itself a nested flex container, so
// `order` values only resolve within whichever flex context an element
// actually sits in. Instead, physically move all four icon-cluster elements
// into one canonical container in one canonical DOM order, so every page's
// rendered markup is identical after this runs — no reliance on `order` math.
function normalizeNavIconCluster() {
var topnav = document.querySelector('.topnav');
if (!topnav) return;
var search = topnav.querySelector('.nav-search-btn');
var bell = topnav.querySelector('a.nav-icon-btn[href="notifications.html"]');
var badge = topnav.querySelector('[data-brand="tier"]');
var logo = topnav.querySelector('#nav-org-logo');
if (!bell && !badge && !logo) return; // nothing to normalize on this page
var cluster = topnav.querySelector('.nav-right');
if (!cluster) {
cluster = document.createElement('div');
cluster.className = 'nav-right';
topnav.appendChild(cluster);
}
// Canonical left-to-right order: search, bell, tier badge, org logo.
// appendChild on an already-attached node MOVES it, so this deterministically
// reorders in place regardless of the page's original markup pattern.
[search, bell, badge, logo].forEach(function (el) {
if (el) cluster.appendChild(el);
});
}
document.addEventListener('keydown', function (e) {
var isMeta = e.metaKey || e.ctrlKey;
if (isMeta && e.key.toLowerCase() === 'k') {
e.preventDefault();
var modal = document.getElementById('cezen-cmdk');
if (modal && modal.classList.contains('cezen-cmdk-open')) closeCmdPalette();
else openCmdPalette();
return;
}
if (e.key === 'Escape') {
var m = document.getElementById('cezen-cmdk');
if (m && m.classList.contains('cezen-cmdk-open')) closeCmdPalette();
}
});
function setupTopnavIcons() {
injectCmdPaletteTrigger();
normalizeNavIconCluster();
}
document.addEventListener('DOMContentLoaded', setupTopnavIcons);
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setupTopnavIcons();
}
// ── Fetch branding and apply ────────────────────────────────────────────────
fetch('/api/settings/branding', { credentials: 'include' })
.then(function (r) { return r.ok ? r.json() : {}; })
.then(function (data) { applyBranding(data); })
.catch(function () { applyBranding({}); }); // fallback: apply defaults silently
})();