360 lines
15 KiB
JavaScript
360 lines
15 KiB
JavaScript
/**
|
|
* 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',
|
|
'workflow.html': 'pro',
|
|
};
|
|
|
|
// Human-readable tier names for the upgrade prompt
|
|
var TIER_NAMES = {
|
|
starter: 'Starter',
|
|
basic: 'Basic',
|
|
pro: 'Pro',
|
|
max: 'Max',
|
|
};
|
|
|
|
// ── 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">🔒</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 closeModal() {
|
|
var m = document.getElementById('cezen-upgrade-modal');
|
|
if (m) m.classList.remove('cezen-modal-open');
|
|
}
|
|
|
|
// ── 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.textContent = '🔒';
|
|
link.appendChild(badge);
|
|
|
|
// Intercept click — show upgrade modal instead of navigating
|
|
var featureName = (link.textContent || file).replace('🔒', '').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();
|
|
|
|
// ── 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; }',
|
|
|
|
/* 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: #fff;',
|
|
' border-radius: 14px;',
|
|
' padding: 40px 36px 32px;',
|
|
' max-width: 420px;',
|
|
' width: 90%;',
|
|
' text-align: center;',
|
|
' box-shadow: 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 { font-size: 2.4rem; margin-bottom: 10px; }',
|
|
|
|
'#cezen-upgrade-title {',
|
|
' font-size: 1.15rem;',
|
|
' font-weight: 700;',
|
|
' color: #0f172a;',
|
|
' margin: 0 0 10px;',
|
|
'}',
|
|
|
|
'#cezen-upgrade-body {',
|
|
' font-size: 0.9rem;',
|
|
' color: #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: #0D9488;',
|
|
' color: #fff !important;',
|
|
' padding: 9px 20px;',
|
|
' border-radius: 7px;',
|
|
' font-size: 0.875rem;',
|
|
' font-weight: 600;',
|
|
' text-decoration: none;',
|
|
' transition: background 0.18s;',
|
|
'}',
|
|
'#cezen-upgrade-cta:hover { background: #0b7a6f; }',
|
|
|
|
'#cezen-upgrade-close {',
|
|
' background: #f1f5f9;',
|
|
' border: none;',
|
|
' border-radius: 7px;',
|
|
' padding: 9px 20px;',
|
|
' font-size: 0.875rem;',
|
|
' color: #334155;',
|
|
' cursor: pointer;',
|
|
' transition: background 0.18s;',
|
|
'}',
|
|
'#cezen-upgrade-close:hover { background: #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);
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// ── 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
|
|
})();
|