568 lines
29 KiB
HTML
568 lines
29 KiB
HTML
<!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 & 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 & 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 & 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 · Powered by Cezen · <span data-brand="tier">Basic Tier</span></p>
|
||
<p>Questions? <a href="mailto:support@cezentech.com">support@cezentech.com</a> · <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:'13–16 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> <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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
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>
|