# Differential Privacy Controls

Differential Privacy Controls [#differential-privacy-controls]

The **Differential Privacy Controls** are a collapsible settings panel — an
enable toggle, an epsilon slider with a derived privacy-level label
(High/Balanced/Low), and a privacy-budget bar that turns warning then error as
the budget is consumed — plus a compact **DP Applied** provenance badge (lock
icon, epsilon used, embedding dimensionality) for rendering beneath protected
output.

> **No turnkey hook.** This primitive does no DP math. There is no
> `@localmode/react` hook behind it — the app owns the DP state, wiring
> [`dpEmbeddingMiddleware` / `dpClassificationMiddleware`](https://localmode.dev/docs/core)
> and a `createPrivacyBudget` tracker from `@localmode/core`, then passing
> `enabled`, `epsilon`, and the live `budget` in. The component only renders and
> reports user intent through the change callbacks.

**When to use it:** any local-first surface that perturbs embeddings or
classification results with differential privacy and wants the user to control
the privacy/accuracy tradeoff and see how much budget is left.

Preview [#preview]

```tsx
'use client';

import { useState } from 'react';

import {
  DifferentialPrivacyControls,
  DpAppliedBadge,
} from '@/components/differential-privacy-controls';

/**
 * Demo for the DifferentialPrivacyControls component, used by the docs live
 * preview.
 *
 * Holds DP state locally and simulates budget consumption with a "Run query"
 * button so you can watch the budget bar transition ok → warning → error. In a
 * real app this state comes from `dpEmbeddingMiddleware` /
 * `dpClassificationMiddleware` and a `createPrivacyBudget` tracker — the
 * component only renders it.
 */
export default function DifferentialPrivacyControlsDemo() {
  const [enabled, setEnabled] = useState(true);
  const [epsilon, setEpsilon] = useState(1.0);
  const [consumed, setConsumed] = useState(0);

  const maxEpsilon = 10;
  const lastApplied = consumed > 0;

  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      <DifferentialPrivacyControls
        enabled={enabled}
        onEnabledChange={setEnabled}
        epsilon={epsilon}
        onEpsilonChange={setEpsilon}
        budget={{ consumed, maxEpsilon }}
      />

      <button
        type="button"
        disabled={!enabled || consumed >= maxEpsilon}
        onClick={() => setConsumed((c) => Math.min(maxEpsilon, c + epsilon))}
        className="h-9 self-start rounded-md border border-input bg-background px-3 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50"
      >
        Run protected query (consume ε)
      </button>

      {/* "DP Applied" provenance chip — only shown when DP was applied */}
      {enabled && lastApplied && (
        <div className="flex items-center gap-2 rounded-md border border-border bg-card p-3 text-sm">
          <span className="text-muted-foreground">Protected result</span>
          <DpAppliedBadge epsilon={epsilon} dimensions={384} />
        </div>
      )}
    </div>
  );
}
```

Installation [#installation]

```bash
npx shadcn@latest add @localmode/ui/security-privacy/differential-privacy-controls
```

Dependencies [#dependencies]

* **Data source:** renders the `enabled` / `epsilon` / `budget` props you pass and emits change callbacks — works with any backend; the component does no DP math. Recommended LocalMode producers: `dpEmbeddingMiddleware` / `dpClassificationMiddleware` + `createPrivacyBudget` from `@localmode/core`, whose state you wire in (optional).
* `lucide-react` — the `ShieldCheck` / `ChevronDown` / `Lock` icons
* `radix-ui` — via the `Collapsible`, `Slider`, and `Switch` primitives
* `ui/collapsible`, `ui/slider`, `ui/switch`, `ui/badge` — the shadcn/ui base primitives (installed automatically as registry dependencies)
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)

Files installed [#files-installed]

* `differential-privacy-controls.tsx` — the panel **and** the `DpAppliedBadge`
* `ui/collapsible.tsx`, `ui/slider.tsx`, `ui/switch.tsx`, `ui/badge.tsx` — base primitives (registry dependencies)
* `lib/utils.ts` — the `cn()` helper (if not already present)

Props [#props]

DifferentialPrivacyControls [#differentialprivacycontrols]

**DifferentialPrivacyControls**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | `boolean` | — | **Required.** Whether differential privacy is enabled. Controlled by the app's DP state (typically whether `dpEmbeddingMiddleware` / `dpClassificationMiddleware` is wired into the pipeline). |
| `onEnabledChange` | `function` | — | **Required.** Called when the user toggles DP on or off. |
| `epsilon` | `number` | — | **Required.** Privacy parameter epsilon (privacy budget per query). Lower epsilon = more privacy, more noise. Matches `DPEmbeddingConfig.epsilon` / `DPClassificationConfig.epsilon` in `@localmode/core`. |
| `onEpsilonChange` | `function` | — | **Required.** Called when the user moves the epsilon slider. |
| `minEpsilon` | `number` | `0.1` | Minimum selectable epsilon. |
| `maxEpsilon` | `number` | `10` | Maximum selectable epsilon. |
| `step` | `number` | `0.1` | Slider step. |
| `budget` | `object` | — | Live privacy budget from the app's tracker (`createPrivacyBudget`). When provided, a budget bar is shown that turns warning then error as the consumed epsilon approaches the maximum. |
| `open` | `boolean` | `true` | Open state of the collapsible panel. Controlled only when paired with `onOpenChange`; without a handler it seeds the initial (uncontrolled) state so the trigger still expands/collapses. |
| `onOpenChange` | `function` | — | Called when the panel is expanded or collapsed. |

DpAppliedBadge [#dpappliedbadge]

**DpAppliedBadge**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `epsilon` | `number` | — | **Required.** Epsilon value applied to the protected output. |
| `dimensions` | `number` | — | Embedding dimensionality the noise was applied across (e.g. the embedding model's `dimensions`). Optional — omitted for non-vector outputs. |

Examples [#examples]

Driven by app DP state [#driven-by-app-dp-state]

The app owns the DP middleware and the budget tracker. The panel renders that
state and reports user changes:

```tsx
import { createPrivacyBudget } from '@localmode/core';
import { DifferentialPrivacyControls } from '@/components/differential-privacy-controls';

const budget = await createPrivacyBudget({ maxEpsilon: 10, persistKey: 'my-app' });

export function Settings() {
  const [enabled, setEnabled] = useState(true);
  const [epsilon, setEpsilon] = useState(1.0);
  const [consumed, setConsumed] = useState(budget.consumed());

  return (
    <DifferentialPrivacyControls
      enabled={enabled}
      onEnabledChange={setEnabled}
      epsilon={epsilon}
      onEpsilonChange={setEpsilon}
      budget={{ consumed, maxEpsilon: 10 }}
    />
  );
}
```

Each protected query consumes epsilon from the real tracker; reflect that into the
budget prop so the bar transitions ok → warning → error:

```tsx
// after a DP-protected embedding/classification runs
budget.consume(epsilon);
setConsumed(budget.consumed());
```

"DP Applied" provenance badge [#dp-applied-provenance-badge]

Render the badge beneath protected output **only when DP was actually applied**,
with the real epsilon and the embedding model's dimensionality:

```tsx
import { DpAppliedBadge } from '@/components/differential-privacy-controls';

{dpEnabled && <DpAppliedBadge epsilon={epsilon} dimensions={model.dimensions} />}
```

The badge fields must match the values emitted by the DP middleware run
(`DPEmbeddingConfig.epsilon`, the embedding model's `dimensions`) — never
placeholder values.

Disabled state [#disabled-state]

When the toggle is off, the slider and budget bar dim and you should not render
the `DpAppliedBadge` — no DP was applied, so showing provenance would be
misleading:

```tsx
<DifferentialPrivacyControls enabled={false} /* … */ />
{/* no DpAppliedBadge while disabled */}
```

Customization [#customization]

The panel is styled with shadcn/ui CSS-variable utilities (`bg-card`,
`border-border`, `text-muted-foreground`) and composes the `Collapsible`,
`Slider`, `Switch`, and `Badge` base primitives, so it inherits your theme. The
budget/level state colors use Tailwind's `red` / `amber` / `emerald` palettes
directly — swap those classes in the copied file to match your design system.

Because you own the file, you can adjust the epsilon→level thresholds
(`privacyLevelForEpsilon`) and the budget warning/error thresholds to match your
privacy policy.

Accessibility [#accessibility]

The ε control renders the Radix `Slider` primitive **directly** (Radix ignores
`aria-label` on the Root), so the accessible name and readout land on the
focusable Thumb — the `role="slider"` element. The Thumb carries a fixed
`aria-label` (`"Epsilon (privacy budget per query)"`) and an `aria-valuetext`
that spells out both the value and the derived privacy level (e.g. `"ε 1.0 —
High privacy"`), so screen readers announce the privacy trade-off, not just a
number. The spent-budget bar is a labelled `role="progressbar"` (`"Privacy
budget consumed"`). `getByRole('slider', { name: /epsilon/i })` resolves the
control uniquely.