mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 12:28:17 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d585afe9c4 |
@@ -151,6 +151,8 @@ Initialize your renderer's state from `spec.state`. Keep user interaction disabl
|
||||
|
||||
On `complete`, inspect `stopReason`: `finish` means composition finished; `unavailable` means the evaluator could not fulfill the request; `limit` means a call, element, or depth budget prevented completion. A complete event can contain a partial spec, or `null` when no root was added. Completion is not a correctness guarantee. Errors and cancellation throw; retain the last snapshot and label it incomplete. Each batched trace is one evaluation (`select` or `layout`), with the individual choices in `step.answers` and usage/timing counted once.
|
||||
|
||||
To avoid visible rearrangement, the playground waits for a successful `complete` event before revealing a new tree in one render. Decision metadata still streams while composing. Follow-up edits continue to update the existing preview incrementally. Apps can choose the same display policy while retaining the core API's provisional snapshots for inspection.
|
||||
|
||||
The playground is a reference implementation: [candidates and server wrapper](https://github.com/vercel-labs/json-render/tree/main/apps/web/lib/jev), [streaming route](https://github.com/vercel-labs/json-render/blob/main/apps/web/app/api/generate/route.ts), and [client](https://github.com/vercel-labs/json-render/blob/main/apps/web/components/playground.tsx).
|
||||
|
||||
## Validation and v1 limits
|
||||
|
||||
@@ -21,10 +21,10 @@ Select Jev, choose Create account settings, and send the request. Edit the name,
|
||||
Jev exposes Choice, Boolean, and Score outputs. It does not produce free-form JSON or prose. We express new UI construction as two batches of finite choices:
|
||||
|
||||
1. Offer the root and independent component membership questions in one evaluation. Exclusive resource variants share a question; reusable recipes get bounded counts. Candidate values include state/action bindings owned by the app.
|
||||
2. Assemble and validate the selected content, then stream a preview immediately. This preview uses catalog order and the root's default slot. Root selection takes precedence over speculative membership for the same recipe/resource.
|
||||
3. Ask final parent slots and sibling positions in a second evaluation against the actual selected set. Validate the combined tree, including depth and cycles, before streaming it. Equal positions retain catalog order. A single root or one child in a single slot needs no second call. No separate finish call is needed.
|
||||
2. Assemble and validate the selected content. The core API emits a provisional snapshot in catalog order and the root's default slot; the playground streams its decision metadata but waits to display new content. Root selection takes precedence over speculative membership for the same recipe/resource.
|
||||
3. Ask final parent slots and sibling positions in a second evaluation against the actual selected set. Validate the combined tree, including depth and cycles, before revealing the finished spec in one JSON edit message. Equal positions retain catalog order. A single root or one child in a single slot needs no second call. No separate finish call is needed.
|
||||
4. On follow-ups, use the selected spec with the sequential edit protocol: add, replace, remove, or move/reorder. Replacements and moves select a target, then choose a valid recipe or destination in a second evaluation. Preserve unchanged elements and earlier versions.
|
||||
5. Each trace represents one evaluation. Batched traces use `select`/`layout` with the independent decisions in `answers`; timing and usage are counted once per call. Provider errors or invalid combined layouts preserve the last valid preview and report failure.
|
||||
5. Each trace represents one evaluation. Batched traces use `select`/`layout` with the independent decisions in `answers`; timing and usage are counted once per call. Provider errors or invalid combined layouts report failure. New trees remain hidden until successful completion; follow-up edits retain their last valid preview.
|
||||
|
||||
There are **no complete UI templates** and no generative-model calls. The example prompt buttons only populate the request text. Jev chooses which elements to include, their order, grouping, and which offered action bindings to use. The registry owns appearance and behavior.
|
||||
|
||||
@@ -48,7 +48,7 @@ The composer validates tree structure and candidate values; it does not guarante
|
||||
|
||||
Name required sections explicitly. For example, request an orders table at the top, revenue/orders/customer metrics in a row, then a weekly revenue chart. The shorter request "a dashboard with the table at the top" can select only a table. Follow-up requests can move an existing table without reconstructing its data.
|
||||
|
||||
The code bounds new batches to 14 elements, each request to 14 evaluation calls, nesting depth four, ten seconds per provider request, and 55 seconds overall. The selected seed may contain up to 100 elements. A limit, cancellation, or error retains the current preview and labels it partial. The shared endpoint uses the web app's request rate limiters. Both models edit the selected version; Clear starts fresh. The stream tab exposes construction decisions alongside spec patches. Provider calls and spec assembly never execute the selected UI actions.
|
||||
The code bounds new batches to 14 elements, each request to 14 evaluation calls, nesting depth four, ten seconds per provider request, and 55 seconds overall. The selected seed may contain up to 100 elements. A limit, cancellation, or error retains the current preview for edits and labels the result partial; new trees remain hidden. The shared endpoint uses the web app's request rate limiters. Both models edit the selected version; Clear starts fresh. The stream tab exposes construction decisions alongside spec patches. Provider calls and spec assembly never execute the selected UI actions.
|
||||
|
||||
Try `Design a user profile card`, then `Remove the bio` or `Make the avatar smaller`. For settings, try `Remove the email notifications switch`, `Change the heading to "Account settings"`, or `Move the email field above the name field`. The server shares existing display labels and matching candidate descriptions to identify edit targets, without sharing raw state or entered field values. Existing specs must use the supported expression subset and form a valid tree. Edits retain state from the selected spec, as in the default model flow; interactive preview state is not saved into version history.
|
||||
|
||||
@@ -64,7 +64,7 @@ The server uses Gateway's experimental v4 evaluation transport with model `types
|
||||
- `packages/core/src/experimental-evaluator.ts`: public Gateway evaluator adapter.
|
||||
- `compose.ts`: public API consumer with playground instructions and cost display.
|
||||
- `../../app/api/generate/route.ts`: shared rate-limited endpoint, dispatching the selected model.
|
||||
- `response.ts`: adapts composition snapshots into the playground's JSONL spec patches and decision metadata.
|
||||
- `response.ts`: reveals finished new trees atomically using the JSON edit protocol, streams follow-up spec patches, and preserves decision metadata.
|
||||
- `../../components/playground.tsx`: shared model toggle, experimental info tooltip, prompt, version history, live preview, and inspectors.
|
||||
- `compose.test.ts`: structure, action boundaries, unknown usage, cancellation, and limits.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment node
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { type JsonPatch, type Spec } from "@json-render/core";
|
||||
import { deepMergeSpec, type Spec } from "@json-render/core";
|
||||
import { applySpecPatch } from "../spec-patch";
|
||||
import { createCompositionResponse } from "./response";
|
||||
import { composeUI } from "./compose";
|
||||
@@ -58,7 +58,7 @@ describe("playground composition response", () => {
|
||||
expect(initialSpec.elements.card!.props.title).toBe("Before");
|
||||
});
|
||||
|
||||
it("adapts snapshots to the existing patch stream, including the final decision", async () => {
|
||||
it("reveals a new tree atomically after layout, preserving every decision", async () => {
|
||||
vi.stubEnv("JEV_AI_GATEWAY_API_KEY", "test");
|
||||
const spec: Spec = {
|
||||
root: "card",
|
||||
@@ -76,12 +76,31 @@ describe("playground composition response", () => {
|
||||
elapsedMs: 10,
|
||||
inputTokens: null,
|
||||
};
|
||||
const provisional: Spec = {
|
||||
...spec,
|
||||
elements: {
|
||||
card: { ...spec.elements.card!, props: { title: "Provisional" } },
|
||||
},
|
||||
};
|
||||
vi.mocked(composeUI).mockImplementation(async function* () {
|
||||
yield { type: "step", spec, step };
|
||||
yield {
|
||||
type: "step",
|
||||
spec: provisional,
|
||||
step: { ...step, choice: "select" },
|
||||
};
|
||||
yield {
|
||||
type: "step",
|
||||
spec,
|
||||
step: { ...step, index: 1, choice: "layout" },
|
||||
};
|
||||
yield {
|
||||
type: "complete",
|
||||
spec,
|
||||
steps: [step, { ...step, index: 1, choice: "finish" }],
|
||||
steps: [
|
||||
{ ...step, choice: "select" },
|
||||
{ ...step, index: 1, choice: "layout" },
|
||||
{ ...step, index: 2, choice: "finish" },
|
||||
],
|
||||
stopReason: "finish",
|
||||
elapsedMs: 20,
|
||||
inputTokens: null,
|
||||
@@ -96,23 +115,59 @@ describe("playground composition response", () => {
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
let actual: Spec = { root: "", elements: {} };
|
||||
for (const line of lines)
|
||||
if (line.op) actual = applySpecPatch(actual, line as JsonPatch);
|
||||
expect(actual).toEqual(spec);
|
||||
expect(lines.slice(0, 2).map((line) => line.__meta)).toEqual([
|
||||
"decision",
|
||||
"decision",
|
||||
]);
|
||||
const updates = lines.filter((line) => line.op || line.__json_edit);
|
||||
expect(updates).toEqual([{ __json_edit: true, ...spec }]);
|
||||
const { __json_edit, ...snapshot } = updates[0];
|
||||
expect(deepMergeSpec({ root: "", elements: {} }, snapshot)).toEqual(spec);
|
||||
expect(
|
||||
lines
|
||||
.filter((line) => line.__meta === "decision")
|
||||
.map((line) => line.choice),
|
||||
).toEqual(["card", "finish"]);
|
||||
).toEqual(["select", "layout", "finish"]);
|
||||
expect(lines.at(-1)).toMatchObject({
|
||||
__meta: "composition",
|
||||
stopReason: "finish",
|
||||
calls: 2,
|
||||
calls: 3,
|
||||
inputTokens: null,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["limit", "unavailable"] as const)(
|
||||
"does not reveal provisional content when creation ends with %s",
|
||||
async (stopReason) => {
|
||||
vi.stubEnv("JEV_AI_GATEWAY_API_KEY", "test");
|
||||
vi.mocked(composeUI).mockImplementation(async function* () {
|
||||
yield {
|
||||
type: "complete",
|
||||
spec: {
|
||||
root: "card",
|
||||
elements: { card: { type: "Card", props: {} } },
|
||||
},
|
||||
steps: [],
|
||||
stopReason,
|
||||
elapsedMs: 1,
|
||||
inputTokens: null,
|
||||
estimatedCostUsd: null,
|
||||
};
|
||||
});
|
||||
const response = createCompositionResponse(
|
||||
new Request("https://example.com/api/generate"),
|
||||
"Create a card",
|
||||
);
|
||||
const lines = (await response.text())
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
expect(lines).toEqual([
|
||||
expect.objectContaining({ __meta: "composition", stopReason }),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains unavailable outcomes and sends failures in the shared protocol", async () => {
|
||||
vi.stubEnv("JEV_AI_GATEWAY_API_KEY", "test");
|
||||
vi.mocked(composeUI).mockImplementation(async function* () {
|
||||
|
||||
@@ -83,11 +83,20 @@ export function createCompositionResponse(
|
||||
)) {
|
||||
if (event.type === "error") throw new Error(event.message);
|
||||
if (event.type === "step") {
|
||||
sendSpec(event.spec);
|
||||
// New trees have a provisional catalog order until layout finishes.
|
||||
// Stream its decision metadata without displaying a moving UI.
|
||||
if (initialSpec) sendSpec(event.spec);
|
||||
send({ __meta: "decision", ...event.step });
|
||||
decisions++;
|
||||
} else {
|
||||
if (event.spec) sendSpec(event.spec);
|
||||
if (event.spec) {
|
||||
if (initialSpec) sendSpec(event.spec);
|
||||
else if (event.stopReason === "finish") {
|
||||
// The existing JSON edit protocol applies a full new tree in
|
||||
// one render, including its state and final child order.
|
||||
send({ __json_edit: true, ...event.spec });
|
||||
}
|
||||
}
|
||||
for (const step of event.steps.slice(decisions))
|
||||
send({ __meta: "decision", ...step });
|
||||
send({
|
||||
|
||||
Reference in New Issue
Block a user