import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; /** * Content Policy editor. * * Three term lists, edited as structured rows rather than raw text: severity is a * segmented control, not a punctuation prefix you have to remember. A Text tab is kept * alongside for bulk paste, because typing 100 terms one row at a time is nobody's idea * of user friendly. * * The live tester is the point of the whole screen. A word list is abstract; "paste a * prompt, watch what happens to it" is not. Edits are testable before saving because the * test runs against the in-memory draft, not the file on disk. */ const TABS = [ { key: "blocked", label: "Blocked", hint: "Terms to catch. Severity decides whether a match blocks outright or only inside a sexual prompt." }, { key: "allowed", label: "Allowed", hint: "Phrases exempted before matching, so a flagged word inside a proper noun does not fire." }, { key: "context", label: "Context", hint: "Words that make a prompt count as sexual. These activate the 'In context' terms — they never block on their own." }, ]; const SEVERITIES = [ { key: "always", short: "Always", tip: "Blocks on its own, in any prompt. Never sanitized — the image is replaced with a grey placeholder." }, { key: "contextual", short: "In context", tip: "Only counts when the prompt is also sexual. The tag is removed and generation continues." }, { key: "pattern", short: "Pattern", tip: "A regular expression. Always blocks, like Always." }, ]; const CSS = ` .rmcp-backdrop { position:fixed; inset:0; background:rgba(0,0,0,.6); backdrop-filter:blur(3px); z-index:10000; display:flex; align-items:center; justify-content:center; } .rmcp { width:min(1080px,94vw); height:min(760px,92vh); background:#1b1d21; color:#e7e9ec; border:1px solid #33373d; border-radius:14px; display:flex; flex-direction:column; overflow:hidden; box-shadow:0 24px 70px rgba(0,0,0,.55); font:13px/1.5 ui-sans-serif,-apple-system,"Segoe UI",sans-serif; } .rmcp-head { padding:16px 20px 0; border-bottom:1px solid #2b2f35; } .rmcp-titlerow { display:flex; align-items:baseline; gap:12px; margin-bottom:14px; } .rmcp-title { font-size:15px; font-weight:600; letter-spacing:-.01em; } .rmcp-sub { color:#868e98; font-size:12px; } .rmcp-x { margin-left:auto; background:none; border:0; color:#868e98; font-size:20px; cursor:pointer; line-height:1; padding:0 2px; } .rmcp-x:hover { color:#e7e9ec; } .rmcp-tabs { display:flex; gap:2px; } .rmcp-tab { background:none; border:0; border-bottom:2px solid transparent; color:#868e98; padding:8px 14px 10px; cursor:pointer; font-size:13px; display:flex; align-items:center; gap:7px; } .rmcp-tab:hover { color:#c8ccd2; } .rmcp-tab.on { color:#e7e9ec; border-bottom-color:#5b9dff; } .rmcp-count { font-size:11px; background:#2b3037; color:#9aa2ac; border-radius:9px; padding:1px 7px; } .rmcp-tab.on .rmcp-count { background:#1e3a63; color:#8fbaff; } .rmcp-body { flex:1; display:flex; min-height:0; } .rmcp-left { flex:1.35; display:flex; flex-direction:column; min-width:0; border-right:1px solid #2b2f35; } .rmcp-right { flex:1; display:flex; flex-direction:column; min-width:0; background:#16181b; } .rmcp-hint { padding:11px 20px; color:#868e98; font-size:12px; border-bottom:1px solid #2b2f35; } .rmcp-toolbar { display:flex; gap:8px; align-items:center; padding:11px 20px; border-bottom:1px solid #2b2f35; } .rmcp-add { flex:1; background:#111316; border:1px solid #33373d; border-radius:8px; padding:7px 11px; color:#e7e9ec; font-size:13px; font-family:inherit; } .rmcp-add:focus { outline:none; border-color:#5b9dff; } .rmcp-seg { display:flex; border:1px solid #33373d; border-radius:8px; overflow:hidden; } .rmcp-seg button { background:#111316; border:0; color:#868e98; padding:7px 10px; cursor:pointer; font-size:12px; border-right:1px solid #2b2f35; } .rmcp-seg button:last-child { border-right:0; } .rmcp-seg button.on { background:#1e3a63; color:#bcd7ff; } .rmcp-seg button:hover:not(.on) { color:#c8ccd2; } .rmcp-list { flex:1; overflow-y:auto; padding:8px 12px 16px; } .rmcp-row { display:flex; align-items:center; gap:9px; padding:5px 8px; border-radius:8px; } .rmcp-row:hover { background:#212429; } .rmcp-term { flex:1; background:none; border:0; color:#e7e9ec; font-size:13px; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; padding:3px 0; min-width:0; } .rmcp-term:focus { outline:none; } .rmcp-row .rmcp-seg { opacity:0; transition:opacity .12s; } .rmcp-row:hover .rmcp-seg, .rmcp-row.sev-always .rmcp-seg, .rmcp-row.sev-pattern .rmcp-seg { opacity:1; } .rmcp-seg.mini button { padding:3px 8px; font-size:11px; } .rmcp-del { background:none; border:0; color:#5c646e; cursor:pointer; font-size:16px; opacity:0; padding:0 4px; line-height:1; } .rmcp-row:hover .rmcp-del { opacity:1; } .rmcp-del:hover { color:#ff7676; } .rmcp-dot { width:7px; height:7px; border-radius:50%; flex:0 0 auto; background:#4a525c; } .rmcp-row.sev-always .rmcp-dot { background:#ff6b6b; } .rmcp-row.sev-pattern .rmcp-dot { background:#c98bff; } .rmcp-row.sev-contextual .rmcp-dot { background:#ffb020; } .rmcp-textarea { flex:1; width:100%; background:#111316; border:0; color:#e7e9ec; padding:14px 20px; font:12.5px/1.7 ui-monospace,SFMono-Regular,Menlo,monospace; resize:none; } .rmcp-textarea:focus { outline:none; } .rmcp-testhead { padding:13px 18px 10px; font-size:11px; font-weight:600; letter-spacing:.06em; text-transform:uppercase; color:#868e98; } .rmcp-test { margin:0 18px; background:#111316; border:1px solid #33373d; border-radius:9px; padding:11px; color:#e7e9ec; font:12.5px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace; resize:none; height:118px; } .rmcp-test:focus { outline:none; border-color:#5b9dff; } .rmcp-verdict { margin:14px 18px 0; border-radius:9px; padding:12px 14px; border:1px solid; } .rmcp-verdict.clean { background:#12281c; border-color:#1f4d33; } .rmcp-verdict.sanitized { background:#2b2210; border-color:#5c481a; } .rmcp-verdict.blocked { background:#2e1416; border-color:#5e2126; } .rmcp-verdict.idle { background:#1a1c20; border-color:#2b2f35; color:#5c646e; } .rmcp-vtop { display:flex; align-items:center; gap:9px; margin-bottom:7px; } .rmcp-badge { font-size:10px; font-weight:700; letter-spacing:.07em; padding:3px 8px; border-radius:5px; } .rmcp-badge.clean { background:#2ecc8a; color:#062616; } .rmcp-badge.sanitized { background:#ffb020; color:#2a1c00; } .rmcp-badge.blocked { background:#ff5c5c; color:#2c0708; } .rmcp-score { margin-left:auto; font-size:11px; color:#868e98; font-family:ui-monospace,monospace; } .rmcp-why { font-size:12px; color:#a8b0ba; margin-bottom:9px; } .rmcp-out { background:#0d0f11; border-radius:7px; padding:9px 11px; font:12px/1.55 ui-monospace,monospace; color:#c8ccd2; max-height:150px; overflow:auto; white-space:pre-wrap; word-break:break-word; } .rmcp-outlabel { font-size:10px; text-transform:uppercase; letter-spacing:.06em; color:#5c646e; margin-bottom:5px; } .rmcp-foot { display:flex; align-items:center; gap:12px; padding:13px 20px; border-top:1px solid #2b2f35; background:#15171a; } .rmcp-status { flex:1; font-size:12px; color:#868e98; min-width:0; } .rmcp-status.err { color:#ff8a8a; } .rmcp-status.ok { color:#5fd6a0; } .rmcp-btn { border-radius:8px; padding:8px 16px; font-size:13px; cursor:pointer; border:1px solid #3a3f47; background:#23272d; color:#e7e9ec; font-family:inherit; } .rmcp-btn:hover { background:#2b3037; } .rmcp-btn.primary { background:#3b7de0; border-color:#3b7de0; color:#fff; font-weight:500; } .rmcp-btn.primary:hover { background:#4a8bee; } .rmcp-btn:disabled { opacity:.45; cursor:default; } .rmcp-empty { color:#5c646e; padding:26px 10px; text-align:center; font-size:12.5px; } `; class ContentPolicyModal { constructor() { this.tab = "blocked"; this.mode = "list"; this.lists = { blocked: [], allowed: [], context: [] }; this.dirty = false; this.newSeverity = "contextual"; this.testTimer = null; } async open() { this._injectCss(); this._build(); await this._load(); } _injectCss() { if (document.getElementById("rmcp-css")) return; const s = document.createElement("style"); s.id = "rmcp-css"; s.textContent = CSS; document.head.appendChild(s); } _build() { this.backdrop = document.createElement("div"); this.backdrop.className = "rmcp-backdrop"; this.backdrop.addEventListener("mousedown", (e) => { if (e.target === this.backdrop) this._close(); }); this.backdrop.innerHTML = `