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.
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_actionsandtime_spent. Everything else goes inapp, 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.