Interactive Answers
Clause Playbook
An agent's negotiation playbook for one clause — market spread as dot columns, walk-away, fallback and preferred zones on a two-handle track, and the settings it acts on.
1 variantMIT
Preview
§ 9.2Liability cap
Negotiation playbook · Corvane MSA
No edits
Comparing your playbook with 2,140 signed MSAs…
Market1 dot ≈ 9 MSAs
p45 of market
Liability cap thresholds
- Walk-away<6
- Fallback6–12
- Preferred12+
Data-breach super-capMultiple of the general cap
2×
Escalate toWho the agent pings before it concedes
Live in the playbook
Installation
Copies the component into components/ui and installs its dependencies and theme tokens.
pnpm dlx shadcn@latest add https://tuffui.com/r/clause-playbook.jsonUsage
Pass your own data and wire the callbacks to your agent runtime.
import {
ClausePlaybook,
marketPercentile,
type ClauseThresholds,
} from "@/components/ui/clause-playbook";
export function LiabilityCapAnswer({ proposal }: { proposal: AgentProposal }) {
const playbook = usePlaybook("liability-cap");
const [thresholds, setThresholds] = useState<ClauseThresholds>(proposal.thresholds);
const [values, setValues] = useState(proposal.values); // { escalate: "legal" }
return (
<ClausePlaybook
number="9.2"
clause="Liability cap"
unit="mo"
domain={[0, 24]}
// Signed contracts bucketed by cap: [{ value: 12, count: 260 }, …]
distribution={playbook.market}
// The live playbook is the baseline; the agent's proposal counts as edits.
defaultThresholds={playbook.thresholds}
thresholds={thresholds}
onThresholdsChange={setThresholds}
settings={[
{ kind: "stepper", id: "superCap", label: "Data-breach super-cap",
defaultValue: playbook.superCap, min: 1, max: 5, format: (v) => `${v}×` },
{ kind: "select", id: "escalate", label: "Escalate to", defaultValue: playbook.escalate,
options: [
{ value: "legal", label: "Legal", hint: "always" },
{ value: "finance", label: "Finance", hint: `below ${thresholds.preferred} mo` },
] },
{ kind: "toggle", id: "autoAccept", label: "Auto-accept within fallback",
description: (t) => `Agent signs off ${t.fallback}–${t.preferred} mo`,
defaultValue: playbook.autoAccept },
]}
values={values}
onValuesChange={setValues}
note={`Agent suggests ${thresholds.preferred} mo · p${marketPercentile(playbook.market, thresholds.preferred)} of market`}
sampleNoun="MSAs"
onApply={(t, v) => savePlaybook("liability-cap", { thresholds: t, ...v })}
onDiscard={() => proposal.dismiss()}
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| clause* | string | — | Clause name in the header, e.g. "Liability cap". Also names the handles for screen readers. |
| number | string | — | Clause number shown as "§ 9.2" before the name. |
| subtitle | ReactNode | — | Line under the title, e.g. the playbook and the contract. |
| icon | Icon | Scales | Phosphor icon in the header tile. |
| distribution* | ClauseBucket[] | — | Market data, one bucket per value. Each bucket is a stack of dots; when stacks get tall one dot stands for several contracts, noted above the chart. Dots below the fallback handle are dim, the fallback zone neutral, the preferred zone accent. |
| domain | [number, number] | [0, 24] | Ends of the scale. |
| step | number | 1 | Handles snap to this step. Arrow keys move one step. |
| largeStep | number | an eighth of the domain | Shift+arrow and Page Up / Page Down move this far. Home and End jump to the ends. |
| ticks | number[] | five even ticks | Axis values. A tick next to a handle gives way to the handle's own value, which is emphasized. |
| unit | string | — | Unit after values, e.g. "mo". Shown on the last tick and in screen-reader text. |
| format | (value: number) => string | — | Formats a value without the unit. |
| thresholds | ClauseThresholds | — | Controlled handles. Below `fallback` is walk-away, from `preferred` up is preferred. Changes made from outside, like an agent's proposal, glide into place. |
| defaultThresholds | ClauseThresholds | — | Starting handles when uncontrolled, and the baseline edits are counted against. Pass the live playbook here and the proposal as `thresholds`. |
| onThresholdsChange | (thresholds: ClauseThresholds) => void | — | Called while a handle is dragged or moved with the keyboard. |
| minGap | number | step | Smallest distance between the two handles. |
| zoneLabels | { walkAway?; fallback?; preferred? } | "Walk-away", "Fallback", "Preferred" | Names of the three zones in the legend and on the handles. |
| settings | ClauseSetting[] | [] | Rows under the chart: a stepper, a select with hints, or a toggle. A row's `defaultValue` is its baseline for the edit count. |
| values | Record<string, ClauseValue> | — | Controlled setting values by id. Ids left out use their `defaultValue`. |
| onValuesChange | (values, changedId?) => void | — | Called with every setting value when one changes. `changedId` is empty on Discard. |
| note | ReactNode | — | The agent's line above the chart, e.g. what it suggests and why. A new string slides in over the old one and is announced. |
| sampleNoun | string | "contracts" | What the market counts, plural, for "1 dot ≈ 22 MSAs". |
| onApply | (thresholds, values) => void | — | Shows Apply, enabled while there are edits. Current values become the new baseline, so the count goes back to zero. |
| onDiscard | () => void | — | Shows Discard. Handles and settings go back to the baseline first. |
| className | string | — | Extra classes for the root element. |
type ClauseThresholds = {
fallback: number; // walk-away below this
preferred: number; // preferred from this up
};
type ClauseBucket = { value: number; count: number };
type ClauseValue = number | string | boolean;
type ClauseSetting = {
id: string;
label: string;
description?: ReactNode | ((thresholds: ClauseThresholds) => ReactNode);
} & (
| { kind: "stepper"; defaultValue: number; min?: number; max?: number;
step?: number; format?: (value: number) => string }
| { kind: "select"; defaultValue: string; options: ClauseSelectOption[] }
| { kind: "toggle"; defaultValue: boolean }
);
type ClauseSelectOption = { value: string; label: string; hint?: string };
// Share of the market below a value, ties counted half: 49 → "p49".
function marketPercentile(distribution: ClauseBucket[], value: number): number;