component
Generative UI (json-render)
Every Poker Study component as a json-render catalog entry: a props schema, a description that tells a model when to use it, an example, and the renderer that draws it. Spread them into your catalog and registry next to @json-render/shadcn.
Every component as a json-render catalog entry, so a model can answer with poker UI instead of text. Spread pokerstudyComponentDefinitions into defineCatalog and pokerstudyComponents into defineRegistry, next to @json-render/shadcn. Props are plain JSON, nullable rather than optional, and data blocks take ids and spots, never payloads. Each component page shows its entry. Below, a spec a coach might stream.
Sample dataInaccuracy−0.17bb
BTN facing a UTG open, 100bb
A♠
5♠
You called. The solver mostly 3-bets A5s here.
8%Fold21%Call+0.41bb71%Raise 8bb+0.58bb
Installation
$ npx shadcn@latest add https://www.pokerstudy.ai/r/pokerstudy-json-render.jsonSource
components/pokerstudy/json-render.tsx
"use client";
import type { BaseComponentProps } from "@json-render/react";
import { useBoundProp } from "@json-render/react";
import { AgentAdvisor } from "@/components/pokerstudy/agent-advisor";
import { DecisionGrader } from "@/components/pokerstudy/decision-grader";
import { GradeBadge } from "@/components/pokerstudy/grade-badge";
import { HandMatrix } from "@/components/pokerstudy/hand-matrix";
import { LimpRange } from "@/components/pokerstudy/limp-range";
import { OpponentProfile } from "@/components/pokerstudy/opponent-profile";
import { Board, HoleCards, PlayingCard } from "@/components/pokerstudy/playing-card";
import { PlayerStatsGrid } from "@/components/pokerstudy/player-stats";
import { PreflopRangeExplorer } from "@/components/pokerstudy/preflop-range-explorer";
import { SessionStats } from "@/components/pokerstudy/session-stats";
import { StrategyBar } from "@/components/pokerstudy/strategy-bar";
import { TrainingReport } from "@/components/pokerstudy/training-report";
import { ChipStack } from "@/components/trainer/chips";
import { PlayingCard as TableCard } from "@/components/trainer/cards";
import { ActionBar } from "@/components/trainer/action-bar";
import { GtoStrategyBar } from "@/components/trainer/gto-strategy-bar";
import { HistoryPanel } from "@/components/trainer/history-panel";
import { PostflopActionBar } from "@/components/trainer/postflop-action-bar";
import { TableTheme } from "@/components/trainer/skin";
import { PokerTable } from "@/components/trainer/table";
import type { PokerstudyComponentName, PokerstudyProps } from "@/lib/pokerstudy/catalog";
import { actionColor, type ActionPaletteId, type PlayerStats, type TableSnapshot } from "@/lib/pokerstudy/poker";
/**
* Renderers for `pokerstudyComponentDefinitions`: each takes the JSON props a
* model wrote and draws the real component. Pass them to `defineRegistry`
* next to the definitions, the way `@json-render/shadcn` pairs
* `shadcnComponents` with `shadcnComponentDefinitions`.
*/
type P<K extends PokerstudyComponentName> = BaseComponentProps<PokerstudyProps<K>>;
/** Where the data blocks fetch from. Defaults are the routes the `pokerstudy-api` item installs. */
export interface PokerstudyEndpoints {
/** RangeExplorer. Default `/api/pokerstudy/node`. */
node?: string;
/** DecisionGrader. Default `/api/pokerstudy/evaluate`. */
evaluate?: string;
/** AgentAdvisor. Default `/api/pokerstudy/decide`. */
decide?: string;
/** TrainingReport, OpponentProfile, LimpRange and SessionStats. Default `/api/pokerstudy`. */
base?: string;
}
/** The action color for a label such as "Raise 2.5bb" or "All-in". */
function labelColor(label: string, palette: ActionPaletteId | null): string {
const l = label.toLowerCase();
if (l.startsWith("fold")) return actionColor("fold", undefined, palette);
if (l.startsWith("check")) return actionColor("check", undefined, palette);
if (l.startsWith("call") || l.startsWith("limp")) return actionColor("call", undefined, palette);
if (l.startsWith("all") || l === "ai" || l.startsWith("jam") || l.startsWith("shove")) return actionColor("allin", undefined, palette);
if (l.startsWith("bet") || l.startsWith("raise") || l.startsWith("open") || /^\d/.test(l)) return actionColor("raise", label, palette);
return actionColor("other");
}
/** The API's snapshot leaves unknown fields out; a spec writes them as null. */
function toSnapshot(snapshot: PokerstudyProps<"AgentAdvisor">["snapshot"]): TableSnapshot {
return Object.fromEntries(Object.entries(snapshot).filter(([, v]) => v != null)) as TableSnapshot;
}
function HandMatrixRenderer({ props, bindings }: P<"HandMatrix">) {
const [selected, setSelected] = useBoundProp<string | null>(props.selected, bindings?.selected);
return (
<HandMatrix
cells={props.cells}
colors={props.colors}
legend={props.legend}
selected={selected}
onSelect={bindings?.selected ? setSelected : undefined}
/>
);
}
function ActionBarRenderer({ props, bindings, emit }: P<"ActionBar">) {
const [, setValue] = useBoundProp<string | null>(props.value, bindings?.value);
return (
<ActionBar
actions={props.actions.map((a) => ({ ...a, weights: {} }))}
history={props.history ?? undefined}
stackBb={props.stackBb ?? undefined}
disabled={false}
onSelect={(action) => {
setValue(action);
emit("select");
}}
/>
);
}
function PostflopActionBarRenderer({ props, bindings, emit }: P<"PostflopActionBar">) {
const [, setValue] = useBoundProp<string | null>(props.value, bindings?.value);
return (
<PostflopActionBar
actions={props.actions}
disabled={false}
onSelect={(label) => {
setValue(label);
emit("select");
}}
/>
);
}
function PokerTableRenderer({ props }: P<"PokerTable">) {
const table = (
<div data-skin={props.theme ?? undefined} className="@container h-[560px] w-full overflow-hidden rounded-lg sm:h-[440px]">
<PokerTable
seats={props.seats.map((s, i) => ({
pos: s.pos,
name: s.name,
remaining: s.stack,
committed: s.bet,
folded: s.folded,
isHero: i === 0,
isDealer: s.dealer,
action: s.action,
hole: s.cards,
acting: s.acting ?? false,
}))}
pot={props.pot}
heroCards={props.heroCards}
avatars={false}
center={
props.board?.length ? (
<div className="flex items-center justify-center gap-1.5">
{props.board.map((card) => (
<TableCard key={card} card={card} size="board" />
))}
</div>
) : undefined
}
/>
</div>
);
return props.theme ? <TableTheme skin={props.theme}>{table}</TableTheme> : table;
}
/**
* The renderers, with the data blocks pointed at `endpoints`. Use this when
* your proxy lives somewhere other than `/api/pokerstudy`.
*/
export function createPokerstudyComponents(endpoints: PokerstudyEndpoints = {}) {
const base = endpoints.base ?? "/api/pokerstudy";
return {
PlayingCard: ({ props }: P<"PlayingCard">) => <PlayingCard card={props.card} size={props.size ?? undefined} />,
HoleCards: ({ props }: P<"HoleCards">) => <HoleCards cards={props.cards} size={props.size ?? undefined} />,
Board: ({ props }: P<"Board">) => <Board cards={props.cards} size={props.size ?? undefined} />,
StrategyBar: ({ props }: P<"StrategyBar">) => (
<StrategyBar
highlight={props.highlight}
items={props.items.map((i) => ({
label: i.label,
freq: i.freq,
color: i.color ?? labelColor(i.label, props.palette),
detail: i.detail ?? undefined,
}))}
/>
),
GradeBadge: ({ props }: P<"GradeBadge">) => <GradeBadge grade={props.grade} evLossBb={props.evLossBb} />,
PlayerStats: ({ props }: P<"PlayerStats">) => (
<PlayerStatsGrid stats={props.stats as PlayerStats} keys={props.keys ?? undefined} />
),
HandMatrix: HandMatrixRenderer,
ChipStack: ({ props }: P<"ChipStack">) => (
<ChipStack amount={props.amount} label={props.label ?? undefined} variant={props.variant ?? undefined} />
),
GtoStrategyBar: ({ props }: P<"GtoStrategyBar">) => (
<GtoStrategyBar
actions={props.actions.map((a) => ({ ...a, weights: {} }))}
strategy={props.strategy}
highlightAction={props.highlightAction}
/>
),
PokerTable: PokerTableRenderer,
ActionBar: ActionBarRenderer,
PostflopActionBar: PostflopActionBarRenderer,
HandHistory: ({ props }: P<"HandHistory">) => (
<div className="h-[360px] overflow-hidden rounded-md">
<HistoryPanel
onClose={() => {}}
items={props.items.map((item) => ({
id: item.id,
cards: item.cards ?? undefined,
title: item.title ?? undefined,
subtitle: item.subtitle,
decisions: item.decisions.map((d) => ({
phase: d.phase ?? undefined,
label: d.label,
tier: d.tier,
freq: d.freq,
evBb: d.evBb,
evLossBb: d.evLossBb ?? undefined,
})),
}))}
/>
</div>
),
RangeExplorer: ({ props }: P<"RangeExplorer">) => (
<PreflopRangeExplorer
stack={props.stack}
history={props.history}
game={props.game ?? undefined}
initialHand={props.initialHand}
endpoint={endpoints.node ?? "/api/pokerstudy/node"}
palette={props.palette ?? undefined}
/>
),
DecisionGrader: ({ props }: P<"DecisionGrader">) => (
<DecisionGrader
snapshot={toSnapshot(props.snapshot)}
raiseTo={props.raiseTo ?? undefined}
endpoint={endpoints.evaluate ?? "/api/pokerstudy/evaluate"}
/>
),
AgentAdvisor: ({ props }: P<"AgentAdvisor">) => (
<AgentAdvisor snapshot={toSnapshot(props.snapshot)} endpoint={endpoints.decide ?? "/api/pokerstudy/decide"} />
),
TrainingReport: () => <TrainingReport endpoint={base} />,
OpponentProfile: ({ props }: P<"OpponentProfile">) => (
<OpponentProfile opponentId={props.opponentId ?? undefined} endpoint={base} />
),
LimpRange: ({ props }: P<"LimpRange">) => (
<LimpRange
opponentId={props.opponentId ?? undefined}
position={props.position ?? undefined}
endpoint={base}
palette={props.palette ?? undefined}
/>
),
SessionStats: ({ props }: P<"SessionStats">) => <SessionStats sessionId={props.sessionId ?? undefined} endpoint={base} />,
};
}
/** Every renderer, fetching through the `/api/pokerstudy` proxy. */
export const pokerstudyComponents = createPokerstudyComponents();