jev-ui

September 21, 2026 · View on GitHub

jev-ui — probabilistic UI for React

jev-ui

npm ci

React components that decide which component to render, how to order a list, and whether to show an affordance — from calibrated judgments returned by TypeSafe's Jev.

The model returns a typed answer with probabilities and a confidence. Your code keeps the thresholds, the fallbacks, and every action.


Quick start

1. Install

bun add jev-ui        # npm install jev-ui · pnpm add jev-ui · yarn add jev-ui

2. Get an API key

Create one in the TypeSafe console. It looks like apikey-....

3. Put it in your environment

Create .env in the root of your Next.js app:

TYPESAFE_API_KEY=apikey-your-key-here

Add it to .gitignore before you paste the key in:

echo ".env" >> .gitignore

Next.js and bun both load .env on their own. There is nothing else to configure.

4. Wrap your app in the provider

jev-ui/action is a ready-made Server Action. The key is read there, on the server — the browser never sees it.

// app/providers.tsx
'use client';

import { JevProvider } from 'jev-ui';
import { askJev } from 'jev-ui/action';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <JevProvider resolve={askJev} state={{ app: { role: 'analyst' } }}>
      {children}
    </JevProvider>
  );
}
// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

5. Render your first judgment

'use client';

import { Branch, Option } from 'jev-ui';

export function Answer({ question }: { question: string }) {
  return (
    <Branch
      ask="Which view best answers the question in `data.question`?"
      data={{ question }}
      minConfidence={0.5}
    >
      <Option k="chart" when="The question is about a trend across many time periods.">
        <RevenueChart />
      </Option>
      <Option k="table" when="The question needs exact per-row numbers.">
        <RevenueTable />
      </Option>
      <Option fallback>
        <RevenueTable />
      </Option>
    </Branch>
  );
}

That is the whole setup. The when text on each <Option> is the highest-leverage string you will write: it becomes the model's criteria, and a bare key like "chart" gives it nothing to reason about.


Configuration

Everything is environment-driven, so the common case needs no code.

VariableDefaultWhat it does
TYPESAFE_API_KEYYour key. Server-side only.
JEV_TRANSPORTlive with a key, else replaylive, record, replay, mock, auto
JEV_MODELjev-latestModel id to send
JEV_FIXTURES_DIRfixturesWhere record writes and replay reads

Need something the environment cannot express — a custom transport, per-request model selection, your own pricing table? Write the action yourself:

// app/actions.ts
'use server';
import { createResolver } from 'jev-ui/server';

const resolve = createResolver({ model: 'jev-1.13.0', pricing: myRates });

export async function askJev(decisions, state) {
  return resolve(decisions, state);
}

What you get

ComponentPrimitiveWhat it does
<Branch>Choice + no-matchpicks one subtree out of several
<Rank>Choice or one Score per itemorders, filters, and truncates a candidate set
<Gate>Noulshows an affordance when a condition holds

Those three decide what renders. useScore answers how much: describe a scale in words, get a number back, and use it as an ordinary prop — show the help text or not, load 10 rows or 50, render the dense table or the roomy one.

Typeahead is <Rank mode="compete" take={4}>. Code finds the candidates; the judgment only picks among them.

State

Three layers, merged local over scope over ambient:

LayerWho sets itRead in an ask as
ambientthe libraryrecent_actions, time_spent
scopeyou, at the providerapp.* — any keys you like
localyou, at the call sitedata.*
const { state, track, setAppField } = useJevState();

track('exported CSV');              // appends to recent_actions, capped
setAppField('tier', 'enterprise');  // one of your own fields

The state prop is live, not just an initial value — pass it from Redux, a session, or a server prop and the provider syncs each key whose value changes. onStateChange fires after every change if you want to persist or mirror it.

The rules it is built around

  • The judgment picks presentation; code owns actions. Nothing here submits a form, spends money, or deletes a row.
  • Every judgment in one render pass is one request. The state is ingested once for all of them, so the marginal question is close to free — and each component reports what it cost through onResolved.
  • Telemetry becomes words. 41200 ms is projected to time_spent: "long" before the model sees it, because Jev underperforms on numeric representations and does not count reliably. Bands also keep re-resolution rare.
  • Gates only add. Content hidden by a judgment is an accessibility and SEO problem, and it is invisible to whoever needed it.

Docs and demos

Full reference and five live demos: the docs site.

Each demo shows its own source read off disk, the question it generated, the state it sent, the answer it got back, and what that render cost. /llms.txt serves the whole documentation set as plain markdown, generated from the same module the pages render.

Running the site yourself, with your own key, makes every demo call the real API:

git clone https://github.com/etweisberg/jev-ui
cd jev-ui && bun install
echo "TYPESAFE_API_KEY=apikey-..." > apps/demo/.env
bun run dev                       # http://localhost:3111

Contributing

See CONTRIBUTING.md for the repo layout, the test tiers, how fixtures are recorded, and how releases work.

MIT licensed.