Getting started

Install, wire a Server Action, and render your first judgment.

jev-ui resolves three kinds of interface question from calibrated judgments: which component to render, how to order a list, and whether to show an affordance. A fourth, useScore, answers how much — you describe a scale in words and get back a number to use as an ordinary prop. The model returns typed answers with probabilities; your code decides what to do with them.

Install

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

Add your key

The key never reaches the browser. Put it in the environment file at the root of your app — Next.js loads it with no extra tooling, and so does bun.

# .env
TYPESAFE_API_KEY=apikey-...

Add that file to your .gitignore before you paste anything into it. With no key the library falls back to the replay transport, so a checkout without one still runs against recorded fixtures rather than crashing.

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, and because the client passes only decisions and data, it cannot be turned into a general proxy to your key.

// 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>
  );
}

Configuration

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

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

Write the action yourself when you need something the environment cannot express — a custom transport, per-request model selection, or your own pricing table:

// 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);
}

Render a 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>
  );
}

The when text is the single highest-leverage string in the component. It becomes the model's criteria, and a bare key like "chart" gives it nothing to reason about.

The rules worth knowing up front

  • The judgment picks presentation; code owns actions. Nothing here should submit a form, spend money, or delete a row.
  • The library collects two things on its own — recent_actions and time_spent. Everything else goes in app, with whatever keys you like, and a question reads it by naming the path: app.tier.
  • Every judgment registered in one render pass becomes one request, because the state is ingested once for all of them.
  • Typed output guarantees the interface, not the truth. Always give a Branch a fallback and a confidence floor.