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

568 lines
29 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>Guardrails — Nexus One AI</title>
<link rel="stylesheet" href="style.css?v=4">
<style>
.gr-card { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; padding:28px; margin-bottom:24px; }
.gr-card-title { font-size:16px; font-weight:700; color:var(--ink); margin-bottom:4px; }
.gr-card-sub { font-size:13px; color:var(--lt); margin-bottom:20px; }
.gr-btn { padding:9px 18px; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer; border:none; font-family:inherit; transition:.15s; }
.gr-btn.primary { background:var(--purple); color:var(--ink); }
.gr-btn.primary:hover { background:#6D28D9; }
.gr-btn.ghost { background:rgba(255,255,255,.03); color:var(--med); border:1px solid var(--bdr); }
.gr-btn.ghost:hover { border-color:var(--purple); color:var(--purple); }
.gr-btn.danger { background:rgba(185,28,28,.08); color:#B91C1C; border:1px solid rgba(239,68,68,.25); }
.gr-btn.danger:hover { background:#DC2626; color:var(--ink); }
.gr-btn:disabled { opacity:.45; cursor:not-allowed; }
/* Tabs */
.gr-tabs { display:flex; gap:4px; border-bottom:2px solid var(--bdr); margin-bottom:24px; }
.gr-tab { padding:10px 18px; font-size:13px; font-weight:600; color:var(--lt); cursor:pointer; border:none; background:none; font-family:inherit; border-bottom:2px solid transparent; margin-bottom:-2px; transition:.15s; }
.gr-tab.active { color:var(--purple); border-bottom-color:var(--purple); }
/* Rules table */
.gr-table-wrap { border:1px solid var(--bdr); border-radius:10px; overflow:hidden; }
table.gr-table { width:100%; border-collapse:collapse; }
.gr-table th { background:rgba(255,255,255,.03); padding:10px 14px; text-align:left; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.5px; color:var(--lt); border-bottom:1px solid var(--bdr); }
.gr-table td { padding:11px 14px; border-bottom:1px solid var(--bdr); font-size:13px; color:var(--ink); vertical-align:middle; }
.gr-table tr:last-child td { border-bottom:none; }
.gr-table tr:hover td { background:rgba(255,255,255,.03); }
.gr-empty { text-align:center; color:var(--lt); padding:36px 0; font-size:13px; }
/* Badges */
.badge-type-keyword { display:inline-block; font-size:10px; font-weight:700; padding:2px 8px; border-radius:8px; background:rgba(59,130,246,.1); color:#1D4ED8; text-transform:uppercase; }
.badge-type-regex { display:inline-block; font-size:10px; font-weight:700; padding:2px 8px; border-radius:8px; background:#F5F3FF; color:#7C3AED; text-transform:uppercase; }
.badge-type-pii { display:inline-block; font-size:10px; font-weight:700; padding:2px 8px; border-radius:8px; background:rgba(245,158,11,.15); color:#92400E; text-transform:uppercase; }
.badge-action-block { display:inline-block; font-size:10px; font-weight:700; padding:2px 8px; border-radius:8px; background:#FEE2E2; color:#B91C1C; text-transform:uppercase; }
.badge-action-warn { display:inline-block; font-size:10px; font-weight:700; padding:2px 8px; border-radius:8px; background:rgba(234,179,8,.15); color:#92400E; text-transform:uppercase; }
.badge-on { display:inline-block; font-size:10px; font-weight:700; padding:2px 8px; border-radius:8px; background:rgba(34,197,94,.15); color:#15803D; text-transform:uppercase; }
.badge-off { display:inline-block; font-size:10px; font-weight:700; padding:2px 8px; border-radius:8px; background:#F3F4F6; color:#6B7280; text-transform:uppercase; }
/* Tester */
.gr-tester textarea { width:100%; padding:10px 14px; border:1.5px solid var(--bdr); border-radius:8px; font-size:13px; font-family:inherit; outline:none; resize:vertical; min-height:100px; box-sizing:border-box; }
.gr-tester textarea:focus { border-color:var(--purple); }
.gr-result { border-radius:10px; padding:14px 18px; margin-top:14px; font-size:13px; }
.gr-result.ok { background:rgba(34,197,94,.08); border:1px solid #86EFAC; color:#15803D; }
.gr-result.blocked { background:rgba(185,28,28,.08); border:1px solid rgba(239,68,68,.25); color:#991B1B; }
.gr-result.warn { background:#FFFBEB; border:1px solid #FDE68A; color:#92400E; }
.gr-match-list { margin-top:10px; display:flex; flex-direction:column; gap:6px; }
.gr-match { background:var(--navy2); border:1px solid var(--bdr); border-radius:8px; padding:8px 12px; font-size:12px; }
/* PII presets grid */
.pii-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:10px; }
@media(max-width:640px){ .pii-grid { grid-template-columns:1fr 1fr; } }
.pii-chip { padding:9px 12px; border-radius:8px; border:1.5px solid var(--bdr); background:var(--navy2); font-size:12px; font-weight:600; color:var(--med); cursor:pointer; font-family:inherit; text-align:left; transition:.15s; }
.pii-chip:hover { border-color:var(--purple); color:var(--purple); }
.pii-chip.added { background:rgba(124,58,237,.12); border-color:rgba(124,58,237,.4); color:#6D28D9; cursor:default; }
/* Modal */
.gr-modal-bg { position:fixed; inset:0; background:rgba(0,0,0,.45); z-index:1000; display:none; align-items:center; justify-content:center; padding:20px; }
.gr-modal-bg.open { display:flex; }
.gr-modal { background:var(--navy2); border-radius:16px; padding:32px; width:100%; max-width:520px; box-shadow:0 20px 60px rgba(0,0,0,.25); }
.gr-modal h3 { margin:0 0 20px; font-size:18px; color:var(--ink); }
.gr-modal label { display:block; font-size:12px; font-weight:600; color:var(--med); margin-bottom:5px; }
.gr-modal input, .gr-modal select, .gr-modal textarea {
width:100%; padding:9px 12px; border:1.5px solid var(--bdr); border-radius:8px;
font-size:13px; font-family:inherit; outline:none; color:var(--ink); box-sizing:border-box; margin-bottom:14px;
}
.gr-modal input:focus, .gr-modal select:focus, .gr-modal textarea:focus { border-color:var(--purple); }
.gr-modal-row { display:flex; gap:10px; }
.gr-modal-row > div { flex:1; }
.gr-modal-footer { display:flex; gap:10px; justify-content:flex-end; margin-top:4px; }
.gr-modal-footer button { padding:9px 20px; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer; border:none; font-family:inherit; }
.gr-modal-footer .save { background:var(--purple); color:var(--ink); }
.gr-modal-footer .save:hover { background:#6D28D9; }
.gr-modal-footer .cancel { background:rgba(255,255,255,.03); color:var(--med); border:1px solid var(--bdr); }
/* Tier gate */
.tier-gate { background:#FFFBEB; border:1px solid #FDE68A; border-radius:12px; padding:28px; text-align:center; }
.tier-gate h3 { color:#92400E; margin:0 0 8px; }
.tier-gate p { color:#B45309; font-size:13px; margin:0; }
/* Toast */
#gr-toast { position:fixed; bottom:24px; right:24px; background:#1E3A5F; color:var(--ink); padding:12px 20px; border-radius:10px; font-size:13px; display:none; z-index:9999; box-shadow:0 8px 24px rgba(0,0,0,.2); max-width:340px; }
#gr-toast.err { background:#7F1D1D; }
</style>
</head>
<body data-role="admin">
<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 & 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">Admin · Safety</div>
<h1>Guardrails &amp; Content Filters</h1>
<p>Block or flag prompts containing sensitive keywords, patterns, or PII before they reach the model.</p>
</div>
<div class="content">
<!-- Tier gate -->
<div class="tier-gate" id="tier-gate" style="display:none">
<h3>🛡 Pro Tier & Above</h3>
<p>Guardrails are available on Mid and Advanced tiers. Advanced tier adds built-in PII detection (Aadhaar, PAN, mobile numbers, credit cards).</p>
</div>
<div id="gr-main" style="display:none">
<!-- Tabs -->
<div class="gr-tabs">
<button class="gr-tab active" onclick="showTab('rules',this)">🚫 Rules</button>
<button class="gr-tab" onclick="showTab('pii',this)">🔍 PII Detection</button>
<button class="gr-tab" onclick="showTab('tester',this)">🧪 Tester</button>
<button class="gr-tab" onclick="showTab('log',this)">📋 Violation Log</button>
</div>
<!-- Rules tab -->
<div id="tab-rules">
<div class="gr-card">
<div class="gr-card-title">Keyword &amp; Regex Rules</div>
<div class="gr-card-sub">Block or warn when a user prompt matches a keyword or regular expression.</div>
<button class="gr-btn primary" onclick="openModal()">+ Add Rule</button>
</div>
<div id="rules-table"><div class="gr-empty">Loading…</div></div>
</div>
<!-- PII tab -->
<div id="tab-pii" style="display:none">
<div class="gr-card">
<div class="gr-card-title">🔍 PII Detection Patterns</div>
<div class="gr-card-sub">One-click rules that detect common Indian PII. Each adds a regex-backed guardrail rule. <span id="pii-advanced-note" style="display:none;color:#7C3AED;font-weight:600">✦ Advanced tier — full PII suite enabled.</span></div>
<div class="pii-grid" id="pii-presets"></div>
</div>
</div>
<!-- Tester tab -->
<div id="tab-tester" style="display:none">
<div class="gr-card gr-tester">
<div class="gr-card-title">🧪 Test a Prompt</div>
<div class="gr-card-sub">Paste any text below to check which active rules it would trigger.</div>
<textarea id="test-input" placeholder="Paste a sample prompt or message to test…"></textarea>
<div style="margin-top:10px">
<button class="gr-btn primary" onclick="testPrompt()">Run Check</button>
</div>
<div id="test-result" style="display:none" class="gr-result">
<div id="test-summary"></div>
<div class="gr-match-list" id="test-matches"></div>
</div>
</div>
</div>
<!-- Log tab -->
<div id="tab-log" style="display:none">
<div class="gr-card">
<div class="gr-card-title">📋 Violation Log</div>
<div class="gr-card-sub">Most recent 100 guardrail triggers across all users.</div>
<div id="log-table"><div class="gr-empty">Loading…</div></div>
</div>
</div>
</div><!-- end gr-main -->
</div>
<!-- Add/Edit rule modal -->
<div class="gr-modal-bg" id="gr-modal-bg">
<div class="gr-modal">
<h3 id="modal-title">Add Rule</h3>
<div class="gr-modal-row">
<div>
<label>Type</label>
<select id="m-type" onchange="updatePatternHint()">
<option value="keyword">Keyword</option>
<option value="regex">Regex</option>
<option value="pii">PII Pattern</option>
</select>
</div>
<div>
<label>Action</label>
<select id="m-action">
<option value="block">Block</option>
<option value="warn">Warn only</option>
</select>
</div>
</div>
<label>Pattern <span id="pattern-hint" style="font-weight:400;color:var(--lt)">— exact keyword match, case-insensitive</span></label>
<input type="text" id="m-pattern" placeholder="e.g. classified">
<label>Label <span style="font-weight:400;color:var(--lt)">(optional description)</span></label>
<input type="text" id="m-label" placeholder="e.g. Classified documents">
<div class="gr-modal-row" style="align-items:center">
<div>
<label>Active</label>
<select id="m-active">
<option value="1">Yes</option>
<option value="0">No</option>
</select>
</div>
</div>
<div class="gr-modal-footer">
<button class="cancel" onclick="closeModal()">Cancel</button>
<button class="save" onclick="saveRule()">Save</button>
</div>
</div>
</div>
<div id="gr-toast"></div>
<footer>
<p>Nexus One AI &nbsp;·&nbsp; Powered by Cezen &nbsp;·&nbsp; <span data-brand="tier">Basic Tier</span></p>
<p>Questions? <a href="mailto:support@cezentech.com">support@cezentech.com</a> &nbsp;·&nbsp; <a href="https://cezentech.com" target="_blank">cezentech.com</a></p>
</footer>
<script>
const _API = '/api';
const MOCK_RULES = [
{id:1, type:'keyword', pattern:'confidential', label:'Confidential Data', action:'block', enabled:true},
{id:2, type:'keyword', pattern:'password', label:'Password Leak', action:'block', enabled:true},
{id:3, type:'regex', pattern:'\\b\\d{12,19}\\b', label:'Card Number', action:'redact', enabled:true},
{id:4, type:'regex', pattern:'\\b[A-Z]{5}\\d{4}[A-Z]\\b', label:'PAN Number', action:'redact', enabled:true},
{id:5, type:'keyword', pattern:'system prompt', label:'Prompt Injection', action:'warn', enabled:false},
{id:6, type:'pii', pattern:'email', label:'Email Address', action:'redact', enabled:true},
{id:7, type:'pii', pattern:'phone', label:'Phone Number', action:'redact', enabled:true},
];
const MOCK_LOG = [
{id:1, triggered_rule:'Confidential Data', input_snippet:'...this doc is confidential and sh...', action:'block', ts:'2026-06-28T09:14:22Z', user:'rajesh.k'},
{id:2, triggered_rule:'Card Number', input_snippet:'...card ending 4242 4242 4242 4242...', action:'redact', ts:'2026-06-28T08:55:01Z', user:'priya.m'},
{id:3, triggered_rule:'Email Address', input_snippet:'...send results to john@example.com...', action:'redact', ts:'2026-06-27T17:30:14Z', user:'arun.s'},
{id:4, triggered_rule:'Prompt Injection', input_snippet:'...ignore previous instructions and...', action:'warn', ts:'2026-06-27T14:22:45Z', user:'api-key-03'},
];
let allRules = [];
let editingId = null;
let currentTab = 'rules';
let isAdvanced = false;
const PII_PRESETS = [
{ key:'aadhaar', label:'Aadhaar Number', desc:'12-digit UID', pattern:'aadhaar' },
{ key:'pan', label:'PAN Card', desc:'ABCDE1234F format', pattern:'pan' },
{ key:'mobile_in', label:'Indian Mobile', desc:'+91 / 10-digit', pattern:'mobile_in' },
{ key:'email', label:'Email Address', desc:'user@domain.com', pattern:'email' },
{ key:'credit_card',label:'Credit Card', desc:'1316 digit card numbers',pattern:'credit_card' },
];
// ── Tier gate ─────────────────────────────────────────────────────────────────
function checkTier() {
// Tier gate disabled for testing — all features visible
document.getElementById("tier-gate").style.display = "none";
var main = document.getElementById("gr-main");
if (main) main.style.display = "";
loadRules(); renderPiiPresets();
}
// ── Tabs ─────────────────────────────────────────────────────────────────────
function showTab(name, btn) {
['rules','pii','tester','log'].forEach(t => {
document.getElementById('tab-'+t).style.display = t === name ? '' : 'none';
});
document.querySelectorAll('.gr-tab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentTab = name;
if (name === 'log') loadLog();
}
// ── Load rules ────────────────────────────────────────────────────────────────
async function loadRules() {
try {
const res = await fetch(`${_API}/guardrails/rules`);
if (!res.ok) throw new Error('Failed');
allRules = await res.json();
renderRules();
renderPiiPresets();
} catch(e) {
allRules = MOCK_RULES;
renderRules();
renderPiiPresets();
}
}
function renderRules() {
const el = document.getElementById('rules-table');
const rules = allRules.filter(r => r.type !== 'pii');
if (!rules.length) {
el.innerHTML = '<div class="gr-empty">No rules yet. Add a keyword or regex rule above.</div>'; return;
}
let html = `<div class="gr-table-wrap"><table class="gr-table">
<tr><th>Type</th><th>Pattern</th><th>Label</th><th>Action</th><th>Status</th><th></th></tr>`;
rules.forEach(r => {
html += `<tr>
<td><span class="badge-type-${r.type}">${r.type}</span></td>
<td><code style="font-size:12px">${escHtml(r.pattern)}</code></td>
<td style="color:var(--lt)">${escHtml(r.label || '—')}</td>
<td><span class="badge-action-${r.action}">${r.action}</span></td>
<td><span class="${r.is_active ? 'badge-on' : 'badge-off'}">${r.is_active ? 'On' : 'Off'}</span></td>
<td style="white-space:nowrap">
<button class="gr-btn ghost" style="padding:4px 10px;font-size:11px;margin-right:4px" onclick="openModal(${r.id})">Edit</button>
<button class="gr-btn danger" style="padding:4px 10px;font-size:11px" onclick="deleteRule(${r.id})">Delete</button>
</td>
</tr>`;
});
html += '</table></div>';
el.innerHTML = html;
}
// ── PII Presets ───────────────────────────────────────────────────────────────
function renderPiiPresets() {
const el = document.getElementById('pii-presets');
const activePii = new Set(allRules.filter(r => r.type === 'pii').map(r => r.pattern));
el.innerHTML = PII_PRESETS.map(p => {
const added = activePii.has(p.pattern);
return `<button class="pii-chip${added ? ' added' : ''}" onclick="togglePii('${p.pattern}','${escHtml(p.label)}')" ${added ? 'disabled' : ''}>
<strong>${escHtml(p.label)}</strong><br>
<span style="font-weight:400;font-size:11px;color:var(--lt)">${escHtml(p.desc)}</span>
${added ? '<br><span style="color:var(--purple);font-size:11px">✓ Active</span>' : ''}
</button>`;
}).join('');
}
async function togglePii(pattern, label) {
const existing = allRules.find(r => r.type === 'pii' && r.pattern === pattern);
if (existing) return; // already added
try {
const res = await fetch(`${_API}/guardrails/rules`, {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ type:'pii', pattern, action:'block', label, is_active:1 })
});
if (!res.ok) throw new Error((await res.json()).detail || 'Failed');
const rule = await res.json();
allRules.push(rule);
renderPiiPresets();
toast(`PII rule added: ${label}`);
} catch(e) { toast(e.message, true); }
}
// ── Modal ─────────────────────────────────────────────────────────────────────
function openModal(id) {
editingId = id || null;
document.getElementById('modal-title').textContent = id ? 'Edit Rule' : 'Add Rule';
if (id) {
const r = allRules.find(x => x.id === id);
document.getElementById('m-type').value = r.type;
document.getElementById('m-pattern').value = r.pattern;
document.getElementById('m-label').value = r.label;
document.getElementById('m-action').value = r.action;
document.getElementById('m-active').value = r.is_active;
} else {
document.getElementById('m-type').value = 'keyword';
document.getElementById('m-pattern').value = '';
document.getElementById('m-label').value = '';
document.getElementById('m-action').value = 'block';
document.getElementById('m-active').value = '1';
}
updatePatternHint();
document.getElementById('gr-modal-bg').classList.add('open');
setTimeout(() => document.getElementById('m-pattern').focus(), 50);
}
function closeModal() { document.getElementById('gr-modal-bg').classList.remove('open'); }
function updatePatternHint() {
const t = document.getElementById('m-type').value;
const hints = {
keyword: '— exact keyword match, case-insensitive',
regex: '— regular expression, e.g. \\b(secret|classified)\\b',
pii: '— PII key: aadhaar, pan, mobile_in, email, credit_card'
};
document.getElementById('pattern-hint').textContent = hints[t] || '';
}
async function saveRule() {
const body = {
type: document.getElementById('m-type').value,
pattern: document.getElementById('m-pattern').value.trim(),
label: document.getElementById('m-label').value.trim(),
action: document.getElementById('m-action').value,
is_active: parseInt(document.getElementById('m-active').value),
};
if (!body.pattern) { toast('Pattern is required', true); return; }
const url = editingId ? `${_API}/guardrails/rules/${editingId}` : `${_API}/guardrails/rules`;
const method = editingId ? 'PUT' : 'POST';
try {
const res = await fetch(url, { method, headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
if (!res.ok) throw new Error((await res.json()).detail || 'Save failed');
const saved = await res.json();
if (editingId) allRules = allRules.map(r => r.id === editingId ? saved : r);
else allRules.push(saved);
closeModal();
renderRules();
renderPiiPresets();
toast(editingId ? 'Rule updated' : 'Rule added');
} catch(e) { toast(e.message, true); }
}
async function deleteRule(id) {
const r = allRules.find(x => x.id === id);
if (!confirm(`Delete rule: "${r?.pattern}"?`)) return;
try {
const res = await fetch(`${_API}/guardrails/rules/${id}`, { method:'DELETE' });
if (!res.ok) throw new Error((await res.json()).detail || 'Failed');
allRules = allRules.filter(r => r.id !== id);
renderRules();
renderPiiPresets();
toast('Rule deleted');
} catch(e) { toast(e.message, true); }
}
// ── Tester ────────────────────────────────────────────────────────────────────
async function testPrompt() {
const text = document.getElementById('test-input').value.trim();
if (!text) { toast('Enter some text to test', true); return; }
try {
const res = await fetch(`${_API}/guardrails/check`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ text })
});
if (!res.ok) throw new Error((await res.json()).detail || 'Check failed');
const d = await res.json();
const resultEl = document.getElementById('test-result');
const summaryEl = document.getElementById('test-summary');
const matchesEl = document.getElementById('test-matches');
resultEl.style.display = '';
if (d.blocked) {
resultEl.className = 'gr-result blocked';
summaryEl.innerHTML = `<strong>🚫 Blocked</strong> — ${d.matches.length} rule${d.matches.length>1?'s':''} triggered`;
} else if (d.matches.length) {
resultEl.className = 'gr-result warn';
summaryEl.innerHTML = `<strong>⚠ Warning</strong> — ${d.matches.length} rule${d.matches.length>1?'s':''} triggered (warn-only)`;
} else {
resultEl.className = 'gr-result ok';
summaryEl.innerHTML = '<strong>✓ Clear</strong> — No rules triggered';
}
matchesEl.innerHTML = d.matches.map(m =>
`<div class="gr-match"><span class="badge-type-${m.type}">${m.type}</span> <span class="badge-action-${m.action}">${m.action}</span> &nbsp;<code>${escHtml(m.pattern)}</code>${m.label ? `${escHtml(m.label)}` : ''}</div>`
).join('');
} catch(e) { toast(e.message, true); }
}
// ── Log ───────────────────────────────────────────────────────────────────────
async function loadLog() {
try {
const res = await fetch(`${_API}/guardrails/log`);
if (!res.ok) throw new Error('Failed');
const rows = await res.json();
const el = document.getElementById('log-table');
if (!rows.length) { el.innerHTML = '<div class="gr-empty">No violations logged yet</div>'; return; }
let html = `<div class="gr-table-wrap"><table class="gr-table">
<tr><th>Time</th><th>User</th><th>Type</th><th>Pattern</th><th>Action</th></tr>`;
rows.forEach(r => {
html += `<tr>
<td style="color:var(--lt);white-space:nowrap">${fmtDate(r.logged_at)}</td>
<td>${escHtml(r.username || '—')}</td>
<td><span class="badge-type-${r.rule_type}">${escHtml(r.rule_type)}</span></td>
<td><code style="font-size:12px">${escHtml(r.pattern)}</code></td>
<td><span class="badge-action-${r.action}">${escHtml(r.action)}</span></td>
</tr>`;
});
html += '</table></div>';
el.innerHTML = html;
} catch(e) {
document.getElementById('log-table').innerHTML = '<div class="gr-empty">Failed to load log</div>';
}
}
// ── Utils ─────────────────────────────────────────────────────────────────────
function fmtDate(iso) {
if (!iso) return '—';
return new Date(iso).toLocaleString('en-IN', {day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit'});
}
function escHtml(s) {
return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function toast(msg, err) {
const el = document.getElementById('gr-toast');
el.textContent = msg; el.className = err ? 'err' : '';
el.style.display = 'block';
setTimeout(() => el.style.display = 'none', 3000);
}
document.getElementById('gr-modal-bg').addEventListener('click', e => { if (e.target === e.currentTarget) closeModal(); });
</script>
<script src="auth.js"></script>
<script>
document.addEventListener('cezenAuthReady', function() {
setTimeout(checkTier, 200);
});
</script>
<script src="branding.js"></script>
</body>
</html>