/** * 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 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 = [ '
', '
', '
', '

Feature Locked

', '

', '
', ' Contact Sales to Upgrade', ' ', '
', '
', ].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 = ''; 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 ──────────────────────────────────────────────────────── 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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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 })();