aipackage/cezen-portal/router.html
2026-06-30 10:51:41 +05:30

778 lines
40 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=4">
<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 { font-size:20px; }
.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 { font-size:20px; margin-bottom:4px; }
.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">Audit Log</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 Manager</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">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">Multimodal Chat</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 Builder</a>
<a href="schedules.html">Scheduled Jobs</a>
<a href="workflows.html">Workflow Automation</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" style="position:relative">🔔</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 style="font-size:18px">🔀</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">💬</div><div class="mr-flow-node-label">Request</div><div class="mr-flow-node-sub">User query</div></div>
<div class="mr-flow-arrow"></div>
<div class="mr-flow-node active"><div class="mr-flow-node-icon">🔀</div><div class="mr-flow-node-label">Router</div><div class="mr-flow-node-sub">Match rules</div></div>
<div class="mr-flow-arrow"></div>
<div class="mr-flow-node"><div class="mr-flow-node-icon">🖼️</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">🧠</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"></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">📚</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">☁️</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 (top → 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 style="font-size:18px">📋</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()">💾 Save Rules</button>
</div>
<div class="mr-card-body">
<div id="routes-list"><!-- injected --></div>
<button class="mr-add-route" onclick="addRoute()"> Add Routing Rule</button>
</div>
</div>
<!-- Fallback & cloud -->
<div class="mr-card" style="margin-bottom:20px">
<div class="mr-card-header">
<span style="font-size:18px">🔩</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">🏠</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">☁️</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">🔒</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()">💾 Save Fallback Settings</button>
</div>
</div>
</div>
<!-- Live test panel -->
<div class="mr-card">
<div class="mr-card-header">
<span style="font-size:18px">🧪</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()">▶ Test Routing</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;
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">⋮⋮</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">▶</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</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</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></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})"> Add condition</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)">↑ Move Up</button>
<button class="mr-btn ghost" onclick="moveRoute(${r._id},1)">↓ Move Down</button>
<button class="mr-btn danger" onclick="deleteRoute(${r._id})">🗑 Delete</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) { alert('Enter a condition value'); 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) { alert('Enter a prompt to test'); 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 showToast(msg) {
const t = document.createElement('div');
t.textContent = msg;
t.style.cssText = 'position:fixed;bottom:24px;right:24px;background:#F0FDFA;color:#6D28D9;border:1px solid #99F6E4;border-radius:10px;padding:11px 18px;font-size:13px;font-weight:600;z-index:9999;box-shadow:0 4px 16px rgba(0,0,0,.1)';
document.body.appendChild(t);
setTimeout(()=>t.remove(), 2500);
}
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"></script>
</body>
</html>