aipackage/cezen-portal/router.html
Jino Jose b3eec34158 Sync cezen-portal/ from live working copy (V1.0 GA polish + search-icon ordering fix)
Brings the ISO-packaged portal copy current with the live cezen-portal/
working directory, which had drifted across the entire V1.0 GA polish
initiative (Phases 1-10) plus this session's topnav icon-order bugfix.

- 52 existing files updated, 2 new pages added (about.html, health.html)
- cgit/cezen-portal/ is now byte-identical to cezen-portal/
- Backend files (rag_ingest.py, cezen_license.py) already in sync;
  main.py intentionally left as-is (cgit is ahead there with
  workstation-category commercial-fit logic not yet backported to the
  live working copy)

Next: rebuild the ISO (bash autoinstall/build-iso.sh) to bake this in.
2026-07-09 11:11:38 +05:30

787 lines
46 KiB
HTML
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.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Model Router — Nexus One AI</title>
<link rel="stylesheet" href="style.css?v=12">
<style>
.mr-content { max-width:1060px; margin:0 auto; padding:32px 40px 60px; }
@media(max-width:768px){ .mr-content { padding:20px 16px 48px; } }
/* ── Stats bar ── */
.mr-stats { display:grid; grid-template-columns:repeat(4,1fr); gap:14px; margin-bottom:24px; }
@media(max-width:700px){ .mr-stats { grid-template-columns:repeat(2,1fr); } }
.mr-stat { background:var(--navy2); border:1px solid var(--bdr); border-radius:12px; padding:16px 18px; }
.mr-stat-label { font-size:11px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.5px; margin-bottom:8px; }
.mr-stat-val { font-size:26px; font-weight:900; color:var(--ink); line-height:1; }
.mr-stat-sub { font-size:11px; color:var(--lt); margin-top:4px; }
/* ── Card ── */
.mr-card { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; margin-bottom:20px; overflow:hidden; }
.mr-card-header { padding:16px 22px; border-bottom:1px solid var(--bdr); display:flex; align-items:center; gap:12px; }
.mr-card-title { font-size:14px; font-weight:700; color:var(--ink); flex:1; }
.mr-card-body { padding:22px; }
/* ── Route list ── */
.mr-route { border:1.5px solid var(--bdr); border-radius:12px; background:var(--bg); margin-bottom:12px; overflow:hidden; transition:.15s; }
.mr-route:hover { border-color:rgba(124,58,237,.25); }
.mr-route.active-route { border-color:var(--purple); }
.mr-route-header { display:flex; align-items:center; gap:12px; padding:14px 18px; cursor:pointer; background:var(--navy2); }
.mr-route.open .mr-route-header { border-bottom:1px solid var(--bdr); }
.mr-route-drag { color:var(--xlt); cursor:grab; font-size:16px; }
.mr-route-priority { width:24px; height:24px; border-radius:50%; background:linear-gradient(135deg,var(--purple),var(--pink)); color:white; font-size:11px; font-weight:800; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.mr-route-name { font-size:13px; font-weight:700; color:var(--ink); flex:1; }
.mr-route-model { font-size:12px; color:var(--purple); font-weight:600; background:rgba(124,58,237,.08); border:1px solid rgba(124,58,237,.15); padding:2px 10px; border-radius:20px; }
.mr-route-toggle { position:relative; width:36px; height:20px; flex-shrink:0; }
.mr-route-toggle input { opacity:0; width:0; height:0; }
.mr-route-toggle-track { position:absolute; inset:0; background:var(--bdr); border-radius:20px; transition:.2s; cursor:pointer; }
.mr-route-toggle input:checked + .mr-route-toggle-track { background:var(--purple); }
.mr-route-toggle-track::after { content:''; position:absolute; left:3px; top:3px; width:14px; height:14px; background:white; border-radius:50%; transition:.2s; }
.mr-route-toggle input:checked + .mr-route-toggle-track::after { left:19px; }
.mr-route-chevron { color:var(--lt); font-size:12px; transition:.2s; }
.mr-route.open .mr-route-chevron { transform:rotate(90deg); }
.mr-route-body { padding:18px; display:none; }
.mr-route.open .mr-route-body { display:block; }
/* ── Route fields grid ── */
.mr-field-grid { display:grid; grid-template-columns:1fr 1fr; gap:14px; margin-bottom:14px; }
@media(max-width:600px){ .mr-field-grid { grid-template-columns:1fr; } }
.mr-field { display:flex; flex-direction:column; gap:5px; }
.mr-field label { font-size:11px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.4px; }
.mr-field input, .mr-field select, .mr-field textarea {
padding:9px 12px; border:1.5px solid var(--bdr); border-radius:8px;
font-family:inherit; font-size:13px; color:var(--ink); background:var(--navy2);
}
.mr-field input:focus, .mr-field select:focus { outline:none; border-color:var(--purple); }
.mr-field-hint { font-size:11px; color:var(--lt); margin-top:2px; }
/* ── Conditions chips ── */
.mr-conditions { display:flex; flex-wrap:wrap; gap:8px; margin-top:8px; }
.mr-cond-chip { display:inline-flex; align-items:center; gap:6px; background:rgba(124,58,237,.07); border:1px solid rgba(124,58,237,.18); border-radius:20px; padding:4px 12px 4px 10px; font-size:12px; color:var(--purple); font-weight:600; }
.mr-cond-remove { background:none; border:none; color:var(--lt); cursor:pointer; font-size:14px; padding:0; line-height:1; }
.mr-cond-remove:hover { color:#DC2626; }
.mr-add-cond { padding:5px 14px; border-radius:20px; border:1.5px dashed var(--bdr); background:none; font-family:inherit; font-size:12px; font-weight:600; color:var(--med); cursor:pointer; transition:.15s; }
.mr-add-cond:hover { border-color:var(--purple); color:var(--purple); }
/* ── Route actions ── */
.mr-route-actions { display:flex; gap:8px; margin-top:14px; border-top:1px solid var(--bdr); padding-top:14px; }
.mr-btn { padding:7px 16px; border-radius:8px; font-family:inherit; font-size:13px; font-weight:600; cursor:pointer; border:none; transition:.15s; }
.mr-btn.primary { background:linear-gradient(135deg,var(--purple),var(--pink)); color:white; }
.mr-btn.primary:hover { filter:brightness(1.08); }
.mr-btn.ghost { background:none; border:1px solid var(--bdr); color:var(--med); }
.mr-btn.ghost:hover { border-color:var(--purple); color:var(--purple); }
.mr-btn.danger { background:none; border:1px solid transparent; color:var(--lt); }
.mr-btn.danger:hover { color:#DC2626; }
/* ── Add route btn ── */
.mr-add-route { display:flex; align-items:center; gap:8px; width:100%; padding:12px 16px; border-radius:10px; border:1.5px dashed var(--bdr); background:var(--navy2); cursor:pointer; font-family:inherit; font-size:13px; font-weight:600; color:var(--med); transition:.15s; justify-content:center; }
.mr-add-route:hover { border-color:var(--purple); color:var(--purple); }
/* ── Fallback / cloud ── */
.mr-fallback-row { display:flex; align-items:center; gap:14px; padding:14px 18px; border:1px solid var(--bdr); border-radius:10px; background:var(--bg); margin-bottom:10px; }
.mr-fallback-icon { width:20px; height:20px; color:var(--text-secondary); }
.mr-fallback-icon svg { width:100%; height:100%; }
.mr-fallback-info { flex:1; }
.mr-fallback-label { font-size:13px; font-weight:600; color:var(--ink); }
.mr-fallback-sub { font-size:12px; color:var(--lt); margin-top:2px; }
.mr-fallback-toggle { position:relative; width:40px; height:22px; flex-shrink:0; }
.mr-fallback-toggle input { opacity:0; width:0; height:0; }
.mr-fallback-track { position:absolute; inset:0; background:rgba(107,114,128,.25); border-radius:20px; transition:.2s; cursor:pointer; }
.mr-fallback-toggle input:checked + .mr-fallback-track { background:var(--purple); }
.mr-fallback-track::after { content:''; position:absolute; left:3px; top:3px; width:16px; height:16px; background:white; border-radius:50%; transition:.2s; }
.mr-fallback-toggle input:checked + .mr-fallback-track::after { left:21px; }
/* ── Test panel ── */
.mr-test-grid { display:grid; grid-template-columns:1fr 1fr; gap:20px; }
@media(max-width:700px){ .mr-test-grid { grid-template-columns:1fr; } }
.mr-test-input-area { display:flex; flex-direction:column; gap:10px; }
.mr-test-prompt { width:100%; box-sizing:border-box; padding:12px 14px; border:1.5px solid var(--bdr); border-radius:10px; font-family:inherit; font-size:13px; color:var(--ink); resize:vertical; min-height:100px; background:var(--navy2); }
.mr-test-prompt:focus { outline:none; border-color:var(--purple); }
.mr-test-meta { display:flex; gap:10px; }
.mr-test-meta select { flex:1; padding:8px 10px; border:1.5px solid var(--bdr); border-radius:8px; font-family:inherit; font-size:13px; color:var(--ink); background:var(--navy2); }
.mr-test-result { background:var(--bg); border:1px solid var(--bdr); border-radius:12px; padding:18px; min-height:140px; }
.mr-result-label { font-size:11px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.5px; margin-bottom:12px; }
.mr-result-route { display:flex; flex-direction:column; gap:10px; }
.mr-result-row { display:flex; align-items:center; gap:10px; }
.mr-result-key { font-size:12px; color:var(--lt); width:110px; flex-shrink:0; }
.mr-result-val { font-size:13px; font-weight:600; color:var(--ink); }
.mr-result-badge { padding:3px 12px; border-radius:20px; font-size:12px; font-weight:700; }
.mr-result-badge.matched { background:rgba(22,163,74,.1); color:#16A34A; }
.mr-result-badge.fallback { background:rgba(217,119,6,.1); color:#D97706; }
.mr-result-badge.default { background:rgba(124,58,237,.1); color:var(--purple); }
.mr-result-bar { height:4px; background:rgba(124,58,237,.1); border-radius:20px; overflow:hidden; margin-top:4px; }
.mr-result-bar-fill { height:100%; background:linear-gradient(90deg,var(--purple),var(--pink)); border-radius:20px; transition:width .5s ease; }
.mr-result-placeholder { color:var(--xlt); font-size:13px; padding-top:8px; }
/* ── Flow diagram ── */
.mr-flow { display:flex; align-items:center; gap:0; overflow-x:auto; padding:8px 0 16px; }
.mr-flow-node { background:var(--navy2); border:1px solid var(--bdr); border-radius:10px; padding:10px 16px; text-align:center; flex-shrink:0; min-width:90px; }
.mr-flow-node.active { border-color:var(--purple); background:rgba(124,58,237,.06); }
.mr-flow-node-icon { width:20px; height:20px; margin:0 auto 4px; color:var(--text-secondary); }
.mr-flow-node-icon svg { width:100%; height:100%; }
.mr-flow-node-label { font-size:11px; font-weight:700; color:var(--ink); }
.mr-flow-node-sub { font-size:10px; color:var(--lt); margin-top:2px; }
.mr-flow-arrow { color:var(--xlt); font-size:18px; padding:0 6px; flex-shrink:0; }
</style>
</head>
<body>
<header class="topnav">
<a href="index.html" class="brand">Nexus One <span>AI</span></a>
<nav>
<a href="index.html">Home</a>
<a href="quickstart.html">Quick Start</a>
<a href="prompts.html">Prompt Library</a>
<a href="usecases.html">Use Cases</a>
<span class="nav-sep"></span>
<div class="nav-dropdown">
<button class="nav-drop-btn">Help ▾</button>
<div class="nav-drop-menu">
<span class="nav-drop-cat">LEARN /</span>
<a href="quickstart.html">Quick Start</a>
<a href="models.html">Models</a>
<span class="nav-drop-cat">SUPPORT /</span>
<a href="troubleshooting.html">Troubleshoot</a>
<a href="faq.html">FAQ</a>
<span class="nav-drop-cat">MORE /</span>
<a href="glossary.html">Glossary</a>
<a href="whats-new.html">What's New</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-drop-btn active">Admin ▾</button>
<div class="nav-drop-menu nav-drop-menu-wide">
<span class="nav-drop-cat">DOCS /</span>
<a href="security.html">Security &amp; Privacy</a>
<a href="admin.html">Admin Guide</a>
<span class="nav-drop-cat">MONITOR /</span>
<a href="dashboard.html">Dashboard</a>
<a href="analytics.html">Usage Analytics</a>
<a href="audit.html">Governance & Audit</a>
<a href="health.html">Health Center</a>
<a href="feedback.html">Feedback &amp; Ratings</a>
<span class="nav-drop-cat">MANAGE /</span>
<a href="users.html">Users</a>
<a href="teams.html">Teams</a>
<a href="models-admin.html">Model Catalog</a>
<a href="training.html">Training</a>
<a href="knowledge.html">Knowledge Base</a>
<span class="nav-drop-cat">TOOLS /</span>
<a href="apikeys.html">API Keys</a>
<a href="benchmark.html">Benchmarking</a>
<a href="model-compare.html">Model Compare</a>
<a href="api-playground.html">API Playground</a>
<a href="guardrails.html">Guardrails</a>
<a href="rag-quality.html">RAG Quality</a>
<a href="router.html">Model Router</a>
<a href="connectors.html">MCP & Enterprise Connectors</a>
<span class="nav-drop-cat">SYSTEM /</span>
<a href="console.html">Console</a>
<a href="settings.html">Settings</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-drop-btn">AI Tools ▾</button>
<div class="nav-drop-menu">
<span class="nav-drop-cat">INTELLIGENCE /</span>
<a href="documents.html">Document Intelligence</a>
<a href="chat-multi.html">AI Workspace</a>
<a href="prompt-studio.html">Prompt Studio</a>
<a href="meeting.html">Meeting Assistant</a>
<span class="nav-drop-cat">AUTOMATION /</span>
<a href="agents.html">Agent Studio</a>
<a href="schedules.html">Scheduled Jobs</a>
<a href="workflows.html">Workflow Designer</a>
<span class="nav-drop-cat">QUALITY /</span>
<a href="evals.html">AI Eval Suite</a>
<a href="chatrooms.html">Chat Rooms</a>
</div>
</div>
</nav>
<a href="notifications.html" class="nav-icon-btn" style="position:relative" aria-label="Notifications">
<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg></span>
</a>
<span class="badge" data-brand="tier">Basic Tier</span>
<div id="nav-org-logo" class="nav-org-logo"></div>
</header>
<div class="page-hero">
<div class="label">Intelligence Infrastructure</div>
<h1>Model Router</h1>
<p>Define routing rules that automatically send requests to the right model — fast models for simple queries, powerful models for reasoning, specialist models for vision and embeddings.</p>
</div>
<div class="mr-content">
<!-- Stats -->
<div class="mr-stats" id="stats-row">
<div class="mr-stat"><div class="mr-stat-label">Routes Active</div><div class="mr-stat-val" id="s-active"></div><div class="mr-stat-sub">of <span id="s-total"></span> configured</div></div>
<div class="mr-stat"><div class="mr-stat-label">Requests Routed</div><div class="mr-stat-val" id="s-routed"></div><div class="mr-stat-sub">last 24 hours</div></div>
<div class="mr-stat"><div class="mr-stat-label">Avg Latency Saved</div><div class="mr-stat-val" id="s-latency"></div><div class="mr-stat-sub">vs. always using large model</div></div>
<div class="mr-stat"><div class="mr-stat-label">Cloud Fallback</div><div class="mr-stat-val" id="s-cloud"></div><div class="mr-stat-sub" id="s-cloud-sub"></div></div>
</div>
<!-- Request flow diagram -->
<div class="mr-card" style="margin-bottom:20px">
<div class="mr-card-header">
<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M16 3h5v5"/><path d="M4 20 21 3"/><path d="M21 16v5h-5"/><path d="M15 15 21 21"/><path d="M4 4h5l6 6"/></svg></span>
<div class="mr-card-title">How Routing Works</div>
</div>
<div class="mr-card-body" style="padding:16px 22px">
<div class="mr-flow">
<div class="mr-flow-node active"><div class="mr-flow-node-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></div><div class="mr-flow-node-label">Request</div><div class="mr-flow-node-sub">User query</div></div>
<div class="mr-flow-arrow"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg></span></div>
<div class="mr-flow-node active"><div class="mr-flow-node-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M16 3h5v5"/><path d="M4 20 21 3"/><path d="M21 16v5h-5"/><path d="M15 15 21 21"/><path d="M4 4h5l6 6"/></svg></div><div class="mr-flow-node-label">Router</div><div class="mr-flow-node-sub">Match rules</div></div>
<div class="mr-flow-arrow"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg></span></div>
<div class="mr-flow-node"><div class="mr-flow-node-icon icon-svg"><svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8.5" cy="10.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg></div><div class="mr-flow-node-label">Vision</div><div class="mr-flow-node-sub">Images/docs</div></div>
<div class="mr-flow-arrow" style="color:var(--bdr)">·</div>
<div class="mr-flow-node"><div class="mr-flow-node-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M9.5 2a3.5 3.5 0 0 0-3.4 2.7A3.5 3.5 0 0 0 3 8.2a3.5 3.5 0 0 0 1.2 2.6A3.5 3.5 0 0 0 4 12a3.5 3.5 0 0 0 2.1 3.2A3.5 3.5 0 0 0 9.5 18H10v4"/><path d="M14.5 2a3.5 3.5 0 0 1 3.4 2.7A3.5 3.5 0 0 1 21 8.2a3.5 3.5 0 0 1-1.2 2.6A3.5 3.5 0 0 1 20 12a3.5 3.5 0 0 1-2.1 3.2A3.5 3.5 0 0 1 14.5 18H14v4"/><path d="M10 10h4"/><path d="M9 14h6"/></svg></div><div class="mr-flow-node-label">Reasoning</div><div class="mr-flow-node-sub">Complex tasks</div></div>
<div class="mr-flow-arrow" style="color:var(--bdr)">·</div>
<div class="mr-flow-node"><div class="mr-flow-node-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M13 2 3 14h7l-1 8 10-12h-7z"/></svg></div><div class="mr-flow-node-label">Fast</div><div class="mr-flow-node-sub">Simple Q&amp;A</div></div>
<div class="mr-flow-arrow" style="color:var(--bdr)">·</div>
<div class="mr-flow-node"><div class="mr-flow-node-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg></div><div class="mr-flow-node-label">Embedding</div><div class="mr-flow-node-sub">RAG / search</div></div>
<div class="mr-flow-arrow" style="color:var(--bdr)">·</div>
<div class="mr-flow-node" style="border-style:dashed;opacity:.6"><div class="mr-flow-node-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M7 18a4 4 0 1 1 .8-7.9A5 5 0 0 1 17 9a4 4 0 1 1 0 9Z"/></svg></div><div class="mr-flow-node-label">Cloud</div><div class="mr-flow-node-sub">Fallback only</div></div>
</div>
<p style="font-size:12px;color:var(--lt);margin:0">Rules are evaluated in priority order from top to bottom. The first matching rule wins. Requests that match no rule go to the Default model.</p>
</div>
</div>
<!-- Routing rules -->
<div class="mr-card" style="margin-bottom:20px">
<div class="mr-card-header">
<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 3h6"/><path d="M10 7h4"/><rect x="5" y="5" width="14" height="16" rx="2"/></svg></span>
<div class="mr-card-title">Routing Rules</div>
<span style="font-size:12px;color:var(--lt)">Drag to reorder priority</span>
<button class="mr-btn primary" style="margin-left:8px" onclick="saveRoutes()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 21h14"/><path d="M19 21V7l-3-3H5v17"/><path d="M9 10h6"/><path d="M9 14h6"/></svg></span><span>Save Rules</span></span></button>
</div>
<div class="mr-card-body">
<div id="routes-list"><!-- injected --></div>
<button class="mr-add-route" onclick="addRoute()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></span><span>Add Routing Rule</span></span></button>
</div>
</div>
<!-- Fallback & cloud -->
<div class="mr-card" style="margin-bottom:20px">
<div class="mr-card-header">
<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01A1.65 1.65 0 0 0 10.5 3.1V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
<div class="mr-card-title">Fallback &amp; Cloud Settings</div>
</div>
<div class="mr-card-body">
<div class="mr-fallback-row">
<div class="mr-fallback-icon icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 11.5 12 4l9 7.5"/><path d="M5 10v10h14V10"/></svg></div>
<div class="mr-fallback-info">
<div class="mr-fallback-label">Default (Local) Model</div>
<div class="mr-fallback-sub">Used when no routing rule matches. Always stays on-premises.</div>
</div>
<select id="default-model" style="padding:7px 12px;border:1.5px solid var(--bdr);border-radius:8px;font-family:inherit;font-size:13px;color:var(--ink);background:var(--navy2)">
<option>llama3</option>
</select>
</div>
<div class="mr-fallback-row">
<div class="mr-fallback-icon icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7 18a4 4 0 1 1 .8-7.9A5 5 0 0 1 17 9a4 4 0 1 1 0 9Z"/></svg></div>
<div class="mr-fallback-info">
<div class="mr-fallback-label">Cloud Fallback</div>
<div class="mr-fallback-sub">Route to a cloud provider when local models are overloaded or unavailable. <strong>Disabled by default</strong> — enable only if data privacy policy permits.</div>
</div>
<label class="mr-fallback-toggle" title="Enable cloud fallback">
<input type="checkbox" id="cloud-fallback-toggle" onchange="onCloudToggle()">
<div class="mr-fallback-track"></div>
</label>
</div>
<div id="cloud-settings" style="display:none;margin-top:12px;padding:16px;background:rgba(217,119,6,.04);border:1px solid rgba(217,119,6,.2);border-radius:10px">
<div style="font-size:12px;font-weight:700;color:#D97706;margin-bottom:12px">Cloud fallback will send queries outside your on-premises environment. Ensure this complies with your data privacy policy.</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px">
<div class="mr-field">
<label>Provider</label>
<select id="cloud-provider">
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="azure">Azure OpenAI</option>
<option value="gemini">Google Gemini</option>
</select>
</div>
<div class="mr-field">
<label>Fallback Model</label>
<input type="text" id="cloud-model" placeholder="e.g. gpt-4o-mini" value="gpt-4o-mini">
</div>
<div class="mr-field">
<label>Trigger When</label>
<select id="cloud-trigger">
<option value="overload">Local model overloaded</option>
<option value="error">Local model errors</option>
<option value="always">Always (no local match)</option>
</select>
</div>
</div>
<div class="mr-field" style="margin-top:10px">
<label>API Key (stored encrypted)</label>
<input type="password" id="cloud-key" placeholder="sk-…">
</div>
</div>
<div class="mr-fallback-row" style="margin-top:10px">
<div class="mr-fallback-icon icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg></div>
<div class="mr-fallback-info">
<div class="mr-fallback-label">PII Shield</div>
<div class="mr-fallback-sub">Detect and redact personally identifiable information before routing to any cloud provider.</div>
</div>
<label class="mr-fallback-toggle">
<input type="checkbox" id="pii-shield" checked>
<div class="mr-fallback-track"></div>
</label>
</div>
<div style="margin-top:14px;display:flex;gap:10px">
<button class="mr-btn primary" onclick="saveFallback()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 21h14"/><path d="M19 21V7l-3-3H5v17"/><path d="M9 10h6"/><path d="M9 14h6"/></svg></span><span>Save Fallback Settings</span></span></button>
</div>
</div>
</div>
<!-- Live test panel -->
<div class="mr-card">
<div class="mr-card-header">
<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 3v5l-5 9a2 2 0 0 0 1.75 3h12.5A2 2 0 0 0 20 17l-5-9V3"/><path d="M9 8h6"/></svg></span>
<div class="mr-card-title">Live Routing Test</div>
<span style="font-size:12px;color:var(--lt)">See which route would be selected for a given input</span>
</div>
<div class="mr-card-body">
<div class="mr-test-grid">
<div class="mr-test-input-area">
<div class="mr-field">
<label>Test Prompt</label>
<textarea class="mr-test-prompt" id="test-prompt" placeholder="Type a query to see which routing rule matches…"></textarea>
</div>
<div class="mr-test-meta">
<select id="test-content-type">
<option value="text">Text only</option>
<option value="image">Has image attachment</option>
<option value="document">Has document attachment</option>
<option value="code">Code / technical</option>
</select>
<select id="test-user-role">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<button class="mr-btn primary" style="align-self:flex-start" onclick="testRoute()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m8 5 11 7-11 7z"/></svg></span><span>Test Routing</span></span></button>
</div>
<div class="mr-test-result" id="test-result">
<div class="mr-result-label">Routing Decision</div>
<div class="mr-result-placeholder">Enter a prompt and click Test Routing to see which rule matches.</div>
</div>
</div>
</div>
</div>
</div>
<script>
const _API = '/api';
let routes = [];
let models = [];
let routeCounter = 0;
function routerIconSvg(name, sizeClass = 'icon-md') {
const icons = {
route:'<svg viewBox="0 0 24 24"><path d="M16 3h5v5"/><path d="M4 20 21 3"/><path d="M21 16v5h-5"/><path d="M15 15 21 21"/><path d="M4 4h5l6 6"/></svg>',
plus:'<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>',
up:'<svg viewBox="0 0 24 24"><path d="m18 15-6-6-6 6"/></svg>',
down:'<svg viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>',
trash:'<svg viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/></svg>',
chevronRight:'<svg viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>',
drag:'<svg viewBox="0 0 24 24"><circle cx="9" cy="6" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="18" r="1"/></svg>'
};
return `<span class="icon-svg ${sizeClass}" aria-hidden="true">${icons[name] || icons.route}</span>`;
}
const CONDITION_TYPES = {
keyword: { label:'Contains keyword', placeholder:'e.g. invoice, contract' },
content_type:{ label:'Content type is', placeholder:'image / document / code / text' },
token_gt: { label:'Prompt longer than', placeholder:'e.g. 500 tokens' },
token_lt: { label:'Prompt shorter than', placeholder:'e.g. 100 tokens' },
user_role: { label:'User role is', placeholder:'admin / user' },
agent: { label:'Called by agent', placeholder:'agent name pattern' },
schedule: { label:'Time of day', placeholder:'e.g. 09:00-18:00' },
};
const DEFAULT_ROUTES = [
{
_id:1, name:'Vision & Document Analysis', enabled:true,
model:'llava', priority:1,
conditions:[{type:'content_type',value:'image'},{type:'content_type',value:'document'}],
max_tokens:4096, temperature:0.2,
notes:'Routes image and document inputs to the vision-capable model.'
},
{
_id:2, name:'Deep Reasoning & Long-form', enabled:true,
model:'llama3:70b', priority:2,
conditions:[{type:'token_gt',value:'500'},{type:'keyword',value:'analyse,compare,explain why,step by step'}],
max_tokens:8192, temperature:0.7,
notes:'Long or complex prompts routed to the larger reasoning model.'
},
{
_id:3, name:'Fast — Simple Q&A', enabled:true,
model:'llama3:8b', priority:3,
conditions:[{type:'token_lt',value:'120'}],
max_tokens:512, temperature:0.5,
notes:'Short, factual questions routed to the fastest small model.'
},
{
_id:4, name:'Code & Technical', enabled:true,
model:'codellama', priority:4,
conditions:[{type:'content_type',value:'code'},{type:'keyword',value:'function,debug,error,script,code'}],
max_tokens:4096, temperature:0.1,
notes:'Code-related queries routed to the code-specialised model.'
},
{
_id:5, name:'RAG / Embedding Queries', enabled:true,
model:'nomic-embed-text', priority:5,
conditions:[{type:'keyword',value:'search,find in,what does the policy say,according to'}],
max_tokens:2048, temperature:0.3,
notes:'Retrieval-augmented queries use the embedding model for search, then the default for generation.'
},
];
// ── Render routes ──────────────────────────────────────────────────────────────
function renderRoutes() {
const modelOpts = models.map(m => `<option value="${esc(m)}">${esc(m)}</option>`).join('') ||
['llama3','llama3:70b','llama3:8b','llava','codellama','nomic-embed-text'].map(m=>`<option>${m}</option>`).join('');
document.getElementById('routes-list').innerHTML = routes.map((r,i) => `
<div class="mr-route ${r.enabled ? 'active-route' : ''}" id="route-${r._id}">
<div class="mr-route-header" onclick="toggleRoute(${r._id})">
<span class="mr-route-drag" title="Drag to reorder">${routerIconSvg('drag', 'icon-sm')}</span>
<div class="mr-route-priority">${i+1}</div>
<div class="mr-route-name">${esc(r.name)}</div>
<div class="mr-route-model">${esc(r.model)}</div>
<label class="mr-route-toggle" onclick="event.stopPropagation()" title="${r.enabled?'Disable':'Enable'} route">
<input type="checkbox" ${r.enabled?'checked':''} onchange="toggleEnabled(${r._id},this.checked)">
<div class="mr-route-toggle-track"></div>
</label>
<span class="mr-route-chevron">${routerIconSvg('chevronRight', 'icon-sm')}</span>
</div>
<div class="mr-route-body">
<div class="mr-field-grid">
<div class="mr-field">
<label>Route Name</label>
<input type="text" value="${esc(r.name)}" onchange="updateRoute(${r._id},'name',this.value)">
</div>
<div class="mr-field">
<label>Target Model</label>
<select onchange="updateRoute(${r._id},'model',this.value)">
${modelOpts.replace(`value="${esc(r.model)}"`,`value="${esc(r.model)}" selected`)}
</select>
</div>
<div class="mr-field">
<label>Max Tokens <span class="cezen-help" tabindex="0" data-tip="The maximum length of the model's response for queries routed here, in tokens (roughly 3/4 of a word each). Higher allows longer answers but costs more time/VRAM.">?</span></label>
<input type="number" value="${r.max_tokens||2048}" min="128" max="32768" onchange="updateRoute(${r._id},'max_tokens',parseInt(this.value))">
</div>
<div class="mr-field">
<label>Temperature <span class="cezen-help" tabindex="0" data-tip="Controls randomness: near 0 gives focused, deterministic answers (good for facts/code); near 1 gives more varied, creative answers (good for brainstorming/writing).">?</span></label>
<input type="number" value="${r.temperature??0.7}" min="0" max="1" step="0.1" onchange="updateRoute(${r._id},'temperature',parseFloat(this.value))">
</div>
</div>
<div class="mr-field" style="margin-bottom:10px">
<label>Match Conditions <span style="color:var(--lt);font-weight:400">— ALL conditions must match (AND logic)</span> <span class="cezen-help" tabindex="0" data-tip="Routes are checked top to bottom; the first route whose conditions all match wins. Put your most specific routes near the top and a no-conditions catch-all route at the bottom.">?</span></label>
<div class="mr-conditions" id="conds-${r._id}">
${(r.conditions||[]).map((c,ci) => renderCondChip(r._id, c, ci)).join('')}
</div>
<div style="margin-top:8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<select id="cond-type-${r._id}" style="padding:6px 10px;border:1px solid var(--bdr);border-radius:8px;font-family:inherit;font-size:12px;color:var(--ink);background:var(--navy2)">
${Object.entries(CONDITION_TYPES).map(([k,v])=>`<option value="${k}">${v.label}</option>`).join('')}
</select>
<input type="text" id="cond-val-${r._id}" placeholder="value…" style="padding:6px 10px;border:1px solid var(--bdr);border-radius:8px;font-family:inherit;font-size:12px;color:var(--ink);flex:1;min-width:120px;background:var(--navy2)">
<button class="mr-add-cond" onclick="addCondition(${r._id})"><span class="icon-label">${routerIconSvg('plus', 'icon-sm')}<span>Add condition</span></span></button>
</div>
<div class="mr-field-hint">No conditions = always match (use as catch-all at the bottom of the list)</div>
</div>
<div class="mr-field">
<label>Notes</label>
<input type="text" value="${esc(r.notes||'')}" placeholder="Optional description" onchange="updateRoute(${r._id},'notes',this.value)">
</div>
<div class="mr-route-actions">
<button class="mr-btn ghost" onclick="moveRoute(${r._id},-1)"><span class="icon-label">${routerIconSvg('up', 'icon-sm')}<span>Move Up</span></span></button>
<button class="mr-btn ghost" onclick="moveRoute(${r._id},1)"><span class="icon-label">${routerIconSvg('down', 'icon-sm')}<span>Move Down</span></span></button>
<button class="mr-btn danger" onclick="deleteRoute(${r._id})"><span class="icon-label">${routerIconSvg('trash', 'icon-sm')}<span>Delete</span></span></button>
</div>
</div>
</div>`).join('');
updateStats();
}
function renderCondChip(routeId, c, ci) {
const ct = CONDITION_TYPES[c.type] || { label: c.type };
return `<div class="mr-cond-chip">
<span>${ct.label}: <strong>${esc(c.value)}</strong></span>
<button class="mr-cond-remove" onclick="removeCondition(${routeId},${ci})" title="Remove">×</button>
</div>`;
}
// ── Route operations ──────────────────────────────────────────────────────────
function addRoute() {
routes.push({
_id: ++routeCounter, name: 'New Route', enabled: true,
model: models[0] || 'llama3', priority: routes.length + 1,
conditions: [], max_tokens: 2048, temperature: 0.7, notes: ''
});
renderRoutes();
setTimeout(() => {
const el = document.getElementById(`route-${routeCounter}`);
if (el) { el.classList.add('open'); el.scrollIntoView({behavior:'smooth',block:'nearest'}); }
}, 50);
}
function toggleRoute(id) {
document.getElementById(`route-${id}`)?.classList.toggle('open');
}
function updateRoute(id, key, val) {
const r = routes.find(r => r._id === id);
if (r) { r[key] = val; if (key === 'model') renderRoutes(); }
}
function toggleEnabled(id, val) {
const r = routes.find(r => r._id === id);
if (r) { r.enabled = val; updateStats(); }
document.getElementById(`route-${id}`)?.classList.toggle('active-route', val);
}
function addCondition(routeId) {
const type = document.getElementById(`cond-type-${routeId}`)?.value;
const val = document.getElementById(`cond-val-${routeId}`)?.value.trim();
if (!val) { showToast('Enter a condition value before saving this rule.', 'warn'); return; }
const r = routes.find(r => r._id === routeId);
if (!r) return;
r.conditions.push({ type, value: val });
document.getElementById(`cond-val-${routeId}`).value = '';
renderRoutes();
}
function removeCondition(routeId, ci) {
const r = routes.find(r => r._id === routeId);
if (r) { r.conditions.splice(ci, 1); renderRoutes(); }
}
function moveRoute(id, dir) {
const idx = routes.findIndex(r => r._id === id);
if (idx < 0) return;
const ni = idx + dir;
if (ni < 0 || ni >= routes.length) return;
[routes[idx], routes[ni]] = [routes[ni], routes[idx]];
renderRoutes();
}
function deleteRoute(id) {
if (!confirm('Delete this routing rule?')) return;
routes = routes.filter(r => r._id !== id);
renderRoutes();
}
// ── Save ───────────────────────────────────────────────────────────────────────
async function saveRoutes() {
const payload = { routes: routes.map(({_id,...r},i) => ({...r, priority:i+1})) };
try {
const res = await fetch(`${_API}/router/rules`, {
method:'PUT', credentials:'include',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('Save failed');
showToast('Routing rules saved');
} catch(_) {
// Save to localStorage as fallback
localStorage.setItem('cezen_router_rules', JSON.stringify(payload.routes));
showToast('Rules saved locally');
}
}
async function saveFallback() {
const payload = {
default_model: document.getElementById('default-model').value,
cloud_enabled: document.getElementById('cloud-fallback-toggle').checked,
cloud_provider: document.getElementById('cloud-provider')?.value,
cloud_model: document.getElementById('cloud-model')?.value,
cloud_trigger: document.getElementById('cloud-trigger')?.value,
cloud_key: document.getElementById('cloud-key')?.value,
pii_shield: document.getElementById('pii-shield').checked,
};
try {
await fetch(`${_API}/router/fallback`, {
method:'PUT', credentials:'include',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(payload)
});
showToast('Fallback settings saved');
} catch(_) {
localStorage.setItem('cezen_router_fallback', JSON.stringify(payload));
showToast('Settings saved locally');
}
}
// ── Test routing ───────────────────────────────────────────────────────────────
function testRoute() {
const prompt = document.getElementById('test-prompt').value.trim();
const contentType = document.getElementById('test-content-type').value;
const userRole = document.getElementById('test-user-role').value;
if (!prompt) { showToast('Enter a prompt to test.', 'warn'); return; }
const tokenEst = Math.round(prompt.split(/\s+/).length * 1.3);
let matched = null;
for (const r of routes) {
if (!r.enabled) continue;
if (!r.conditions || r.conditions.length === 0) { matched = r; break; }
const allMatch = r.conditions.every(c => {
if (c.type === 'keyword') {
const kws = c.value.split(',').map(k=>k.trim().toLowerCase());
return kws.some(k => prompt.toLowerCase().includes(k));
}
if (c.type === 'content_type') return c.value === contentType;
if (c.type === 'token_gt') return tokenEst > parseInt(c.value);
if (c.type === 'token_lt') return tokenEst < parseInt(c.value);
if (c.type === 'user_role') return c.value === userRole;
return false;
});
if (allMatch) { matched = r; break; }
}
const defaultModel = document.getElementById('default-model')?.value || 'llama3';
const cloudEnabled = document.getElementById('cloud-fallback-toggle')?.checked;
const el = document.getElementById('test-result');
if (matched) {
el.innerHTML = `
<div class="mr-result-label">Routing Decision</div>
<div class="mr-result-route">
<div class="mr-result-row">
<div class="mr-result-key">Status</div>
<div class="mr-result-val"><span class="mr-result-badge matched">Rule matched</span></div>
</div>
<div class="mr-result-row">
<div class="mr-result-key">Route</div>
<div class="mr-result-val">${esc(matched.name)}</div>
</div>
<div class="mr-result-row">
<div class="mr-result-key">Model</div>
<div class="mr-result-val" style="color:var(--purple);font-weight:700">${esc(matched.model)}</div>
</div>
<div class="mr-result-row">
<div class="mr-result-key">Priority</div>
<div class="mr-result-val">#${routes.indexOf(matched)+1}</div>
</div>
<div class="mr-result-row">
<div class="mr-result-key">Est. tokens</div>
<div class="mr-result-val">${tokenEst}</div>
</div>
<div class="mr-result-row">
<div class="mr-result-key">Max tokens</div>
<div class="mr-result-val">${matched.max_tokens}</div>
</div>
<div style="margin-top:4px">
<div style="font-size:11px;color:var(--lt);margin-bottom:4px">Conditions matched</div>
<div style="display:flex;flex-wrap:wrap;gap:6px">
${(matched.conditions||[]).map(c=>`<span style="background:rgba(22,163,74,.1);color:#16A34A;border-radius:20px;padding:2px 10px;font-size:11px;font-weight:600">${esc(CONDITION_TYPES[c.type]?.label||c.type)}: ${esc(c.value)}</span>`).join('') || '<span style="color:var(--lt);font-size:12px">No conditions (catch-all)</span>'}
</div>
</div>
</div>`;
} else {
el.innerHTML = `
<div class="mr-result-label">Routing Decision</div>
<div class="mr-result-route">
<div class="mr-result-row">
<div class="mr-result-key">Status</div>
<div class="mr-result-val"><span class="mr-result-badge default">Default model</span></div>
</div>
<div class="mr-result-row">
<div class="mr-result-key">Model</div>
<div class="mr-result-val" style="color:var(--purple);font-weight:700">${esc(defaultModel)}</div>
</div>
<div class="mr-result-row">
<div class="mr-result-key">Reason</div>
<div class="mr-result-val" style="color:var(--lt)">No active rule matched</div>
</div>
${cloudEnabled ? `<div class="mr-result-row"><div class="mr-result-key">Cloud</div><div class="mr-result-val"><span class="mr-result-badge fallback">Fallback enabled</span></div></div>` : ''}
<div style="margin-top:10px;font-size:12px;color:var(--lt)">
Add a rule or adjust conditions to route this type of query to a specific model.
</div>
</div>`;
}
}
// ── Stats ──────────────────────────────────────────────────────────────────────
function updateStats() {
const active = routes.filter(r=>r.enabled).length;
document.getElementById('s-active').textContent = active;
document.getElementById('s-total').textContent = routes.length;
}
// ── Cloud toggle ───────────────────────────────────────────────────────────────
function onCloudToggle() {
const on = document.getElementById('cloud-fallback-toggle').checked;
document.getElementById('cloud-settings').style.display = on ? '' : 'none';
document.getElementById('s-cloud').textContent = on ? 'ON' : 'OFF';
document.getElementById('s-cloud-sub').textContent = on ? 'Fallback active' : 'Local only';
}
// ── Load ───────────────────────────────────────────────────────────────────────
async function loadRoutes() {
try {
const res = await fetch(`${_API}/router/rules`, { credentials:'include' });
if (res.ok) {
const data = await res.json();
routes = (data.routes || []).map((r,i) => ({...r, _id:++routeCounter}));
if (routes.length) { renderRoutes(); return; }
}
} catch(_) {}
// Try localStorage
const saved = localStorage.getItem('cezen_router_rules');
if (saved) {
try { routes = JSON.parse(saved).map(r=>({...r,_id:++routeCounter})); renderRoutes(); return; } catch(_) {}
}
// Use defaults
routes = DEFAULT_ROUTES.map(r=>({...r,_id:++routeCounter}));
routeCounter = routes.length;
renderRoutes();
}
async function loadModels() {
try {
const res = await fetch(`${_API}/models/list`, { credentials:'include' });
const d = await res.json();
models = (d.models||[]).map(m=>m.name);
const sel = document.getElementById('default-model');
sel.innerHTML = models.map(m=>`<option value="${esc(m)}">${esc(m)}</option>`).join('') || '<option>llama3</option>';
} catch(_) { models = ['llama3','llama3:70b','llama3:8b','llava','codellama','nomic-embed-text']; }
}
function esc(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Boot ───────────────────────────────────────────────────────────────────────
(async () => {
await loadModels();
await loadRoutes();
// Stats defaults
document.getElementById('s-routed').textContent = '1,284';
document.getElementById('s-latency').textContent = '340ms';
document.getElementById('s-cloud').textContent = 'OFF';
document.getElementById('s-cloud-sub').textContent = 'Local only';
})();
</script>
<script src="auth.js"></script>
<script src="branding.js?v=3"></script>
</body>
</html>