{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "decision-grader",
  "title": "Decision grader",
  "description": "Pick an action at a table snapshot and get it graded by the Poker Study AI agent: grade, EV lost, and what the agent would do. Needs your API key.",
  "registryDependencies": [
    "https://www.pokerstudy.ai/r/poker-core.json",
    "https://www.pokerstudy.ai/r/playing-card.json",
    "https://www.pokerstudy.ai/r/grade-badge.json",
    "https://www.pokerstudy.ai/r/pokerstudy-api.json"
  ],
  "files": [
    {
      "path": "src/components/pokerstudy/decision-grader.tsx",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { GradeBadge } from \"@/components/pokerstudy/grade-badge\";\nimport { Board, HoleCards } from \"@/components/pokerstudy/playing-card\";\nimport { actionColor, type EvaluateResponse, type TableSnapshot } from \"@/lib/pokerstudy/poker\";\n\nexport type { TableSnapshot };\n\ntype Selected = { action: string; amount?: number };\n\ntype State =\n  | { status: \"idle\" }\n  | { status: \"loading\"; selected: Selected }\n  | { status: \"error\"; message: string }\n  | { status: \"done\"; selected: Selected; result: EvaluateResponse };\n\nfunction choices(snapshot: TableSnapshot, raiseTo?: number[]): (Selected & { label: string })[] {\n  const out: (Selected & { label: string })[] = [];\n  for (const a of snapshot.valid_actions) {\n    if (a.action === \"fold\") out.push({ action: \"fold\", label: \"Fold\" });\n    else if (a.action === \"check\") out.push({ action: \"check\", label: \"Check\" });\n    else if (a.action === \"call\") out.push({ action: \"call\", amount: a.amount, label: `Call ${a.amount}` });\n    else if (a.action === \"raise\") {\n      const sizes = raiseTo?.filter((n) => n >= a.min && n <= a.max) ?? [];\n      for (const amount of sizes.length > 0 ? sizes : [a.min]) {\n        out.push({ action: \"raise\", amount, label: `Raise to ${amount}` });\n      }\n    }\n  }\n  return out;\n}\n\nconst kindOf = (action: string) =>\n  action === \"fold\" ? \"fold\" : action === \"check\" ? \"check\" : action === \"call\" ? \"call\" : \"raise\";\n\nfunction bb(chips: number, bigBlind: number) {\n  const v = chips / bigBlind;\n  return `${v >= 0 ? \"+\" : \"\"}${v.toFixed(Math.abs(v) < 10 ? 2 : 1)}bb`;\n}\n\n/**\n * Play a spot, get graded. Shows the hero's cards and board with one button\n * per legal action; picking one posts the snapshot to `endpoint` and shows the\n * grade, the EV given up, and what the Poker Study AI agent would have done.\n *\n * `endpoint` defaults to the `/api/pokerstudy/evaluate` route this item\n * installs, which adds your key on the server.\n */\nexport function DecisionGrader({\n  snapshot,\n  raiseTo,\n  endpoint = \"/api/pokerstudy/evaluate\",\n  initialResult,\n  onResult,\n}: {\n  snapshot: TableSnapshot;\n  /** Raise-to amounts to offer, in chips. Defaults to the minimum raise. */\n  raiseTo?: number[];\n  endpoint?: string;\n  /** Render a result without calling the API, e.g. for a static preview. */\n  initialResult?: { selected: Selected; result: EvaluateResponse };\n  onResult?: (result: EvaluateResponse, selected: Selected) => void;\n}) {\n  const [state, setState] = useState<State>(\n    initialResult ? { status: \"done\", ...initialResult } : { status: \"idle\" },\n  );\n  const bigBlind = snapshot.big_blind ?? 20;\n  const busy = state.status === \"loading\";\n  const picked = state.status === \"done\" || state.status === \"loading\" ? state.selected : null;\n\n  async function grade(selected: Selected) {\n    setState({ status: \"loading\", selected });\n    try {\n      const res = await fetch(endpoint, {\n        method: \"POST\",\n        headers: { \"content-type\": \"application/json\" },\n        body: JSON.stringify({ ...snapshot, selected }),\n      });\n      const body = (await res.json().catch(() => null)) as (EvaluateResponse & { error?: string }) | null;\n      if (!res.ok || !body || body.error) {\n        setState({ status: \"error\", message: body?.error ?? `HTTP ${res.status}` });\n        return;\n      }\n      setState({ status: \"done\", selected, result: body });\n      onResult?.(body, selected);\n    } catch (err) {\n      setState({ status: \"error\", message: (err as Error).message });\n    }\n  }\n\n  return (\n    <div className=\"flex w-full max-w-[560px] flex-col gap-5\">\n      <div className=\"flex flex-wrap items-end gap-6\">\n        <div className=\"flex flex-col gap-1.5\">\n          <span className=\"text-xs text-muted-foreground\">Your hand</span>\n          <HoleCards cards={snapshot.hole} size=\"md\" />\n        </div>\n        {snapshot.board && snapshot.board.length > 0 ? (\n          <div className=\"flex flex-col gap-1.5\">\n            <span className=\"text-xs text-muted-foreground\">Board</span>\n            <Board cards={snapshot.board} size=\"sm\" />\n          </div>\n        ) : null}\n        <div className=\"flex flex-col gap-1.5 text-sm\">\n          <span className=\"text-xs text-muted-foreground\">Pot</span>\n          <span className=\"font-mono tabular-nums text-foreground\">\n            {snapshot.pot ?? bigBlind * 1.5} <span className=\"text-muted-foreground\">· BB {bigBlind}</span>\n          </span>\n        </div>\n      </div>\n\n      <div className=\"flex flex-wrap gap-2\">\n        {choices(snapshot, raiseTo).map((c) => {\n          const active = picked?.action === c.action && picked?.amount === c.amount;\n          return (\n            <button\n              key={c.label}\n              type=\"button\"\n              disabled={busy}\n              onClick={() => grade({ action: c.action, amount: c.amount })}\n              style={{ borderColor: actionColor(kindOf(c.action), c.label) }}\n              className={`rounded-md border-2 px-3 py-1.5 text-sm font-semibold text-foreground transition-opacity disabled:opacity-50 ${\n                active ? \"bg-white/10\" : \"bg-transparent hover:bg-white/5\"\n              }`}\n            >\n              {c.label}\n            </button>\n          );\n        })}\n      </div>\n\n      <div aria-live=\"polite\" className=\"min-h-[5.5rem]\">\n        {state.status === \"loading\" ? (\n          <span className=\"text-sm text-muted-foreground\">Playing the hand out…</span>\n        ) : state.status === \"error\" ? (\n          <p role=\"alert\" className=\"text-sm text-red-300\">\n            {state.message}\n          </p>\n        ) : state.status === \"done\" ? (\n          <div className=\"flex flex-col gap-3\">\n            <GradeBadge grade={state.result.grade} evLossBb={state.result.ev_loss_bb} />\n            <dl className=\"grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm\">\n              <dt className=\"text-muted-foreground\">You</dt>\n              <dd className=\"font-mono tabular-nums text-foreground\">\n                {state.result.selected.action}\n                {state.result.selected.amount ? ` ${state.result.selected.amount}` : \"\"} ·{\" \"}\n                {bb(state.result.selected.ev, bigBlind)}\n              </dd>\n              <dt className=\"text-muted-foreground\">Agent</dt>\n              <dd className=\"font-mono tabular-nums text-foreground\">\n                {state.result.bot.action}\n                {state.result.bot.amount ? ` ${state.result.bot.amount}` : \"\"} · {bb(state.result.bot.ev, bigBlind)}\n              </dd>\n              <dt className=\"text-muted-foreground\">Equity</dt>\n              <dd className=\"font-mono tabular-nums text-foreground\">{Math.round(state.result.equity * 100)}%</dd>\n            </dl>\n          </div>\n        ) : (\n          <span className=\"text-sm text-muted-foreground\">Pick an action to grade it.</span>\n        )}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pokerstudy/decision-grader.tsx"
    }
  ],
  "type": "registry:block"
}