+
+ {STEPS.map((s, i) => (
+ -
+
+
+ ))}
+
+
+ {STEPS[step] === "Protections" && (
+
+
+ {initial.protections.map((p) => {
+ const options = p.graded ? GRADES : ["off", "on"];
+ const floor = p.inherited ? RANK[p.inherited] : 0;
+ return (
+
+ |
+ {p.title}
+ {p.inherited && (
+
+ {LABEL[p.inherited]} for every agent{p.inherited_mode === "enforce" ? "" : " · watching"}
+
+ )}
+ {p.needs && p.needs_installed === false && choices[p.key] !== "off" && Model not installed here}
+ |
+
+
+ {options.map((o) => (
+
+ ))}
+
+ |
+
+ );
+ })}
+
+
+ )}
+
+ {STEPS[step] === "Words & topics" && (
+ <>
+
+
+
+
+
+ setAvoid(e.target.value)} placeholder="Legal advice, medical diagnosis" />
+
+
+
+ setAllowed(e.target.value)} placeholder="Orders, shipping, returns (empty: anything)" />
+
+ >
+ )}
+
+ {STEPS[step] === "Message" && (
+
+
+ setMessage(e.target.value)} placeholder="Sorry, I can't help with that. A teammate will follow up." />
+
+ )}
+
+ {STEPS[step] === "Review" && (
+ <>
+ {!saved && busy && !preview &&
Replaying the last 7 days…
}
+ {preview && (
+
+
+ Last 7 days for {initial.agent}
+ · {preview.replayed} requests
+
+
+ {preview.counts?.newly_blocked ?? 0} newly blocked
+ {preview.counts?.newly_escalated ?? 0} newly held
+
+
+ )}
+
+ {initial.protections
+ .filter((p) => choices[p.key] !== "off")
+ .map((p) => (
+ -
+ {p.title}: {LABEL[choices[p.key]]}
+
+ ))}
+ {payload().words.length > 0 && - Blocked words: {payload().words.length}
}
+ {avoid && - Avoids: {avoid}
}
+ {allowed && - Only: {allowed}
}
+ {message && - Message: “{message}”
}
+
+
+
+
+ >
+ )}
+
+ {STEPS[step] === "Test" && saved && (
+ <>
+
+ Saved · {saved.policy.mode === "enforce" ? "enforcing" : "watching"}
+ {saved.not_installed?.length > 0 && Model not installed: {saved.not_installed.join(", ")}}
+
+ Open policy
+
+
+
+ >
+ )}
+
+ {error &&
{error}
}
+
+ {step < STEPS.indexOf("Review") && (
+
+ {step > 0 && (
+
+ )}
+
+
+ )}
+
+ );
+}
+
+/** First-run choice: the protection's default, never below what every agent already gets. */
+function startLevel(p: Protection): string {
+ if (!p.graded) return p.default === "off" ? "off" : "on";
+ if (p.inherited && RANK[p.inherited] > RANK[p.default]) return p.inherited;
+ return p.default;
+}
diff --git a/dashboard/components/product/policies/RuleTests.tsx b/dashboard/components/product/policies/RuleTests.tsx
new file mode 100644
index 00000000..5a4cbd97
--- /dev/null
+++ b/dashboard/components/product/policies/RuleTests.tsx
@@ -0,0 +1,107 @@
+"use client";
+
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { callJson } from "@/components/kit/Act";
+
+type Result = { id: string; sample: string; surface: string; fires: boolean; fired: boolean | null; passed: boolean; trace_id: string | null };
+
+/**
+ * Messages this rule must catch, and ones it must leave alone. Each is re-checked
+ * against every proposed change on the Tune tab, before anything is applied.
+ */
+export function RuleTests({ ruleId, results, agents }: { ruleId: string; results: Result[]; agents: string[] }) {
+ const router = useRouter();
+ const [text, setText] = useState("");
+ const [surface, setSurface] = useState("input");
+ const [agent, setAgent] = useState(agents[0] || "");
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState("");
+ const base = `/api/rules/${encodeURIComponent(ruleId)}/examples`;
+
+ const run = async (fn: () => Promise