Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/components/alert-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ export function AlertItem({
<p className="min-w-0 flex-1 truncate text-sm text-neutral-900">
{titleLabel ?? alert.title.replace(/https?:\/\//g, "")}
</p>
{alert.suppressed && (
<span
className="hidden shrink-0 items-center rounded-full bg-neutral-100 px-2 py-0.5 text-[0.625rem] font-semibold tracking-wide text-neutral-500 uppercase ring-1 ring-neutral-950/5 ring-inset sm:inline-flex"
title={alert.suppressionReason?.trim() || "Held by the AI relevance filter"}
>
Held
</span>
)}
{(mag.added > 0 || mag.removed > 0) && (
<span className="hidden shrink-0 items-center gap-1.5 tabular-nums sm:inline-flex">
{mag.added > 0 && (
Expand Down Expand Up @@ -173,6 +181,15 @@ export function AlertItem({
</div>
{expanded && (
<div className={`border-t border-neutral-950/[0.05] pb-4 pt-3 ${rowPx}`}>
{alert.suppressed && (
<div className="mb-3 rounded-xl bg-neutral-50 px-3.5 py-3 text-xs leading-relaxed text-neutral-600 ring-1 ring-neutral-950/5">
<span className="font-semibold text-neutral-700">Held by the AI relevance filter.</span>{" "}
{alert.suppressionReason?.trim()
? `${alert.suppressionReason.trim().replace(/[.!?]+$/, "")}. `
: "Judged not to match this monitor's note. "}
No notification was sent.
</div>
)}
<DetailsView details={details} diffAnchorAlertId={alert.id} attribution={attribution} />
</div>
)}
Expand Down
43 changes: 43 additions & 0 deletions src/components/monitor-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export function MonitorCard({
const [checkIntervalHours, setCheckIntervalHours] = useState(target.checkIntervalHours ?? 1);
const [linkScope, setLinkScope] = useState<LinkScope>(() => coerceLinkScope(target.linkScope));
const [aiSummaryEnabled, setAiSummaryEnabled] = useState(target.aiChangeSummaryEnabled ?? false);
const [aiTriageEnabled, setAiTriageEnabled] = useState(target.aiTriageEnabled ?? false);
const [busy, setBusy] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(highlightEdit ?? false);
const [contentOpen, setContentOpen] = useState(false);
Expand All @@ -126,6 +127,7 @@ export function MonitorCard({
setCheckIntervalHours(target.checkIntervalHours ?? 1);
setLinkScope(coerceLinkScope(target.linkScope));
setAiSummaryEnabled(target.aiChangeSummaryEnabled ?? false);
setAiTriageEnabled(target.aiTriageEnabled ?? false);
}, [target]);

useEffect(() => {
Expand Down Expand Up @@ -177,6 +179,11 @@ export function MonitorCard({
if (await patch({ aiChangeSummaryEnabled: next })) setAiSummaryEnabled(next);
}

async function setAiTriagePreference(next: boolean) {
if (!aiSummaryConfigured && next) return;
if (await patch({ aiTriageEnabled: next })) setAiTriageEnabled(next);
}

return (
<li
ref={cardRef}
Expand Down Expand Up @@ -427,6 +434,42 @@ export function MonitorCard({
)}
</div>

<div className="mt-3 flex flex-wrap items-center gap-2">
<label className="inline-flex cursor-pointer items-center gap-2 text-xs text-neutral-700">
<span
className={`group relative inline-flex w-9 shrink-0 rounded-full p-0.5 ring-1 ring-inset transition-colors ${
aiTriageEnabled ? "bg-brand-500 ring-brand-500" : "bg-neutral-200 ring-neutral-950/5"
} ${busy || (!aiSummaryConfigured && !aiTriageEnabled) ? "opacity-60" : ""}`}
>
<span
className={`aspect-square w-1/2 rounded-full bg-white shadow-xs ring-1 ring-neutral-950/5 transition-transform ${
aiTriageEnabled ? "translate-x-full" : ""
}`}
/>
<input
type="checkbox"
className="absolute inset-0 size-full appearance-none focus:outline-none"
checked={aiTriageEnabled}
disabled={busy || (!aiSummaryConfigured && !aiTriageEnabled)}
onChange={() => void setAiTriagePreference(!aiTriageEnabled)}
aria-label="AI relevance filter"
/>
</span>
<span className="font-medium text-neutral-800">AI relevance filter</span>
</label>
{!aiSummaryConfigured && (
<Link href="/dashboard/settings" className="text-xs text-brand-700 hover:underline">
Set up
</Link>
)}
</div>
{aiTriageEnabled && (
<p className="mt-1.5 text-xs leading-relaxed text-neutral-500">
Changes that don&apos;t match this monitor&apos;s note are held in the dashboard instead of
notifying. Nothing is deleted.
</p>
)}

<div className="mt-4 border-t border-neutral-950/5 pt-3">
<button
type="button"
Expand Down