mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 04:18:15 +08:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22545b49cc | ||
|
|
a8afd8bfe2 | ||
|
|
dc489601be | ||
|
|
7758d4bbd5 |
@@ -21,7 +21,8 @@
|
|||||||
"@json-render/mcp",
|
"@json-render/mcp",
|
||||||
"@json-render/svelte",
|
"@json-render/svelte",
|
||||||
"@json-render/solid",
|
"@json-render/solid",
|
||||||
"@json-render/react-three-fiber"
|
"@json-render/react-three-fiber",
|
||||||
|
"@json-render/yaml"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"linked": [],
|
"linked": [],
|
||||||
|
|||||||
+34
-25
@@ -13,34 +13,43 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
lint:
|
||||||
name: Lint, Type Check & Build
|
name: Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
- uses: pnpm/action-setup@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: "pnpm"
|
cache: pnpm
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
- run: pnpm lint
|
||||||
|
|
||||||
- name: Install dependencies
|
test:
|
||||||
run: pnpm install --frozen-lockfile
|
name: Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: pnpm
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
- name: Build packages
|
||||||
|
run: pnpm turbo run build --filter='./packages/*'
|
||||||
|
- run: pnpm test
|
||||||
|
|
||||||
- name: Lint
|
typecheck:
|
||||||
run: pnpm lint
|
name: Type Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
- name: Type check
|
steps:
|
||||||
run: pnpm type-check
|
- uses: actions/checkout@v4
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Test
|
- uses: actions/setup-node@v4
|
||||||
run: pnpm test
|
with:
|
||||||
|
node-version: 20
|
||||||
- name: Build
|
cache: pnpm
|
||||||
run: pnpm build
|
- run: pnpm install --frozen-lockfile
|
||||||
|
- run: pnpm type-check
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ function Dashboard({ spec }) {
|
|||||||
| `@json-render/jotai` | Jotai adapter for `StateStore` |
|
| `@json-render/jotai` | Jotai adapter for `StateStore` |
|
||||||
| `@json-render/xstate` | XState Store (atom) adapter for `StateStore` |
|
| `@json-render/xstate` | XState Store (atom) adapter for `StateStore` |
|
||||||
| `@json-render/mcp` | MCP Apps integration for Claude, ChatGPT, Cursor, VS Code |
|
| `@json-render/mcp` | MCP Apps integration for Claude, ChatGPT, Cursor, VS Code |
|
||||||
|
| `@json-render/yaml` | YAML wire format with streaming parser, edit modes, AI SDK transform |
|
||||||
|
|
||||||
## Renderers
|
## Renderers
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
# web
|
# web
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/yaml@0.14.0
|
||||||
|
- @json-render/codegen@0.14.0
|
||||||
|
- @json-render/react@0.14.0
|
||||||
|
|
||||||
## 0.1.7
|
## 0.1.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ interface PromptOptions {
|
|||||||
system?: string; // Custom system message intro
|
system?: string; // Custom system message intro
|
||||||
customRules?: string[]; // Additional rules to append
|
customRules?: string[]; // Additional rules to append
|
||||||
mode?: "standalone" | "inline" | "generate" | "chat"; // Output mode (default: "standalone")
|
mode?: "standalone" | "inline" | "generate" | "chat"; // Output mode (default: "standalone")
|
||||||
|
editModes?: EditMode[]; // Edit modes to document in prompt (default: ["patch"])
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpecValidationResult<T> {
|
interface SpecValidationResult<T> {
|
||||||
@@ -480,9 +481,10 @@ function buildUserPrompt(options: UserPromptOptions): string
|
|||||||
|
|
||||||
interface UserPromptOptions {
|
interface UserPromptOptions {
|
||||||
prompt: string; // The user's text prompt
|
prompt: string; // The user's text prompt
|
||||||
currentSpec?: Spec | null; // Existing spec to refine (triggers patch-only mode)
|
currentSpec?: Spec | null; // Existing spec to refine (triggers edit mode)
|
||||||
state?: Record<string, unknown> | null; // Runtime state context to include
|
state?: Record<string, unknown> | null; // Runtime state context to include
|
||||||
maxPromptLength?: number; // Max length for user text (truncates before wrapping)
|
maxPromptLength?: number; // Max length for user text (truncates before wrapping)
|
||||||
|
editModes?: EditMode[]; // Edit modes for refinement (default: ["patch"])
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -492,14 +494,15 @@ interface UserPromptOptions {
|
|||||||
const userPrompt = buildUserPrompt({ prompt: "create a todo app" });
|
const userPrompt = buildUserPrompt({ prompt: "create a todo app" });
|
||||||
```
|
```
|
||||||
|
|
||||||
### Refinement (patch-only mode)
|
### Refinement (edit modes)
|
||||||
|
|
||||||
When `currentSpec` is provided, the prompt instructs the AI to output only the patches needed for the change, not recreate the entire spec:
|
When `currentSpec` is provided, the prompt instructs the AI to use the specified edit modes instead of recreating the entire spec. Available modes: `"patch"` (RFC 6902), `"merge"` (RFC 7396), and `"diff"` (unified diff).
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const userPrompt = buildUserPrompt({
|
const userPrompt = buildUserPrompt({
|
||||||
prompt: "add a dark mode toggle",
|
prompt: "add a dark mode toggle",
|
||||||
currentSpec: existingSpec,
|
currentSpec: existingSpec,
|
||||||
|
editModes: ["patch", "merge"],
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -514,6 +517,93 @@ const userPrompt = buildUserPrompt({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Edit Modes
|
||||||
|
|
||||||
|
Universal edit mode utilities for modifying existing specs. Used by `buildUserPrompt` internally and available for direct use.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
buildEditInstructions,
|
||||||
|
buildEditUserPrompt,
|
||||||
|
isNonEmptySpec,
|
||||||
|
type EditMode,
|
||||||
|
type EditConfig,
|
||||||
|
} from '@json-render/core';
|
||||||
|
|
||||||
|
type EditMode = "patch" | "merge" | "diff";
|
||||||
|
```
|
||||||
|
|
||||||
|
### buildEditInstructions
|
||||||
|
|
||||||
|
Generate the prompt section describing available edit modes. Supports both JSON and YAML formats.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function buildEditInstructions(config: EditConfig, format: "json" | "yaml"): string
|
||||||
|
|
||||||
|
const instructions = buildEditInstructions({ modes: ["patch", "merge"] }, "json");
|
||||||
|
```
|
||||||
|
|
||||||
|
### buildEditUserPrompt
|
||||||
|
|
||||||
|
Build a user prompt for editing an existing spec. Includes the current spec (with line numbers when diff mode is enabled) and mode-specific instructions.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function buildEditUserPrompt(options: BuildEditUserPromptOptions): string
|
||||||
|
|
||||||
|
interface BuildEditUserPromptOptions {
|
||||||
|
prompt: string;
|
||||||
|
currentSpec?: Spec | null;
|
||||||
|
config?: EditConfig;
|
||||||
|
format: "json" | "yaml";
|
||||||
|
maxPromptLength?: number;
|
||||||
|
serializer?: (spec: Spec) => string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### isNonEmptySpec
|
||||||
|
|
||||||
|
Check whether a value is a non-empty spec (has a root string and at least one element).
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function isNonEmptySpec(spec: unknown): spec is Spec
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deep Merge and Diff
|
||||||
|
|
||||||
|
Format-agnostic utilities for merging and diffing spec objects.
|
||||||
|
|
||||||
|
### deepMergeSpec
|
||||||
|
|
||||||
|
Deep-merge with RFC 7396 semantics: `null` deletes, arrays replace, objects recurse. Neither input is mutated.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { deepMergeSpec } from '@json-render/core';
|
||||||
|
|
||||||
|
function deepMergeSpec(
|
||||||
|
base: Record<string, unknown>,
|
||||||
|
patch: Record<string, unknown>
|
||||||
|
): Record<string, unknown>
|
||||||
|
|
||||||
|
const merged = deepMergeSpec(currentSpec, { elements: { main: { props: { title: "New" } } } });
|
||||||
|
```
|
||||||
|
|
||||||
|
### diffToPatches
|
||||||
|
|
||||||
|
Generate RFC 6902 JSON Patch operations that transform one object into another. Arrays are compared shallowly and replaced atomically; plain objects recurse.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { diffToPatches } from '@json-render/core';
|
||||||
|
|
||||||
|
function diffToPatches(
|
||||||
|
oldObj: Record<string, unknown>,
|
||||||
|
newObj: Record<string, unknown>,
|
||||||
|
basePath?: string
|
||||||
|
): JsonPatch[]
|
||||||
|
|
||||||
|
const patches = diffToPatches(oldSpec, newSpec);
|
||||||
|
// [{ op: "replace", path: "/elements/main/props/title", value: "New Title" }]
|
||||||
|
```
|
||||||
|
|
||||||
## evaluateVisibility
|
## evaluateVisibility
|
||||||
|
|
||||||
Evaluates a visibility condition against the state model.
|
Evaluates a visibility condition against the state model.
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/yaml")
|
||||||
|
|
||||||
|
# @json-render/yaml
|
||||||
|
|
||||||
|
YAML wire format for json-render. Progressive rendering and surgical edits via streaming YAML.
|
||||||
|
|
||||||
|
## Prompt Generation
|
||||||
|
|
||||||
|
### yamlPrompt
|
||||||
|
|
||||||
|
Generate a YAML-format system prompt from any json-render catalog. Works with catalogs from any renderer.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function yamlPrompt(
|
||||||
|
catalog: Catalog,
|
||||||
|
options?: YamlPromptOptions
|
||||||
|
): string
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { yamlPrompt } from "@json-render/yaml";
|
||||||
|
|
||||||
|
const systemPrompt = yamlPrompt(catalog, {
|
||||||
|
mode: "standalone",
|
||||||
|
customRules: ["Always use dark theme"],
|
||||||
|
editModes: ["merge"],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### YamlPromptOptions
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Option</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Default</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>system</code></td>
|
||||||
|
<td><code>string</code></td>
|
||||||
|
<td><code>{'\"You are a UI generator that outputs YAML.\"'}</code></td>
|
||||||
|
<td>Custom system message intro</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>mode</code></td>
|
||||||
|
<td><code>{'\"standalone\" | \"inline\"'}</code></td>
|
||||||
|
<td><code>{'\"standalone\"'}</code></td>
|
||||||
|
<td>Standalone outputs only YAML; inline allows conversational responses with embedded YAML fences</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>customRules</code></td>
|
||||||
|
<td><code>{'string[]'}</code></td>
|
||||||
|
<td><code>{'[]'}</code></td>
|
||||||
|
<td>Additional rules appended to the prompt</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>editModes</code></td>
|
||||||
|
<td><code>{'EditMode[]'}</code></td>
|
||||||
|
<td><code>{'[\"merge\"]'}</code></td>
|
||||||
|
<td>Edit modes to document in the prompt (patch, merge, diff)</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## AI SDK Transform
|
||||||
|
|
||||||
|
### createYamlTransform
|
||||||
|
|
||||||
|
Creates a `TransformStream` that intercepts AI SDK stream chunks and converts YAML spec/edit blocks into json-render patch data parts.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function createYamlTransform(
|
||||||
|
options?: YamlTransformOptions
|
||||||
|
): TransformStream<StreamChunk, StreamChunk>
|
||||||
|
```
|
||||||
|
|
||||||
|
Recognized fence types:
|
||||||
|
|
||||||
|
- <code>{'```yaml-spec'}</code> -- Full YAML spec, parsed progressively
|
||||||
|
- <code>{'```yaml-edit'}</code> -- Partial YAML, deep-merged with current spec
|
||||||
|
- <code>{'```yaml-patch'}</code> -- RFC 6902 JSON Patch lines
|
||||||
|
- <code>{'```diff'}</code> -- Unified diff against serialized spec
|
||||||
|
|
||||||
|
### pipeYamlRender
|
||||||
|
|
||||||
|
Convenience wrapper that pipes an AI SDK stream through the YAML transform. Drop-in replacement for `pipeJsonRender` from `@json-render/core`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function pipeYamlRender<T>(
|
||||||
|
stream: ReadableStream<T>,
|
||||||
|
options?: YamlTransformOptions
|
||||||
|
): ReadableStream<T>
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { pipeYamlRender } from "@json-render/yaml";
|
||||||
|
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
|
||||||
|
|
||||||
|
const stream = createUIMessageStream({
|
||||||
|
execute: async ({ writer }) => {
|
||||||
|
writer.merge(pipeYamlRender(result.toUIMessageStream()));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return createUIMessageStreamResponse({ stream });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Streaming Parser
|
||||||
|
|
||||||
|
### createYamlStreamCompiler
|
||||||
|
|
||||||
|
Create a streaming YAML compiler that incrementally parses YAML text and emits JSON Patch operations by diffing each successful parse against the previous snapshot.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function createYamlStreamCompiler<T>(
|
||||||
|
initial?: Partial<T>
|
||||||
|
): YamlStreamCompiler<T>
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createYamlStreamCompiler } from "@json-render/yaml";
|
||||||
|
|
||||||
|
const compiler = createYamlStreamCompiler<Spec>();
|
||||||
|
|
||||||
|
compiler.push("root: main\n");
|
||||||
|
compiler.push("elements:\n main:\n type: Card\n");
|
||||||
|
|
||||||
|
const { result, newPatches } = compiler.flush();
|
||||||
|
```
|
||||||
|
|
||||||
|
### YamlStreamCompiler
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Method</th>
|
||||||
|
<th>Returns</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>push(chunk)</code></td>
|
||||||
|
<td><code>{'{ result: T; newPatches: JsonPatch[] }'}</code></td>
|
||||||
|
<td>Push a chunk of text, returns current result and new patches</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>flush()</code></td>
|
||||||
|
<td><code>{'{ result: T; newPatches: JsonPatch[] }'}</code></td>
|
||||||
|
<td>Flush remaining buffer, return final result</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>getResult()</code></td>
|
||||||
|
<td><code>T</code></td>
|
||||||
|
<td>Get the current compiled result</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>getPatches()</code></td>
|
||||||
|
<td><code>{'JsonPatch[]'}</code></td>
|
||||||
|
<td>Get all patches applied so far</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>reset(initial?)</code></td>
|
||||||
|
<td><code>void</code></td>
|
||||||
|
<td>Reset to initial state</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## Fence Constants
|
||||||
|
|
||||||
|
Exported string constants for fence detection in custom parsers:
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Constant</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>YAML_SPEC_FENCE</code></td>
|
||||||
|
<td><code>{'\"```yaml-spec\"'}</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>YAML_EDIT_FENCE</code></td>
|
||||||
|
<td><code>{'\"```yaml-edit\"'}</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>YAML_PATCH_FENCE</code></td>
|
||||||
|
<td><code>{'\"```yaml-patch\"'}</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>DIFF_FENCE</code></td>
|
||||||
|
<td><code>{'\"```diff\"'}</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>FENCE_CLOSE</code></td>
|
||||||
|
<td><code>{'\"```\"'}</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## Re-exports from @json-render/core
|
||||||
|
|
||||||
|
### diffToPatches
|
||||||
|
|
||||||
|
Generate RFC 6902 JSON Patch operations that transform one object into another.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function diffToPatches(
|
||||||
|
oldObj: Record<string, unknown>,
|
||||||
|
newObj: Record<string, unknown>,
|
||||||
|
basePath?: string
|
||||||
|
): JsonPatch[]
|
||||||
|
```
|
||||||
|
|
||||||
|
### deepMergeSpec
|
||||||
|
|
||||||
|
Deep-merge with RFC 7396 semantics: `null` deletes, arrays replace, objects recurse.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function deepMergeSpec(
|
||||||
|
base: Record<string, unknown>,
|
||||||
|
patch: Record<string, unknown>
|
||||||
|
): Record<string, unknown>
|
||||||
|
```
|
||||||
@@ -16,8 +16,8 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render
|
|||||||
|
|
||||||
GitHub repository: https://github.com/vercel-labs/json-render
|
GitHub repository: https://github.com/vercel-labs/json-render
|
||||||
Documentation: https://json-render.dev/docs
|
Documentation: https://json-render.dev/docs
|
||||||
npm packages: @json-render/core, @json-render/react, @json-render/vue, @json-render/svelte, @json-render/solid, @json-render/shadcn, @json-render/react-three-fiber, @json-render/react-native, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen, @json-render/mcp, @json-render/redux, @json-render/zustand, @json-render/jotai, @json-render/xstate
|
npm packages: @json-render/core, @json-render/react, @json-render/vue, @json-render/svelte, @json-render/solid, @json-render/shadcn, @json-render/react-three-fiber, @json-render/react-native, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen, @json-render/mcp, @json-render/redux, @json-render/zustand, @json-render/jotai, @json-render/xstate, @json-render/yaml
|
||||||
Skills: json-render ships AI agent skills that teach coding agents how to use each package. Install with "npx skills add vercel-labs/json-render --skill <name>". Available skills: core, react, react-pdf, react-email, react-native, shadcn, react-three-fiber, image, remotion, vue, svelte, solid, codegen, mcp, redux, zustand, jotai, xstate. See /docs/skills for details.
|
Skills: json-render ships AI agent skills that teach coding agents how to use each package. Install with "npx skills add vercel-labs/json-render --skill <name>". Available skills: core, react, react-pdf, react-email, react-native, shadcn, react-three-fiber, image, remotion, vue, svelte, solid, codegen, mcp, redux, zustand, jotai, xstate, yaml. See /docs/skills for details.
|
||||||
|
|
||||||
You have access to the full json-render documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/docs/ directory.
|
You have access to the full json-render documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/docs/ directory.
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,73 @@
|
|||||||
import { streamText } from "ai";
|
import { streamText } from "ai";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { buildUserPrompt } from "@json-render/core";
|
import type { Spec, EditMode } from "@json-render/core";
|
||||||
|
import {
|
||||||
|
buildUserPrompt,
|
||||||
|
buildEditUserPrompt,
|
||||||
|
isNonEmptySpec,
|
||||||
|
} from "@json-render/core";
|
||||||
|
import { yamlPrompt } from "@json-render/yaml";
|
||||||
|
import { stringify as yamlStringify } from "yaml";
|
||||||
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
|
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
|
||||||
import { playgroundCatalog } from "@/lib/render/catalog";
|
import { playgroundCatalog } from "@/lib/render/catalog";
|
||||||
|
|
||||||
export const maxDuration = 30;
|
export const maxDuration = 30;
|
||||||
|
|
||||||
const SYSTEM_PROMPT = playgroundCatalog.prompt({
|
const PLAYGROUND_RULES = [
|
||||||
customRules: [
|
"NEVER use viewport height classes (min-h-screen, h-screen) - the UI renders inside a fixed-size container.",
|
||||||
"NEVER use viewport height classes (min-h-screen, h-screen) - the UI renders inside a fixed-size container.",
|
"NEVER use page background colors (bg-gray-50) - the container has its own background.",
|
||||||
"NEVER use page background colors (bg-gray-50) - the container has its own background.",
|
"For forms or small UIs: use Card as root with maxWidth:'sm' or 'md' and centered:true.",
|
||||||
"For forms or small UIs: use Card as root with maxWidth:'sm' or 'md' and centered:true.",
|
"For content-heavy UIs (blogs, dashboards, product listings): use Stack or Grid as root. Use Grid with 2-3 columns for card layouts.",
|
||||||
"For content-heavy UIs (blogs, dashboards, product listings): use Stack or Grid as root. Use Grid with 2-3 columns for card layouts.",
|
"Wrap each repeated item in a Card for visual separation and structure.",
|
||||||
"Wrap each repeated item in a Card for visual separation and structure.",
|
"Use realistic, professional sample data. Include 3-5 items with varied content. Never leave state arrays empty.",
|
||||||
"Use realistic, professional sample data. Include 3-5 items with varied content. Never leave state arrays empty.",
|
'For form inputs (Input, Textarea, Select), always include checks for validation (e.g. required, email, minLength). Always pair checks with a $bindState expression on the value prop (e.g. { "$bindState": "/path" }).',
|
||||||
'For form inputs (Input, Textarea, Select), always include checks for validation (e.g. required, email, minLength). Always pair checks with a $bindState expression on the value prop (e.g. { "$bindState": "/path" }).',
|
];
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const MAX_PROMPT_LENGTH = 500;
|
const MAX_PROMPT_LENGTH = 500;
|
||||||
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
|
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
|
||||||
|
|
||||||
|
function getSystemPrompt(isYaml: boolean, editModes?: EditMode[]): string {
|
||||||
|
if (isYaml) {
|
||||||
|
return yamlPrompt(playgroundCatalog, {
|
||||||
|
mode: "standalone",
|
||||||
|
customRules: PLAYGROUND_RULES,
|
||||||
|
editModes: editModes ?? ["merge"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return playgroundCatalog.prompt({
|
||||||
|
customRules: PLAYGROUND_RULES,
|
||||||
|
editModes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildYamlUserPrompt(
|
||||||
|
prompt: string,
|
||||||
|
previousSpec?: Spec | null,
|
||||||
|
editModes?: EditMode[],
|
||||||
|
): string {
|
||||||
|
if (isNonEmptySpec(previousSpec)) {
|
||||||
|
return buildEditUserPrompt({
|
||||||
|
prompt,
|
||||||
|
currentSpec: previousSpec,
|
||||||
|
config: { modes: editModes ?? ["merge"] },
|
||||||
|
format: "yaml",
|
||||||
|
maxPromptLength: MAX_PROMPT_LENGTH,
|
||||||
|
serializer: (s) => yamlStringify(s, { indent: 2 }).trimEnd(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userText = prompt.slice(0, MAX_PROMPT_LENGTH);
|
||||||
|
return [
|
||||||
|
userText,
|
||||||
|
"",
|
||||||
|
"Output the full spec in a ```yaml-spec fence. Stream progressively — output elements one at a time.",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
// Get client IP for rate limiting
|
|
||||||
const headersList = await headers();
|
const headersList = await headers();
|
||||||
const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
|
const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
|
||||||
|
|
||||||
// Check rate limits (minute and daily)
|
|
||||||
const [minuteResult, dailyResult] = await Promise.all([
|
const [minuteResult, dailyResult] = await Promise.all([
|
||||||
minuteRateLimit.limit(ip),
|
minuteRateLimit.limit(ip),
|
||||||
dailyRateLimit.limit(ip),
|
dailyRateLimit.limit(ip),
|
||||||
@@ -48,22 +89,34 @@ export async function POST(req: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { prompt, context } = await req.json();
|
const { prompt, context, format, editModes } = await req.json();
|
||||||
|
const isYaml = format === "yaml";
|
||||||
|
|
||||||
const userPrompt = buildUserPrompt({
|
const systemPrompt = getSystemPrompt(isYaml, editModes);
|
||||||
prompt,
|
const userPrompt = isYaml
|
||||||
currentSpec: context?.previousSpec,
|
? buildYamlUserPrompt(prompt, context?.previousSpec, editModes)
|
||||||
maxPromptLength: MAX_PROMPT_LENGTH,
|
: buildUserPrompt({
|
||||||
});
|
prompt,
|
||||||
|
currentSpec: context?.previousSpec,
|
||||||
|
maxPromptLength: MAX_PROMPT_LENGTH,
|
||||||
|
editModes,
|
||||||
|
});
|
||||||
|
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model: process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL,
|
model: process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL,
|
||||||
system: SYSTEM_PROMPT,
|
system: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: systemPrompt,
|
||||||
|
providerOptions: {
|
||||||
|
anthropic: { cacheControl: { type: "ephemeral" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
prompt: userPrompt,
|
prompt: userPrompt,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stream the text, then append token usage metadata at the end
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const textStream = result.textStream;
|
const textStream = result.textStream;
|
||||||
|
|
||||||
@@ -72,7 +125,6 @@ export async function POST(req: Request) {
|
|||||||
for await (const chunk of textStream) {
|
for await (const chunk of textStream) {
|
||||||
controller.enqueue(encoder.encode(chunk));
|
controller.enqueue(encoder.encode(chunk));
|
||||||
}
|
}
|
||||||
// Append usage metadata after stream completes
|
|
||||||
try {
|
try {
|
||||||
const usage = await result.usage;
|
const usage = await result.usage;
|
||||||
const meta = JSON.stringify({
|
const meta = JSON.stringify({
|
||||||
@@ -80,10 +132,12 @@ export async function POST(req: Request) {
|
|||||||
promptTokens: usage.inputTokens,
|
promptTokens: usage.inputTokens,
|
||||||
completionTokens: usage.outputTokens,
|
completionTokens: usage.outputTokens,
|
||||||
totalTokens: usage.totalTokens,
|
totalTokens: usage.totalTokens,
|
||||||
|
cachedTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0,
|
||||||
|
cacheWriteTokens: usage.inputTokenDetails?.cacheWriteTokens ?? 0,
|
||||||
});
|
});
|
||||||
controller.enqueue(encoder.encode(`\n${meta}\n`));
|
controller.enqueue(encoder.encode(`\n${meta}\n`));
|
||||||
} catch {
|
} catch {
|
||||||
// Usage not available — skip silently
|
// Usage not available
|
||||||
}
|
}
|
||||||
controller.close();
|
controller.close();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ function getHighlighter() {
|
|||||||
if (!highlighterPromise) {
|
if (!highlighterPromise) {
|
||||||
highlighterPromise = createHighlighter({
|
highlighterPromise = createHighlighter({
|
||||||
themes: [vercelLightTheme, vercelDarkTheme],
|
themes: [vercelLightTheme, vercelDarkTheme],
|
||||||
langs: ["json", "tsx", "typescript"],
|
langs: ["json", "tsx", "typescript", "yaml"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return highlighterPromise;
|
return highlighterPromise;
|
||||||
@@ -158,7 +158,7 @@ if (typeof window !== "undefined") {
|
|||||||
|
|
||||||
interface CodeBlockProps {
|
interface CodeBlockProps {
|
||||||
code: string;
|
code: string;
|
||||||
lang: "json" | "tsx" | "typescript";
|
lang: "json" | "tsx" | "typescript" | "yaml";
|
||||||
fillHeight?: boolean;
|
fillHeight?: boolean;
|
||||||
hideCopyButton?: boolean;
|
hideCopyButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,16 @@
|
|||||||
|
|
||||||
import { useEffect, useState, useCallback, useRef, useMemo } from "react";
|
import { useEffect, useState, useCallback, useRef, useMemo } from "react";
|
||||||
import { flushSync } from "react-dom";
|
import { flushSync } from "react-dom";
|
||||||
import { useUIStream, type TokenUsage } from "@json-render/react";
|
|
||||||
import type { Spec } from "@json-render/core";
|
import type { Spec } from "@json-render/core";
|
||||||
import { collectUsedComponents, serializeProps } from "@json-render/codegen";
|
import { collectUsedComponents, serializeProps } from "@json-render/codegen";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { stringify as yamlStringify } from "yaml";
|
||||||
|
import type { EditMode } from "@json-render/core";
|
||||||
|
import {
|
||||||
|
usePlaygroundStream,
|
||||||
|
type StreamFormat,
|
||||||
|
type TokenUsage,
|
||||||
|
} from "@/lib/use-playground-stream";
|
||||||
import {
|
import {
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
@@ -22,10 +28,10 @@ import { PlaygroundRenderer } from "@/lib/render/renderer";
|
|||||||
import { playgroundCatalog } from "@/lib/render/catalog";
|
import { playgroundCatalog } from "@/lib/render/catalog";
|
||||||
import { buildCatalogDisplayData } from "@/lib/render/catalog-display";
|
import { buildCatalogDisplayData } from "@/lib/render/catalog-display";
|
||||||
|
|
||||||
type Tab = "json" | "nested" | "stream" | "catalog" | "visual";
|
type Tab = "spec" | "nested" | "stream" | "catalog" | "visual";
|
||||||
type RenderView = "preview" | "code";
|
type RenderView = "preview" | "code";
|
||||||
type MobileView =
|
type MobileView =
|
||||||
| "json"
|
| "spec"
|
||||||
| "nested"
|
| "nested"
|
||||||
| "stream"
|
| "stream"
|
||||||
| "catalog"
|
| "catalog"
|
||||||
@@ -40,6 +46,79 @@ interface Version {
|
|||||||
status: "generating" | "complete" | "error";
|
status: "generating" | "complete" | "error";
|
||||||
usage: TokenUsage | null;
|
usage: TokenUsage | null;
|
||||||
rawLines: string[];
|
rawLines: string[];
|
||||||
|
format: StreamFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTokens(n: number): string {
|
||||||
|
if (n >= 1000) return `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k`;
|
||||||
|
return String(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PlaygroundControls({
|
||||||
|
format,
|
||||||
|
setFormat,
|
||||||
|
editModes,
|
||||||
|
setEditModes,
|
||||||
|
showClear,
|
||||||
|
onClear,
|
||||||
|
}: {
|
||||||
|
format: StreamFormat;
|
||||||
|
setFormat: (f: StreamFormat) => void;
|
||||||
|
editModes: EditMode[];
|
||||||
|
setEditModes: React.Dispatch<React.SetStateAction<EditMode[]>>;
|
||||||
|
showClear: boolean;
|
||||||
|
onClear: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex items-center rounded border border-border text-[10px] font-mono overflow-hidden">
|
||||||
|
{(["jsonl", "yaml"] as const).map((f) => (
|
||||||
|
<button
|
||||||
|
key={f}
|
||||||
|
onClick={() => setFormat(f)}
|
||||||
|
className={`px-1.5 py-0.5 transition-colors ${
|
||||||
|
format === f
|
||||||
|
? "bg-muted text-foreground"
|
||||||
|
: "text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{f}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center rounded border border-border text-[10px] font-mono overflow-hidden">
|
||||||
|
{(["patch", "merge", "diff"] as const).map((m) => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
onClick={() => {
|
||||||
|
setEditModes((prev) =>
|
||||||
|
prev.includes(m)
|
||||||
|
? prev.length > 1
|
||||||
|
? prev.filter((x) => x !== m)
|
||||||
|
: prev
|
||||||
|
: [...prev, m],
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className={`px-1.5 py-0.5 transition-colors ${
|
||||||
|
editModes.includes(m)
|
||||||
|
? "bg-muted text-foreground"
|
||||||
|
: "text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{m}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{showClear && (
|
||||||
|
<button
|
||||||
|
onClick={onClear}
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,13 +179,15 @@ export function Playground() {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
const [activeTab, setActiveTab] = useState<Tab>("json");
|
const [activeTab, setActiveTab] = useState<Tab>("spec");
|
||||||
const [catalogSection, setCatalogSection] = useState<
|
const [catalogSection, setCatalogSection] = useState<
|
||||||
"components" | "actions"
|
"components" | "actions"
|
||||||
>("components");
|
>("components");
|
||||||
const [renderView, setRenderView] = useState<RenderView>("preview");
|
const [renderView, setRenderView] = useState<RenderView>("preview");
|
||||||
const [mobileView, setMobileView] = useState<MobileView>("preview");
|
const [mobileView, setMobileView] = useState<MobileView>("preview");
|
||||||
const [versionsSheetOpen, setVersionsSheetOpen] = useState(false);
|
const [versionsSheetOpen, setVersionsSheetOpen] = useState(false);
|
||||||
|
const [format, setFormat] = useState<StreamFormat>("jsonl");
|
||||||
|
const [editModes, setEditModes] = useState<EditMode[]>(["patch"]);
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const mobileInputRef = useRef<HTMLTextAreaElement>(null);
|
const mobileInputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const versionsEndRef = useRef<HTMLDivElement>(null);
|
const versionsEndRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -124,12 +205,13 @@ export function Playground() {
|
|||||||
rawLines: streamRawLines,
|
rawLines: streamRawLines,
|
||||||
send,
|
send,
|
||||||
clear,
|
clear,
|
||||||
} = useUIStream({
|
} = usePlaygroundStream({
|
||||||
api: "/api/generate",
|
api: "/api/generate",
|
||||||
|
format,
|
||||||
|
editModes,
|
||||||
onError: (err: Error) => {
|
onError: (err: Error) => {
|
||||||
console.error("Generation error:", err);
|
console.error("Generation error:", err);
|
||||||
toast.error(err.message || "Generation failed. Please try again.");
|
toast.error(err.message || "Generation failed. Please try again.");
|
||||||
// Mark the version as errored
|
|
||||||
if (generatingVersionIdRef.current) {
|
if (generatingVersionIdRef.current) {
|
||||||
const erroredVersionId = generatingVersionIdRef.current;
|
const erroredVersionId = generatingVersionIdRef.current;
|
||||||
setVersions((prev) =>
|
setVersions((prev) =>
|
||||||
@@ -140,7 +222,7 @@ export function Playground() {
|
|||||||
generatingVersionIdRef.current = null;
|
generatingVersionIdRef.current = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
} as Parameters<typeof useUIStream>[0]);
|
});
|
||||||
|
|
||||||
// Get the selected version
|
// Get the selected version
|
||||||
const selectedVersion = versions.find((v) => v.id === selectedVersionId);
|
const selectedVersion = versions.find((v) => v.id === selectedVersionId);
|
||||||
@@ -215,6 +297,7 @@ export function Playground() {
|
|||||||
status: "generating",
|
status: "generating",
|
||||||
usage: null,
|
usage: null,
|
||||||
rawLines: [],
|
rawLines: [],
|
||||||
|
format,
|
||||||
};
|
};
|
||||||
|
|
||||||
generatingVersionIdRef.current = newVersionId;
|
generatingVersionIdRef.current = newVersionId;
|
||||||
@@ -224,7 +307,7 @@ export function Playground() {
|
|||||||
|
|
||||||
// Pass the current tree as context so the API can iterate on it
|
// Pass the current tree as context so the API can iterate on it
|
||||||
await send(inputValue.trim(), { previousSpec: currentTreeRef.current });
|
await send(inputValue.trim(), { previousSpec: currentTreeRef.current });
|
||||||
}, [inputValue, isStreaming, send]);
|
}, [inputValue, isStreaming, send, format]);
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent) => {
|
(e: React.KeyboardEvent) => {
|
||||||
@@ -250,9 +333,16 @@ export function Playground() {
|
|||||||
[selectedVersionId, isStreaming],
|
[selectedVersionId, isStreaming],
|
||||||
);
|
);
|
||||||
|
|
||||||
const jsonCode = currentTree
|
const specCode = useMemo(() => {
|
||||||
? JSON.stringify(currentTree, null, 2)
|
if (!currentTree)
|
||||||
: "// waiting...";
|
return format === "yaml" ? "# waiting..." : "// waiting...";
|
||||||
|
if (format === "yaml") {
|
||||||
|
return yamlStringify(currentTree, { indent: 2 }).trimEnd();
|
||||||
|
}
|
||||||
|
return JSON.stringify(currentTree, null, 2);
|
||||||
|
}, [currentTree, format]);
|
||||||
|
|
||||||
|
const specLang = format === "yaml" ? "yaml" : "json";
|
||||||
|
|
||||||
const nestedCode = useMemo(() => {
|
const nestedCode = useMemo(() => {
|
||||||
if (!currentTree || !currentTree.root) return "// waiting...";
|
if (!currentTree || !currentTree.root) return "// waiting...";
|
||||||
@@ -275,7 +365,7 @@ export function Playground() {
|
|||||||
const componentName = element.type;
|
const componentName = element.type;
|
||||||
|
|
||||||
const propsObj: Record<string, unknown> = {};
|
const propsObj: Record<string, unknown> = {};
|
||||||
for (const [k, v] of Object.entries(element.props)) {
|
for (const [k, v] of Object.entries(element.props ?? {})) {
|
||||||
if (v !== null && v !== undefined) {
|
if (v !== null && v !== undefined) {
|
||||||
propsObj[k] = v;
|
propsObj[k] = v;
|
||||||
}
|
}
|
||||||
@@ -321,6 +411,15 @@ ${jsx}
|
|||||||
}`;
|
}`;
|
||||||
}, [currentTree]);
|
}, [currentTree]);
|
||||||
|
|
||||||
|
// Determine syntax lang for raw stream based on selected version's format
|
||||||
|
const streamLang = isSelectedVersionGenerating
|
||||||
|
? format === "yaml"
|
||||||
|
? "yaml"
|
||||||
|
: "json"
|
||||||
|
: selectedVersion?.format === "yaml"
|
||||||
|
? "yaml"
|
||||||
|
: "json";
|
||||||
|
|
||||||
// Chat pane content
|
// Chat pane content
|
||||||
const chatPane = (
|
const chatPane = (
|
||||||
<div className="h-full flex flex-col border-t border-border">
|
<div className="h-full flex flex-col border-t border-border">
|
||||||
@@ -392,9 +491,15 @@ ${jsx}
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{version.usage && (
|
{version.usage && (
|
||||||
<div className="flex items-center gap-2 mt-1 ml-6">
|
<div className="mt-1 ml-6">
|
||||||
<span className="text-[10px] font-mono text-muted-foreground/60">
|
<span className="text-[10px] font-mono text-muted-foreground/60">
|
||||||
{version.usage.totalTokens.toLocaleString()} tokens
|
{formatTokens(
|
||||||
|
version.usage.promptTokens - version.usage.cachedTokens,
|
||||||
|
)}{" "}
|
||||||
|
in · {formatTokens(version.usage.completionTokens)} out
|
||||||
|
{version.usage.cachedTokens > 0
|
||||||
|
? ` · ${formatTokens(version.usage.cachedTokens)} cached`
|
||||||
|
: ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -425,20 +530,18 @@ ${jsx}
|
|||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between items-center mt-2">
|
<div className="flex justify-between items-center mt-2">
|
||||||
{versions.length > 0 ? (
|
<PlaygroundControls
|
||||||
<button
|
format={format}
|
||||||
onClick={() => {
|
setFormat={setFormat}
|
||||||
setVersions([]);
|
editModes={editModes}
|
||||||
setSelectedVersionId(null);
|
setEditModes={setEditModes}
|
||||||
clear();
|
showClear={versions.length > 0}
|
||||||
}}
|
onClear={() => {
|
||||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
setVersions([]);
|
||||||
>
|
setSelectedVersionId(null);
|
||||||
Clear
|
clear();
|
||||||
</button>
|
}}
|
||||||
) : (
|
/>
|
||||||
<div />
|
|
||||||
)}
|
|
||||||
{isStreaming ? (
|
{isStreaming ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => clear()}
|
onClick={() => clear()}
|
||||||
@@ -492,18 +595,18 @@ ${jsx}
|
|||||||
const copyText =
|
const copyText =
|
||||||
activeTab === "stream"
|
activeTab === "stream"
|
||||||
? currentRawLines.join("\n")
|
? currentRawLines.join("\n")
|
||||||
: activeTab === "json"
|
: activeTab === "spec"
|
||||||
? jsonCode
|
? specCode
|
||||||
: activeTab === "nested"
|
: activeTab === "nested"
|
||||||
? nestedCode
|
? nestedCode
|
||||||
: activeTab === "visual"
|
: activeTab === "visual"
|
||||||
? jsonCode
|
? specCode
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const codePane = (
|
const codePane = (
|
||||||
<div className="h-full flex flex-col border-t border-border">
|
<div className="h-full flex flex-col border-t border-border">
|
||||||
<div className="border-b border-border px-3 h-9 flex items-center gap-3">
|
<div className="border-b border-border px-3 h-9 flex items-center gap-3">
|
||||||
{(["json", "visual", "nested", "stream", "catalog"] as const).map(
|
{(["spec", "visual", "nested", "stream", "catalog"] as const).map(
|
||||||
(tab) => (
|
(tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
@@ -514,7 +617,7 @@ ${jsx}
|
|||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab}
|
{tab === "spec" ? (format === "yaml" ? "yaml" : "json") : tab}
|
||||||
</button>
|
</button>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
@@ -679,7 +782,7 @@ ${jsx}
|
|||||||
currentRawLines.length > 0 ? (
|
currentRawLines.length > 0 ? (
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
code={currentRawLines.join("\n")}
|
code={currentRawLines.join("\n")}
|
||||||
lang="json"
|
lang={streamLang}
|
||||||
fillHeight
|
fillHeight
|
||||||
hideCopyButton
|
hideCopyButton
|
||||||
/>
|
/>
|
||||||
@@ -691,7 +794,12 @@ ${jsx}
|
|||||||
) : activeTab === "nested" ? (
|
) : activeTab === "nested" ? (
|
||||||
<CodeBlock code={nestedCode} lang="json" fillHeight hideCopyButton />
|
<CodeBlock code={nestedCode} lang="json" fillHeight hideCopyButton />
|
||||||
) : (
|
) : (
|
||||||
<CodeBlock code={jsonCode} lang="json" fillHeight hideCopyButton />
|
<CodeBlock
|
||||||
|
code={specCode}
|
||||||
|
lang={specLang}
|
||||||
|
fillHeight
|
||||||
|
hideCopyButton
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -790,7 +898,7 @@ ${jsx}
|
|||||||
: 0}
|
: 0}
|
||||||
</button>
|
</button>
|
||||||
{/* Code tabs */}
|
{/* Code tabs */}
|
||||||
{(["json", "visual", "nested", "stream", "catalog"] as const).map(
|
{(["spec", "visual", "nested", "stream", "catalog"] as const).map(
|
||||||
(tab) => (
|
(tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
@@ -801,7 +909,7 @@ ${jsx}
|
|||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab}
|
{tab === "spec" ? (format === "yaml" ? "yaml" : "json") : tab}
|
||||||
</button>
|
</button>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
@@ -982,7 +1090,7 @@ ${jsx}
|
|||||||
currentRawLines.length > 0 ? (
|
currentRawLines.length > 0 ? (
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
code={currentRawLines.join("\n")}
|
code={currentRawLines.join("\n")}
|
||||||
lang="json"
|
lang={streamLang}
|
||||||
fillHeight
|
fillHeight
|
||||||
hideCopyButton
|
hideCopyButton
|
||||||
/>
|
/>
|
||||||
@@ -998,8 +1106,13 @@ ${jsx}
|
|||||||
fillHeight
|
fillHeight
|
||||||
hideCopyButton
|
hideCopyButton
|
||||||
/>
|
/>
|
||||||
) : mobileView === "json" ? (
|
) : mobileView === "spec" ? (
|
||||||
<CodeBlock code={jsonCode} lang="json" fillHeight hideCopyButton />
|
<CodeBlock
|
||||||
|
code={specCode}
|
||||||
|
lang={specLang}
|
||||||
|
fillHeight
|
||||||
|
hideCopyButton
|
||||||
|
/>
|
||||||
) : mobileView === "preview" ? (
|
) : mobileView === "preview" ? (
|
||||||
currentTree && currentTree.root ? (
|
currentTree && currentTree.root ? (
|
||||||
<div className="w-full min-h-full flex items-center justify-center p-6">
|
<div className="w-full min-h-full flex items-center justify-center p-6">
|
||||||
@@ -1075,20 +1188,18 @@ ${jsx}
|
|||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between items-center mt-2">
|
<div className="flex justify-between items-center mt-2">
|
||||||
{versions.length > 0 ? (
|
<PlaygroundControls
|
||||||
<button
|
format={format}
|
||||||
onClick={() => {
|
setFormat={setFormat}
|
||||||
setVersions([]);
|
editModes={editModes}
|
||||||
setSelectedVersionId(null);
|
setEditModes={setEditModes}
|
||||||
clear();
|
showClear={versions.length > 0}
|
||||||
}}
|
onClear={() => {
|
||||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
setVersions([]);
|
||||||
>
|
setSelectedVersionId(null);
|
||||||
Clear
|
clear();
|
||||||
</button>
|
}}
|
||||||
) : (
|
/>
|
||||||
<div />
|
|
||||||
)}
|
|
||||||
{isStreaming ? (
|
{isStreaming ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => clear()}
|
onClick={() => clear()}
|
||||||
@@ -1165,9 +1276,16 @@ ${jsx}
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{version.usage && (
|
{version.usage && (
|
||||||
<div className="flex items-center gap-2 mt-1 ml-6">
|
<div className="mt-1 ml-6">
|
||||||
<span className="text-[10px] font-mono text-muted-foreground/60">
|
<span className="text-[10px] font-mono text-muted-foreground/60">
|
||||||
{version.usage.totalTokens.toLocaleString()} tokens
|
{formatTokens(
|
||||||
|
version.usage.promptTokens -
|
||||||
|
version.usage.cachedTokens,
|
||||||
|
)}{" "}
|
||||||
|
in · {formatTokens(version.usage.completionTokens)} out
|
||||||
|
{version.usage.cachedTokens > 0
|
||||||
|
? ` · ${formatTokens(version.usage.cachedTokens)} cached`
|
||||||
|
: ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ export const docsNavigation: NavSection[] = [
|
|||||||
{ title: "@json-render/zustand", href: "/docs/api/zustand" },
|
{ title: "@json-render/zustand", href: "/docs/api/zustand" },
|
||||||
{ title: "@json-render/jotai", href: "/docs/api/jotai" },
|
{ title: "@json-render/jotai", href: "/docs/api/jotai" },
|
||||||
{ title: "@json-render/xstate", href: "/docs/api/xstate" },
|
{ title: "@json-render/xstate", href: "/docs/api/xstate" },
|
||||||
|
{ title: "@json-render/yaml", href: "/docs/api/yaml" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export const PAGE_TITLES: Record<string, string> = {
|
|||||||
"docs/api/jotai": "@json-render/jotai API",
|
"docs/api/jotai": "@json-render/jotai API",
|
||||||
"docs/api/react-three-fiber": "@json-render/react-three-fiber API",
|
"docs/api/react-three-fiber": "@json-render/react-three-fiber API",
|
||||||
"docs/api/xstate": "@json-render/xstate API",
|
"docs/api/xstate": "@json-render/xstate API",
|
||||||
|
"docs/api/yaml": "@json-render/yaml API",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import type { Spec, JsonPatch } from "@json-render/core";
|
||||||
|
import { setByPath, getByPath, removeByPath } from "@json-render/core";
|
||||||
|
|
||||||
|
export function setSpecValue(
|
||||||
|
newSpec: Spec,
|
||||||
|
path: string,
|
||||||
|
value: unknown,
|
||||||
|
): void {
|
||||||
|
if (path === "/root") {
|
||||||
|
newSpec.root = value as string;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (path === "/state") {
|
||||||
|
newSpec.state = value as Record<string, unknown>;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (path.startsWith("/state/")) {
|
||||||
|
if (!newSpec.state) newSpec.state = {};
|
||||||
|
setByPath(
|
||||||
|
newSpec.state as Record<string, unknown>,
|
||||||
|
path.slice("/state".length),
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (path.startsWith("/elements/")) {
|
||||||
|
const pathParts = path.slice("/elements/".length).split("/");
|
||||||
|
const elementKey = pathParts[0];
|
||||||
|
if (!elementKey) return;
|
||||||
|
if (pathParts.length === 1) {
|
||||||
|
if (value == null || typeof value !== "object") return;
|
||||||
|
const el = value as Record<string, unknown>;
|
||||||
|
newSpec.elements[elementKey] = {
|
||||||
|
...el,
|
||||||
|
type: typeof el.type === "string" ? el.type : "",
|
||||||
|
props: el.props != null && typeof el.props === "object" ? el.props : {},
|
||||||
|
children: Array.isArray(el.children) ? el.children : [],
|
||||||
|
} as Spec["elements"][string];
|
||||||
|
} else {
|
||||||
|
const element = newSpec.elements[elementKey];
|
||||||
|
if (element) {
|
||||||
|
const newElement = { ...element };
|
||||||
|
setByPath(
|
||||||
|
newElement as unknown as Record<string, unknown>,
|
||||||
|
"/" + pathParts.slice(1).join("/"),
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
newSpec.elements[elementKey] = newElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeSpecValue(newSpec: Spec, path: string): void {
|
||||||
|
if (path === "/state") {
|
||||||
|
delete newSpec.state;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (path.startsWith("/state/") && newSpec.state) {
|
||||||
|
removeByPath(
|
||||||
|
newSpec.state as Record<string, unknown>,
|
||||||
|
path.slice("/state".length),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (path.startsWith("/elements/")) {
|
||||||
|
const pathParts = path.slice("/elements/".length).split("/");
|
||||||
|
const elementKey = pathParts[0];
|
||||||
|
if (!elementKey) return;
|
||||||
|
if (pathParts.length === 1) {
|
||||||
|
delete newSpec.elements[elementKey];
|
||||||
|
} else {
|
||||||
|
const element = newSpec.elements[elementKey];
|
||||||
|
if (element) {
|
||||||
|
const newElement = { ...element };
|
||||||
|
removeByPath(
|
||||||
|
newElement as unknown as Record<string, unknown>,
|
||||||
|
"/" + pathParts.slice(1).join("/"),
|
||||||
|
);
|
||||||
|
newSpec.elements[elementKey] = newElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSpecValue(spec: Spec, path: string): unknown {
|
||||||
|
if (path === "/root") return spec.root;
|
||||||
|
if (path === "/state") return spec.state;
|
||||||
|
if (path.startsWith("/state/") && spec.state) {
|
||||||
|
return getByPath(
|
||||||
|
spec.state as Record<string, unknown>,
|
||||||
|
path.slice("/state".length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return getByPath(spec as unknown as Record<string, unknown>, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeSpec(spec: Spec): void {
|
||||||
|
if (
|
||||||
|
spec.state === null ||
|
||||||
|
(spec.state !== undefined && typeof spec.state !== "object")
|
||||||
|
) {
|
||||||
|
spec.state = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(spec.elements)) {
|
||||||
|
const el = spec.elements[key];
|
||||||
|
if (!el || typeof el !== "object") {
|
||||||
|
delete spec.elements[key];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (el.props == null || typeof el.props !== "object") {
|
||||||
|
spec.elements[key] = { ...el, props: {} } as Spec["elements"][string];
|
||||||
|
}
|
||||||
|
if (!Array.isArray(spec.elements[key]!.children)) {
|
||||||
|
spec.elements[key] = {
|
||||||
|
...spec.elements[key]!,
|
||||||
|
children: [],
|
||||||
|
} as Spec["elements"][string];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applySpecPatch(spec: Spec, patch: JsonPatch): Spec {
|
||||||
|
const newSpec = {
|
||||||
|
...spec,
|
||||||
|
elements: { ...spec.elements },
|
||||||
|
...(spec.state ? { state: { ...spec.state } } : {}),
|
||||||
|
};
|
||||||
|
switch (patch.op) {
|
||||||
|
case "add":
|
||||||
|
case "replace":
|
||||||
|
setSpecValue(newSpec, patch.path, patch.value);
|
||||||
|
break;
|
||||||
|
case "remove":
|
||||||
|
removeSpecValue(newSpec, patch.path);
|
||||||
|
break;
|
||||||
|
case "move":
|
||||||
|
if (patch.from) {
|
||||||
|
const moveValue = getSpecValue(newSpec, patch.from);
|
||||||
|
removeSpecValue(newSpec, patch.from);
|
||||||
|
setSpecValue(newSpec, patch.path, moveValue);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "copy":
|
||||||
|
if (patch.from) {
|
||||||
|
setSpecValue(newSpec, patch.path, getSpecValue(newSpec, patch.from));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "test":
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
normalizeSpec(newSpec);
|
||||||
|
return newSpec;
|
||||||
|
}
|
||||||
@@ -0,0 +1,482 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useCallback, useRef, useEffect } from "react";
|
||||||
|
import type { Spec, JsonPatch, EditMode } from "@json-render/core";
|
||||||
|
import { deepMergeSpec, diffToPatches } from "@json-render/core";
|
||||||
|
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
||||||
|
import { applyPatch as applyUnifiedDiff } from "diff";
|
||||||
|
import {
|
||||||
|
createYamlStreamCompiler,
|
||||||
|
YAML_SPEC_FENCE,
|
||||||
|
YAML_EDIT_FENCE,
|
||||||
|
YAML_PATCH_FENCE,
|
||||||
|
DIFF_FENCE,
|
||||||
|
FENCE_CLOSE,
|
||||||
|
} from "@json-render/yaml";
|
||||||
|
import { applySpecPatch } from "./spec-patch";
|
||||||
|
|
||||||
|
export type StreamFormat = "jsonl" | "yaml";
|
||||||
|
|
||||||
|
export interface TokenUsage {
|
||||||
|
promptTokens: number;
|
||||||
|
completionTokens: number;
|
||||||
|
totalTokens: number;
|
||||||
|
cachedTokens: number;
|
||||||
|
cacheWriteTokens: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsePlaygroundStreamOptions {
|
||||||
|
api: string;
|
||||||
|
format: StreamFormat;
|
||||||
|
editModes?: EditMode[];
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
onComplete?: (spec: Spec) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsePlaygroundStreamReturn {
|
||||||
|
spec: Spec | null;
|
||||||
|
isStreaming: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
usage: TokenUsage | null;
|
||||||
|
rawLines: string[];
|
||||||
|
send: (prompt: string, context?: Record<string, unknown>) => Promise<void>;
|
||||||
|
clear: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JSONL helpers ──
|
||||||
|
|
||||||
|
type ParsedLine =
|
||||||
|
| { type: "patch"; patch: JsonPatch }
|
||||||
|
| { type: "usage"; usage: TokenUsage }
|
||||||
|
| { type: "json-edit"; mergeObj: Record<string, unknown> }
|
||||||
|
| null;
|
||||||
|
|
||||||
|
function parseLine(line: string): ParsedLine {
|
||||||
|
try {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("//")) return null;
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
if (parsed.__meta === "usage") {
|
||||||
|
return {
|
||||||
|
type: "usage",
|
||||||
|
usage: {
|
||||||
|
promptTokens: parsed.promptTokens ?? 0,
|
||||||
|
completionTokens: parsed.completionTokens ?? 0,
|
||||||
|
totalTokens: parsed.totalTokens ?? 0,
|
||||||
|
cachedTokens: parsed.cachedTokens ?? 0,
|
||||||
|
cacheWriteTokens: parsed.cacheWriteTokens ?? 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (parsed.__json_edit === true) {
|
||||||
|
const mergeObj = { ...parsed };
|
||||||
|
delete mergeObj.__json_edit;
|
||||||
|
return { type: "json-edit", mergeObj };
|
||||||
|
}
|
||||||
|
return { type: "patch", patch: parsed as JsonPatch };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FenceState = "outside" | "yaml-spec" | "yaml-edit" | "yaml-patch" | "diff";
|
||||||
|
|
||||||
|
// ── Hook ──
|
||||||
|
|
||||||
|
export function usePlaygroundStream({
|
||||||
|
api,
|
||||||
|
format,
|
||||||
|
editModes,
|
||||||
|
onError,
|
||||||
|
onComplete,
|
||||||
|
}: UsePlaygroundStreamOptions): UsePlaygroundStreamReturn {
|
||||||
|
const [spec, setSpec] = useState<Spec | null>(null);
|
||||||
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [usage, setUsage] = useState<TokenUsage | null>(null);
|
||||||
|
const [rawLines, setRawLines] = useState<string[]>([]);
|
||||||
|
const rawLinesRef = useRef<string[]>([]);
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
const onCompleteRef = useRef(onComplete);
|
||||||
|
onCompleteRef.current = onComplete;
|
||||||
|
const onErrorRef = useRef(onError);
|
||||||
|
onErrorRef.current = onError;
|
||||||
|
const formatRef = useRef(format);
|
||||||
|
formatRef.current = format;
|
||||||
|
const editModesRef = useRef(editModes);
|
||||||
|
editModesRef.current = editModes;
|
||||||
|
|
||||||
|
const clear = useCallback(() => {
|
||||||
|
setSpec(null);
|
||||||
|
setError(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const send = useCallback(
|
||||||
|
async (prompt: string, context?: Record<string, unknown>) => {
|
||||||
|
abortControllerRef.current = new AbortController();
|
||||||
|
|
||||||
|
setIsStreaming(true);
|
||||||
|
setError(null);
|
||||||
|
setUsage(null);
|
||||||
|
rawLinesRef.current = [];
|
||||||
|
setRawLines([]);
|
||||||
|
|
||||||
|
const previousSpec = context?.previousSpec as Spec | undefined;
|
||||||
|
let currentSpec: Spec =
|
||||||
|
previousSpec && previousSpec.root
|
||||||
|
? { ...previousSpec, elements: { ...previousSpec.elements } }
|
||||||
|
: { root: "", elements: {} };
|
||||||
|
setSpec(currentSpec);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(api, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt,
|
||||||
|
context,
|
||||||
|
format: formatRef.current,
|
||||||
|
editModes: editModesRef.current,
|
||||||
|
}),
|
||||||
|
signal: abortControllerRef.current.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorMessage = `HTTP error: ${response.status}`;
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
if (errorData.message) errorMessage = errorData.message;
|
||||||
|
else if (errorData.error) errorMessage = errorData.error;
|
||||||
|
} catch {
|
||||||
|
// use default
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) throw new Error("No response body");
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
if (formatRef.current === "yaml") {
|
||||||
|
// ── YAML streaming ──
|
||||||
|
let fenceState: FenceState = "outside";
|
||||||
|
const compiler = createYamlStreamCompiler<Record<string, unknown>>();
|
||||||
|
let yamlEditAccumulated = "";
|
||||||
|
let yamlPatchAccumulated = "";
|
||||||
|
let diffAccumulated = "";
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
const lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop() ?? "";
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
// Check for usage metadata (appended after stream)
|
||||||
|
if (trimmed.startsWith("{") && trimmed.includes('"__meta"')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
if (parsed.__meta === "usage") {
|
||||||
|
setUsage({
|
||||||
|
promptTokens: parsed.promptTokens ?? 0,
|
||||||
|
completionTokens: parsed.completionTokens ?? 0,
|
||||||
|
totalTokens: parsed.totalTokens ?? 0,
|
||||||
|
cachedTokens: parsed.cachedTokens ?? 0,
|
||||||
|
cacheWriteTokens: parsed.cacheWriteTokens ?? 0,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// not JSON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rawLinesRef.current.push(line);
|
||||||
|
|
||||||
|
if (fenceState === "outside") {
|
||||||
|
if (
|
||||||
|
trimmed === YAML_SPEC_FENCE ||
|
||||||
|
trimmed.startsWith(YAML_SPEC_FENCE + " ")
|
||||||
|
) {
|
||||||
|
fenceState = "yaml-spec";
|
||||||
|
compiler.reset();
|
||||||
|
} else if (
|
||||||
|
trimmed === YAML_EDIT_FENCE ||
|
||||||
|
trimmed.startsWith(YAML_EDIT_FENCE + " ")
|
||||||
|
) {
|
||||||
|
fenceState = "yaml-edit";
|
||||||
|
yamlEditAccumulated = "";
|
||||||
|
} else if (
|
||||||
|
trimmed === YAML_PATCH_FENCE ||
|
||||||
|
trimmed.startsWith(YAML_PATCH_FENCE + " ")
|
||||||
|
) {
|
||||||
|
fenceState = "yaml-patch";
|
||||||
|
yamlPatchAccumulated = "";
|
||||||
|
} else if (
|
||||||
|
trimmed === DIFF_FENCE ||
|
||||||
|
trimmed.startsWith(DIFF_FENCE + " ")
|
||||||
|
) {
|
||||||
|
fenceState = "diff";
|
||||||
|
diffAccumulated = "";
|
||||||
|
}
|
||||||
|
} else if (trimmed === FENCE_CLOSE || trimmed === "````") {
|
||||||
|
if (fenceState === "yaml-spec") {
|
||||||
|
const { result, newPatches } = compiler.flush();
|
||||||
|
if (result && typeof result === "object" && result.root) {
|
||||||
|
for (const patch of newPatches) {
|
||||||
|
currentSpec = applySpecPatch(currentSpec, patch);
|
||||||
|
}
|
||||||
|
setSpec({ ...currentSpec });
|
||||||
|
}
|
||||||
|
} else if (fenceState === "yaml-edit") {
|
||||||
|
try {
|
||||||
|
const editObj = yamlParse(yamlEditAccumulated);
|
||||||
|
if (editObj && typeof editObj === "object") {
|
||||||
|
const merged = deepMergeSpec(
|
||||||
|
currentSpec as unknown as Record<string, unknown>,
|
||||||
|
editObj as Record<string, unknown>,
|
||||||
|
);
|
||||||
|
const patches = diffToPatches(
|
||||||
|
currentSpec as unknown as Record<string, unknown>,
|
||||||
|
merged,
|
||||||
|
);
|
||||||
|
for (const patch of patches) {
|
||||||
|
currentSpec = applySpecPatch(currentSpec, patch);
|
||||||
|
}
|
||||||
|
setSpec({ ...currentSpec });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Invalid YAML edit
|
||||||
|
}
|
||||||
|
} else if (fenceState === "yaml-patch") {
|
||||||
|
for (const patchLine of yamlPatchAccumulated.split("\n")) {
|
||||||
|
const t = patchLine.trim();
|
||||||
|
if (!t) continue;
|
||||||
|
try {
|
||||||
|
const patch = JSON.parse(t) as JsonPatch;
|
||||||
|
if (patch.op) {
|
||||||
|
currentSpec = applySpecPatch(currentSpec, patch);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip invalid JSON lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSpec({ ...currentSpec });
|
||||||
|
} else if (fenceState === "diff") {
|
||||||
|
try {
|
||||||
|
const specYaml = yamlStringify(currentSpec, { indent: 2 });
|
||||||
|
const patched = applyUnifiedDiff(specYaml, diffAccumulated);
|
||||||
|
if (typeof patched === "string") {
|
||||||
|
const parsed = yamlParse(patched);
|
||||||
|
if (parsed && typeof parsed === "object") {
|
||||||
|
const patches = diffToPatches(
|
||||||
|
currentSpec as unknown as Record<string, unknown>,
|
||||||
|
parsed as Record<string, unknown>,
|
||||||
|
);
|
||||||
|
for (const patch of patches) {
|
||||||
|
currentSpec = applySpecPatch(currentSpec, patch);
|
||||||
|
}
|
||||||
|
setSpec({ ...currentSpec });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Diff apply or reparse failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fenceState = "outside";
|
||||||
|
} else if (fenceState === "yaml-spec") {
|
||||||
|
const { newPatches } = compiler.push(line + "\n");
|
||||||
|
if (newPatches.length > 0) {
|
||||||
|
for (const patch of newPatches) {
|
||||||
|
currentSpec = applySpecPatch(currentSpec, patch);
|
||||||
|
}
|
||||||
|
setSpec({ ...currentSpec });
|
||||||
|
}
|
||||||
|
} else if (fenceState === "yaml-edit") {
|
||||||
|
yamlEditAccumulated += line + "\n";
|
||||||
|
} else if (fenceState === "yaml-patch") {
|
||||||
|
yamlPatchAccumulated += line + "\n";
|
||||||
|
} else if (fenceState === "diff") {
|
||||||
|
diffAccumulated += line + "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setRawLines([...rawLinesRef.current]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process remaining buffer
|
||||||
|
if (buffer.trim()) {
|
||||||
|
const trimmed = buffer.trim();
|
||||||
|
if (trimmed.startsWith("{") && trimmed.includes('"__meta"')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
if (parsed.__meta === "usage") {
|
||||||
|
setUsage({
|
||||||
|
promptTokens: parsed.promptTokens ?? 0,
|
||||||
|
completionTokens: parsed.completionTokens ?? 0,
|
||||||
|
totalTokens: parsed.totalTokens ?? 0,
|
||||||
|
cachedTokens: parsed.cachedTokens ?? 0,
|
||||||
|
cacheWriteTokens: parsed.cacheWriteTokens ?? 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// not JSON
|
||||||
|
}
|
||||||
|
} else if (fenceState === "yaml-spec") {
|
||||||
|
compiler.push(buffer);
|
||||||
|
const { result, newPatches } = compiler.flush();
|
||||||
|
if (result && typeof result === "object" && result.root) {
|
||||||
|
for (const patch of newPatches) {
|
||||||
|
currentSpec = applySpecPatch(currentSpec, patch);
|
||||||
|
}
|
||||||
|
setSpec({ ...currentSpec });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ── JSONL streaming ──
|
||||||
|
let jsonlDiffState: "outside" | "diff" = "outside";
|
||||||
|
let jsonlDiffAccumulated = "";
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
const lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop() ?? "";
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
|
||||||
|
// Diff fence detection within JSONL mode
|
||||||
|
if (jsonlDiffState === "outside") {
|
||||||
|
if (
|
||||||
|
trimmed === DIFF_FENCE ||
|
||||||
|
trimmed.startsWith(DIFF_FENCE + " ")
|
||||||
|
) {
|
||||||
|
jsonlDiffState = "diff";
|
||||||
|
jsonlDiffAccumulated = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
jsonlDiffState === "diff" &&
|
||||||
|
(trimmed === FENCE_CLOSE || trimmed === "````")
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const specJson = JSON.stringify(currentSpec, null, 2);
|
||||||
|
const patched = applyUnifiedDiff(
|
||||||
|
specJson,
|
||||||
|
jsonlDiffAccumulated,
|
||||||
|
);
|
||||||
|
if (typeof patched === "string") {
|
||||||
|
const parsed = JSON.parse(patched);
|
||||||
|
if (parsed && typeof parsed === "object") {
|
||||||
|
const patches = diffToPatches(
|
||||||
|
currentSpec as unknown as Record<string, unknown>,
|
||||||
|
parsed as Record<string, unknown>,
|
||||||
|
);
|
||||||
|
for (const patch of patches) {
|
||||||
|
currentSpec = applySpecPatch(currentSpec, patch);
|
||||||
|
}
|
||||||
|
setSpec({ ...currentSpec });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Diff apply failed
|
||||||
|
}
|
||||||
|
jsonlDiffState = "outside";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonlDiffState === "diff") {
|
||||||
|
jsonlDiffAccumulated += line + "\n";
|
||||||
|
rawLinesRef.current.push(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard JSONL line parsing
|
||||||
|
const result = parseLine(trimmed);
|
||||||
|
if (!result) continue;
|
||||||
|
if (result.type === "usage") {
|
||||||
|
setUsage(result.usage);
|
||||||
|
} else if (result.type === "json-edit") {
|
||||||
|
const merged = deepMergeSpec(
|
||||||
|
currentSpec as unknown as Record<string, unknown>,
|
||||||
|
result.mergeObj,
|
||||||
|
);
|
||||||
|
const patches = diffToPatches(
|
||||||
|
currentSpec as unknown as Record<string, unknown>,
|
||||||
|
merged,
|
||||||
|
);
|
||||||
|
for (const patch of patches) {
|
||||||
|
currentSpec = applySpecPatch(currentSpec, patch);
|
||||||
|
}
|
||||||
|
rawLinesRef.current.push(trimmed);
|
||||||
|
setSpec({ ...currentSpec });
|
||||||
|
} else {
|
||||||
|
rawLinesRef.current.push(trimmed);
|
||||||
|
currentSpec = applySpecPatch(currentSpec, result.patch);
|
||||||
|
setSpec({ ...currentSpec });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setRawLines([...rawLinesRef.current]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer.trim()) {
|
||||||
|
const trimmed = buffer.trim();
|
||||||
|
const result = parseLine(trimmed);
|
||||||
|
if (result) {
|
||||||
|
if (result.type === "usage") {
|
||||||
|
setUsage(result.usage);
|
||||||
|
} else if (result.type === "json-edit") {
|
||||||
|
const merged = deepMergeSpec(
|
||||||
|
currentSpec as unknown as Record<string, unknown>,
|
||||||
|
result.mergeObj,
|
||||||
|
);
|
||||||
|
const patches = diffToPatches(
|
||||||
|
currentSpec as unknown as Record<string, unknown>,
|
||||||
|
merged,
|
||||||
|
);
|
||||||
|
for (const patch of patches) {
|
||||||
|
currentSpec = applySpecPatch(currentSpec, patch);
|
||||||
|
}
|
||||||
|
rawLinesRef.current.push(trimmed);
|
||||||
|
setSpec({ ...currentSpec });
|
||||||
|
} else {
|
||||||
|
rawLinesRef.current.push(trimmed);
|
||||||
|
currentSpec = applySpecPatch(currentSpec, result.patch);
|
||||||
|
setSpec({ ...currentSpec });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCompleteRef.current?.(currentSpec);
|
||||||
|
} catch (err) {
|
||||||
|
if ((err as Error).name === "AbortError") return;
|
||||||
|
const error = err instanceof Error ? err : new Error(String(err));
|
||||||
|
setError(error);
|
||||||
|
onErrorRef.current?.(error);
|
||||||
|
} finally {
|
||||||
|
setIsStreaming(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[api],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
abortControllerRef.current?.abort();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { spec, isStreaming, error, usage, rawLines, send, clear };
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"@json-render/codegen": "workspace:*",
|
"@json-render/codegen": "workspace:*",
|
||||||
"@json-render/core": "workspace:*",
|
"@json-render/core": "workspace:*",
|
||||||
"@json-render/react": "workspace:*",
|
"@json-render/react": "workspace:*",
|
||||||
|
"@json-render/yaml": "workspace:*",
|
||||||
"@mdx-js/loader": "^3.1.1",
|
"@mdx-js/loader": "^3.1.1",
|
||||||
"@mdx-js/mdx": "^3.1.1",
|
"@mdx-js/mdx": "^3.1.1",
|
||||||
"@mdx-js/react": "^3.1.1",
|
"@mdx-js/react": "^3.1.1",
|
||||||
@@ -34,9 +35,9 @@
|
|||||||
"bash-tool": "1.3.14",
|
"bash-tool": "1.3.14",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"diff": "^8.0.3",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"geist": "1.7.0",
|
"geist": "1.7.0",
|
||||||
"just-bash": "2.9.6",
|
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "16.1.1",
|
"next": "16.1.1",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
@@ -51,6 +52,7 @@
|
|||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"unist-util-visit": "5.1.0",
|
"unist-util-visit": "5.1.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
|
"yaml": "^2.8.2",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# example-chat
|
# example-chat
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/react@0.14.0
|
||||||
|
- @json-render/shadcn@0.14.0
|
||||||
|
|
||||||
## 0.1.7
|
## 0.1.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-chat",
|
"name": "example-chat",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# example-dashboard
|
# example-dashboard
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/codegen@0.14.0
|
||||||
|
- @json-render/react@0.14.0
|
||||||
|
|
||||||
## 0.1.7
|
## 0.1.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-dashboard",
|
"name": "example-dashboard",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# example-image
|
# example-image
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/image@0.14.0
|
||||||
|
|
||||||
## 0.1.4
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-image",
|
"name": "example-image",
|
||||||
"version": "0.1.4",
|
"version": "0.1.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
# example-mcp
|
# example-mcp
|
||||||
|
|
||||||
|
## 0.1.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/mcp@0.14.0
|
||||||
|
- @json-render/react@0.14.0
|
||||||
|
- @json-render/shadcn@0.14.0
|
||||||
|
|
||||||
## 0.1.3
|
## 0.1.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-mcp",
|
"name": "example-mcp",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# example-no-ai
|
# example-no-ai
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/react@0.14.0
|
||||||
|
- @json-render/shadcn@0.14.0
|
||||||
|
|
||||||
## 0.1.7
|
## 0.1.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-no-ai",
|
"name": "example-no-ai",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# example-react-email
|
# example-react-email
|
||||||
|
|
||||||
|
## 0.1.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/react-email@0.14.0
|
||||||
|
|
||||||
## 0.1.3
|
## 0.1.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-email",
|
"name": "example-react-email",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# example-react-native
|
# example-react-native
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/react-native@0.14.0
|
||||||
|
|
||||||
## 0.1.7
|
## 0.1.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-native",
|
"name": "example-react-native",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# example-react-pdf
|
# example-react-pdf
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/react-pdf@0.14.0
|
||||||
|
|
||||||
## 0.1.7
|
## 0.1.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-pdf",
|
"name": "example-react-pdf",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# example-react-three-fiber
|
# example-react-three-fiber
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/react@0.14.0
|
||||||
|
- @json-render/react-three-fiber@0.14.0
|
||||||
|
|
||||||
## 0.1.7
|
## 0.1.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-three-fiber",
|
"name": "example-react-three-fiber",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# example-remotion
|
# example-remotion
|
||||||
|
|
||||||
|
## 0.1.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/remotion@0.14.0
|
||||||
|
|
||||||
## 0.1.7
|
## 0.1.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-remotion",
|
"name": "example-remotion",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# example-solid
|
# example-solid
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/solid@0.14.0
|
||||||
|
|
||||||
## 0.1.1
|
## 0.1.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-solid",
|
"name": "example-solid",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# com.example.json-render-demo
|
# com.example.json-render-demo
|
||||||
|
|
||||||
|
## 0.0.9
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/react@0.14.0
|
||||||
|
|
||||||
## 0.0.8
|
## 0.0.8
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.example.json-render-demo",
|
"name": "com.example.json-render-demo",
|
||||||
"version": "0.0.8",
|
"version": "0.0.9",
|
||||||
"description": "Test",
|
"description": "Test",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "~~proprietary~~",
|
"license": "~~proprietary~~",
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# com.example.json-render-fullpage-demo
|
# com.example.json-render-fullpage-demo
|
||||||
|
|
||||||
|
## 0.0.9
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/react@0.14.0
|
||||||
|
|
||||||
## 0.0.8
|
## 0.0.8
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.example.json-render-fullpage-demo",
|
"name": "com.example.json-render-fullpage-demo",
|
||||||
"version": "0.0.8",
|
"version": "0.0.9",
|
||||||
"description": "Full-page Stripe App example (alpha)",
|
"description": "Full-page Stripe App example (alpha)",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "~~proprietary~~",
|
"license": "~~proprietary~~",
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# svelte-chat
|
# svelte-chat
|
||||||
|
|
||||||
|
## 0.0.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/svelte@0.14.0
|
||||||
|
|
||||||
## 0.0.4
|
## 0.0.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "svelte-chat",
|
"name": "svelte-chat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.4",
|
"version": "0.0.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# example-svelte
|
# example-svelte
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/svelte@0.14.0
|
||||||
|
|
||||||
## 0.1.4
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-svelte",
|
"name": "example-svelte",
|
||||||
"version": "0.1.4",
|
"version": "0.1.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
# vite-renderers
|
# vite-renderers
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/react@0.14.0
|
||||||
|
- @json-render/solid@0.14.0
|
||||||
|
- @json-render/svelte@0.14.0
|
||||||
|
- @json-render/vue@0.14.0
|
||||||
|
|
||||||
## 0.1.5
|
## 0.1.5
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vite-renderers",
|
"name": "vite-renderers",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# example-vue
|
# example-vue
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/vue@0.14.0
|
||||||
|
|
||||||
## 0.1.5
|
## 0.1.5
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-vue",
|
"name": "example-vue",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
|
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/codegen
|
# @json-render/codegen
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/codegen",
|
"name": "@json-render/codegen",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Utilities for generating code from json-render UI trees",
|
"description": "Utilities for generating code from json-render UI trees",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
# @json-render/core
|
# @json-render/core
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- a8afd8b: Add YAML wire format package and universal edit modes for surgical spec refinement.
|
||||||
|
|
||||||
|
### New:
|
||||||
|
- **`@json-render/yaml`** -- YAML wire format for json-render. Includes streaming YAML parser, `yamlPrompt()` for system prompts, and AI SDK transform (`pipeYamlRender`) as a drop-in alternative to JSONL streaming. Supports four fence types: `yaml-spec`, `yaml-edit`, `yaml-patch`, and `diff`.
|
||||||
|
- **Universal edit modes** in `@json-render/core` -- three strategies for multi-turn spec refinement: `patch` (RFC 6902), `merge` (RFC 7396), and `diff` (unified diff). New `editModes` option on `buildUserPrompt()` and `PromptOptions`. New helpers: `deepMergeSpec()`, `diffToPatches()`, `buildEditUserPrompt()`, `buildEditInstructions()`, `isNonEmptySpec()`.
|
||||||
|
|
||||||
|
### Improved:
|
||||||
|
- **Playground** -- format toggle (JSONL / YAML), edit mode picker (patch / merge / diff), and token usage display with prompt caching stats.
|
||||||
|
- **Prompt caching** -- generate API uses Anthropic ephemeral cache control for system prompts.
|
||||||
|
- **CI** -- lint, type-check, and test jobs now run in parallel.
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
+36
-1
@@ -210,7 +210,20 @@ Schema options:
|
|||||||
| Export | Purpose |
|
| Export | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
| `buildUserPrompt(options)` | Build a user prompt with optional spec refinement and state context |
|
| `buildUserPrompt(options)` | Build a user prompt with optional spec refinement and state context |
|
||||||
|
| `buildEditUserPrompt(options)` | Build a user prompt for editing an existing spec (used internally by `buildUserPrompt`) |
|
||||||
|
| `buildEditInstructions(config, format)` | Generate the prompt section describing available edit modes |
|
||||||
|
| `isNonEmptySpec(spec)` | Check whether a spec has a root and at least one element |
|
||||||
| `UserPromptOptions` | Options type for `buildUserPrompt` |
|
| `UserPromptOptions` | Options type for `buildUserPrompt` |
|
||||||
|
| `EditMode` | `"patch" \| "merge" \| "diff"` |
|
||||||
|
| `EditConfig` | Configuration for edit modes (`{ modes: EditMode[] }`) |
|
||||||
|
| `BuildEditUserPromptOptions` | Options type for `buildEditUserPrompt` |
|
||||||
|
|
||||||
|
### Merge and Diff
|
||||||
|
|
||||||
|
| Export | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `deepMergeSpec(base, patch)` | RFC 7396 deep merge (null deletes, arrays replace, objects recurse) |
|
||||||
|
| `diffToPatches(oldObj, newObj)` | Generate RFC 6902 JSON Patch operations from object diff |
|
||||||
|
|
||||||
### Spec Validation
|
### Spec Validation
|
||||||
|
|
||||||
@@ -491,10 +504,11 @@ import { buildUserPrompt } from "@json-render/core";
|
|||||||
// Fresh generation
|
// Fresh generation
|
||||||
const prompt = buildUserPrompt({ prompt: "create a todo app" });
|
const prompt = buildUserPrompt({ prompt: "create a todo app" });
|
||||||
|
|
||||||
// Refinement with existing spec (triggers patch-only mode)
|
// Refinement with edit modes (default: patch-only)
|
||||||
const refinementPrompt = buildUserPrompt({
|
const refinementPrompt = buildUserPrompt({
|
||||||
prompt: "add a dark mode toggle",
|
prompt: "add a dark mode toggle",
|
||||||
currentSpec: existingSpec,
|
currentSpec: existingSpec,
|
||||||
|
editModes: ["patch", "merge"],
|
||||||
});
|
});
|
||||||
|
|
||||||
// With runtime state context
|
// With runtime state context
|
||||||
@@ -504,6 +518,27 @@ const contextPrompt = buildUserPrompt({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When `currentSpec` is provided, the prompt instructs the AI to use the specified edit modes. Available modes:
|
||||||
|
|
||||||
|
- **`"patch"`** — RFC 6902 JSON Patch. One operation per line. Best for precise, targeted single-field updates.
|
||||||
|
- **`"merge"`** — RFC 7396 JSON Merge Patch. Partial object deep-merged; `null` deletes. Best for structural changes.
|
||||||
|
- **`"diff"`** — Unified diff against the serialized spec. Best for small text-level changes.
|
||||||
|
|
||||||
|
## Deep Merge and Diff
|
||||||
|
|
||||||
|
Format-agnostic utilities for working with specs:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { deepMergeSpec, diffToPatches } from "@json-render/core";
|
||||||
|
|
||||||
|
// RFC 7396 deep merge: null deletes, arrays replace, objects recurse
|
||||||
|
const merged = deepMergeSpec(baseSpec, { elements: { main: { props: { title: "New" } } } });
|
||||||
|
|
||||||
|
// RFC 6902 diff: generate JSON Patch operations from two objects
|
||||||
|
const patches = diffToPatches(oldSpec, newSpec);
|
||||||
|
// [{ op: "replace", path: "/elements/main/props/title", value: "New" }]
|
||||||
|
```
|
||||||
|
|
||||||
## Spec Validation
|
## Spec Validation
|
||||||
|
|
||||||
Validate spec structure and auto-fix common issues:
|
Validate spec structure and auto-fix common issues:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/core",
|
"name": "@json-render/core",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
|
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import type { JsonPatch } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape a single JSON Pointer token per RFC 6901.
|
||||||
|
* `~` → `~0`, `/` → `~1`.
|
||||||
|
*/
|
||||||
|
function escapeToken(token: string): string {
|
||||||
|
return token.replace(/~/g, "~0").replace(/\//g, "~1");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPath(basePath: string, key: string): string {
|
||||||
|
return `${basePath}/${escapeToken(key)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shallow equality for arrays — used to avoid emitting patches when the
|
||||||
|
* children list hasn't actually changed.
|
||||||
|
*/
|
||||||
|
function arraysEqual(a: unknown[], b: unknown[]): boolean {
|
||||||
|
if (a.length !== b.length) return false;
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
if (a[i] !== b[i]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produce RFC 6902 JSON Patch operations that transform `oldObj` into `newObj`.
|
||||||
|
*
|
||||||
|
* - New keys → `add`
|
||||||
|
* - Changed scalar/array values → `replace`
|
||||||
|
* - Removed keys → `remove`
|
||||||
|
* - Arrays are compared shallowly and replaced atomically (not element-diffed)
|
||||||
|
* - Plain objects recurse
|
||||||
|
*/
|
||||||
|
export function diffToPatches(
|
||||||
|
oldObj: Record<string, unknown>,
|
||||||
|
newObj: Record<string, unknown>,
|
||||||
|
basePath = "",
|
||||||
|
): JsonPatch[] {
|
||||||
|
const patches: JsonPatch[] = [];
|
||||||
|
|
||||||
|
// Keys present in newObj
|
||||||
|
for (const key of Object.keys(newObj)) {
|
||||||
|
const path = buildPath(basePath, key);
|
||||||
|
const oldVal = oldObj[key];
|
||||||
|
const newVal = newObj[key];
|
||||||
|
|
||||||
|
if (!(key in oldObj)) {
|
||||||
|
patches.push({ op: "add", path, value: newVal });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both exist — compare
|
||||||
|
if (isPlainObject(oldVal) && isPlainObject(newVal)) {
|
||||||
|
patches.push(...diffToPatches(oldVal, newVal, path));
|
||||||
|
} else if (Array.isArray(oldVal) && Array.isArray(newVal)) {
|
||||||
|
if (!arraysEqual(oldVal, newVal)) {
|
||||||
|
patches.push({ op: "replace", path, value: newVal });
|
||||||
|
}
|
||||||
|
} else if (oldVal !== newVal) {
|
||||||
|
patches.push({ op: "replace", path, value: newVal });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys removed from oldObj
|
||||||
|
for (const key of Object.keys(oldObj)) {
|
||||||
|
if (!(key in newObj)) {
|
||||||
|
patches.push({ op: "remove", path: buildPath(basePath, key) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return patches;
|
||||||
|
}
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
import type { Spec } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit mode for modifying an existing spec.
|
||||||
|
*
|
||||||
|
* - `"patch"` — RFC 6902 JSON Patch. One operation per line.
|
||||||
|
* - `"merge"` — RFC 7396 JSON Merge Patch. Partial object deep-merged; `null` deletes.
|
||||||
|
* - `"diff"` — Unified diff (POSIX). Line-level text edits against the serialized spec.
|
||||||
|
*/
|
||||||
|
export type EditMode = "patch" | "merge" | "diff";
|
||||||
|
|
||||||
|
export interface EditConfig {
|
||||||
|
/** Which edit modes are enabled. When >1, the AI chooses per edit. */
|
||||||
|
modes: EditMode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_MODES: EditMode[] = ["patch"];
|
||||||
|
|
||||||
|
function normalizeModes(config?: EditConfig): EditMode[] {
|
||||||
|
if (!config?.modes?.length) return DEFAULT_MODES;
|
||||||
|
return config.modes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JSON-format instructions ──
|
||||||
|
|
||||||
|
function jsonPatchInstructions(): string {
|
||||||
|
return [
|
||||||
|
"PATCH MODE (RFC 6902 JSON Patch):",
|
||||||
|
"Output one JSON object per line. Each line is a patch operation.",
|
||||||
|
'- Add: {"op":"add","path":"/elements/new-key","value":{...}}',
|
||||||
|
'- Replace: {"op":"replace","path":"/elements/existing-key","value":{...}}',
|
||||||
|
'- Remove: {"op":"remove","path":"/elements/old-key"}',
|
||||||
|
"Only output patches for what needs to change.",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonMergeInstructions(): string {
|
||||||
|
return [
|
||||||
|
"MERGE MODE (RFC 7396 JSON Merge Patch):",
|
||||||
|
"Output a single JSON object on one line with __json_edit set to true.",
|
||||||
|
"Include only the keys that changed. Unmentioned keys are preserved.",
|
||||||
|
"Set a key to null to delete it.",
|
||||||
|
"",
|
||||||
|
"Example (update a title and add an element):",
|
||||||
|
'{"__json_edit":true,"elements":{"main":{"props":{"title":"New Title"}},"new-el":{"type":"Card","props":{},"children":[]}}}',
|
||||||
|
"",
|
||||||
|
"Example (delete an element):",
|
||||||
|
'{"__json_edit":true,"elements":{"old-widget":null}}',
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonDiffInstructions(): string {
|
||||||
|
return [
|
||||||
|
"DIFF MODE (unified diff):",
|
||||||
|
"Output a unified diff inside a ```diff code fence.",
|
||||||
|
"The diff applies against the JSON-serialized current spec.",
|
||||||
|
"",
|
||||||
|
"Example:",
|
||||||
|
"```diff",
|
||||||
|
"--- a/spec.json",
|
||||||
|
"+++ b/spec.json",
|
||||||
|
"@@ -3,1 +3,1 @@",
|
||||||
|
'- "title": "Login"',
|
||||||
|
'+ "title": "Welcome Back"',
|
||||||
|
"```",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── YAML-format instructions ──
|
||||||
|
|
||||||
|
function yamlPatchInstructions(): string {
|
||||||
|
return [
|
||||||
|
"PATCH MODE (RFC 6902 JSON Patch):",
|
||||||
|
"Output RFC 6902 JSON Patch lines inside a ```yaml-patch code fence.",
|
||||||
|
"Each line is one JSON patch operation.",
|
||||||
|
"",
|
||||||
|
"Example:",
|
||||||
|
"```yaml-patch",
|
||||||
|
'{"op":"replace","path":"/elements/main/props/title","value":"New Title"}',
|
||||||
|
'{"op":"add","path":"/elements/new-el","value":{"type":"Card","props":{},"children":[]}}',
|
||||||
|
"```",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function yamlMergeInstructions(): string {
|
||||||
|
return [
|
||||||
|
"MERGE MODE (RFC 7396 JSON Merge Patch):",
|
||||||
|
"Output only the changed parts in a ```yaml-edit code fence.",
|
||||||
|
"Uses deep merge semantics: only keys you include are updated. Unmentioned elements and props are preserved.",
|
||||||
|
"Set a key to null to delete it.",
|
||||||
|
"",
|
||||||
|
"Example edit (update title, add a new element):",
|
||||||
|
"```yaml-edit",
|
||||||
|
"elements:",
|
||||||
|
" main:",
|
||||||
|
" props:",
|
||||||
|
" title: Updated Title",
|
||||||
|
" new-chart:",
|
||||||
|
" type: Card",
|
||||||
|
" props: {}",
|
||||||
|
" children: []",
|
||||||
|
"```",
|
||||||
|
"",
|
||||||
|
"Example deletion:",
|
||||||
|
"```yaml-edit",
|
||||||
|
"elements:",
|
||||||
|
" old-widget: null",
|
||||||
|
"```",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function yamlDiffInstructions(): string {
|
||||||
|
return [
|
||||||
|
"DIFF MODE (unified diff):",
|
||||||
|
"Output a unified diff inside a ```diff code fence.",
|
||||||
|
"The diff applies against the YAML-serialized current spec.",
|
||||||
|
"",
|
||||||
|
"Example:",
|
||||||
|
"```diff",
|
||||||
|
"--- a/spec.yaml",
|
||||||
|
"+++ b/spec.yaml",
|
||||||
|
"@@ -6,1 +6,1 @@",
|
||||||
|
"- title: Login",
|
||||||
|
"+ title: Welcome Back",
|
||||||
|
"```",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mode selection guidance ──
|
||||||
|
|
||||||
|
function modeSelectionGuidance(modes: EditMode[]): string {
|
||||||
|
if (modes.length === 1) return "";
|
||||||
|
const parts = ["Choose the best edit strategy for the requested change:"];
|
||||||
|
if (modes.includes("patch")) {
|
||||||
|
parts.push("- PATCH: best for precise, targeted single-field updates");
|
||||||
|
}
|
||||||
|
if (modes.includes("merge")) {
|
||||||
|
parts.push(
|
||||||
|
"- MERGE: best for structural changes (add/remove elements, reparent children, update multiple props at once)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (modes.includes("diff")) {
|
||||||
|
parts.push(
|
||||||
|
"- DIFF: best for small text-level changes when you can see the exact lines to change",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return parts.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the prompt section describing available edit modes.
|
||||||
|
* Only documents the modes that are enabled.
|
||||||
|
*/
|
||||||
|
export function buildEditInstructions(
|
||||||
|
config: EditConfig | undefined,
|
||||||
|
format: "json" | "yaml",
|
||||||
|
): string {
|
||||||
|
const modes = normalizeModes(config);
|
||||||
|
const sections: string[] = [];
|
||||||
|
|
||||||
|
sections.push("EDITING EXISTING SPECS:");
|
||||||
|
sections.push("");
|
||||||
|
|
||||||
|
const guidance = modeSelectionGuidance(modes);
|
||||||
|
if (guidance) {
|
||||||
|
sections.push(guidance);
|
||||||
|
sections.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mode of modes) {
|
||||||
|
if (format === "json") {
|
||||||
|
switch (mode) {
|
||||||
|
case "patch":
|
||||||
|
sections.push(jsonPatchInstructions());
|
||||||
|
break;
|
||||||
|
case "merge":
|
||||||
|
sections.push(jsonMergeInstructions());
|
||||||
|
break;
|
||||||
|
case "diff":
|
||||||
|
sections.push(jsonDiffInstructions());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (mode) {
|
||||||
|
case "patch":
|
||||||
|
sections.push(yamlPatchInstructions());
|
||||||
|
break;
|
||||||
|
case "merge":
|
||||||
|
sections.push(yamlMergeInstructions());
|
||||||
|
break;
|
||||||
|
case "diff":
|
||||||
|
sections.push(yamlDiffInstructions());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sections.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLineNumbers(text: string): string {
|
||||||
|
const lines = text.split("\n");
|
||||||
|
const width = String(lines.length).length;
|
||||||
|
return lines
|
||||||
|
.map((line, i) => `${String(i + 1).padStart(width)}| ${line}`)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNonEmptySpec(spec: unknown): spec is Spec {
|
||||||
|
if (!spec || typeof spec !== "object") return false;
|
||||||
|
const s = spec as Record<string, unknown>;
|
||||||
|
return (
|
||||||
|
typeof s.root === "string" &&
|
||||||
|
typeof s.elements === "object" &&
|
||||||
|
s.elements !== null &&
|
||||||
|
Object.keys(s.elements as object).length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildEditUserPromptOptions {
|
||||||
|
prompt: string;
|
||||||
|
currentSpec?: Spec | null;
|
||||||
|
config?: EditConfig;
|
||||||
|
format: "json" | "yaml";
|
||||||
|
maxPromptLength?: number;
|
||||||
|
/** Serialise the spec. Defaults to JSON.stringify for json, must be provided for yaml. */
|
||||||
|
serializer?: (spec: Spec) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the user prompt for edits, including the current spec
|
||||||
|
* (with line numbers when diff mode is enabled) and mode instructions.
|
||||||
|
*/
|
||||||
|
export function buildEditUserPrompt(
|
||||||
|
options: BuildEditUserPromptOptions,
|
||||||
|
): string {
|
||||||
|
const { prompt, currentSpec, config, format, maxPromptLength, serializer } =
|
||||||
|
options;
|
||||||
|
|
||||||
|
let userText = String(prompt || "");
|
||||||
|
if (maxPromptLength !== undefined && maxPromptLength > 0) {
|
||||||
|
userText = userText.slice(0, maxPromptLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNonEmptySpec(currentSpec)) {
|
||||||
|
return userText;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modes = normalizeModes(config);
|
||||||
|
const showLineNumbers = modes.includes("diff");
|
||||||
|
|
||||||
|
const serialize = serializer ?? ((s: Spec) => JSON.stringify(s, null, 2));
|
||||||
|
const specText = serialize(currentSpec);
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (showLineNumbers) {
|
||||||
|
parts.push("CURRENT UI STATE (line numbers for reference):");
|
||||||
|
parts.push("```");
|
||||||
|
parts.push(addLineNumbers(specText));
|
||||||
|
parts.push("```");
|
||||||
|
} else {
|
||||||
|
parts.push(
|
||||||
|
"CURRENT UI STATE (already loaded, DO NOT recreate existing elements):",
|
||||||
|
);
|
||||||
|
parts.push("```");
|
||||||
|
parts.push(specText);
|
||||||
|
parts.push("```");
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.push("");
|
||||||
|
parts.push(`USER REQUEST: ${userText}`);
|
||||||
|
parts.push("");
|
||||||
|
|
||||||
|
if (modes.length === 1) {
|
||||||
|
const mode = modes[0]!;
|
||||||
|
switch (mode) {
|
||||||
|
case "patch":
|
||||||
|
parts.push(
|
||||||
|
format === "yaml"
|
||||||
|
? "Output ONLY the patches in a ```yaml-patch fence."
|
||||||
|
: "Output ONLY the JSON Patch lines needed for the change.",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "merge":
|
||||||
|
parts.push(
|
||||||
|
format === "yaml"
|
||||||
|
? "Output ONLY the changes in a ```yaml-edit fence. Include only keys that need to change."
|
||||||
|
: "Output ONLY a single JSON merge line with __json_edit set to true. Include only keys that need to change.",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "diff":
|
||||||
|
parts.push("Output ONLY the unified diff in a ```diff fence.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const modeNames = modes.map((m) => {
|
||||||
|
switch (m) {
|
||||||
|
case "patch":
|
||||||
|
return format === "yaml" ? "```yaml-patch fence" : "JSON Patch lines";
|
||||||
|
case "merge":
|
||||||
|
return format === "yaml"
|
||||||
|
? "```yaml-edit fence"
|
||||||
|
: "JSON merge line (__json_edit)";
|
||||||
|
case "diff":
|
||||||
|
return "```diff fence";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parts.push(
|
||||||
|
`Choose the best edit strategy and output using one of: ${modeNames.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join("\n");
|
||||||
|
}
|
||||||
@@ -178,3 +178,19 @@ export { defineCatalog } from "./schema";
|
|||||||
export type { UserPromptOptions } from "./prompt";
|
export type { UserPromptOptions } from "./prompt";
|
||||||
|
|
||||||
export { buildUserPrompt } from "./prompt";
|
export { buildUserPrompt } from "./prompt";
|
||||||
|
|
||||||
|
// Object diff & merge (format-agnostic)
|
||||||
|
export { deepMergeSpec } from "./merge";
|
||||||
|
export { diffToPatches } from "./diff";
|
||||||
|
|
||||||
|
// Edit modes
|
||||||
|
export type {
|
||||||
|
EditMode,
|
||||||
|
EditConfig,
|
||||||
|
BuildEditUserPromptOptions,
|
||||||
|
} from "./edit-modes";
|
||||||
|
export {
|
||||||
|
buildEditInstructions,
|
||||||
|
buildEditUserPrompt,
|
||||||
|
isNonEmptySpec,
|
||||||
|
} from "./edit-modes";
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep-merge `patch` into `base`, returning a new object.
|
||||||
|
*
|
||||||
|
* Semantics (RFC 7396 JSON Merge Patch):
|
||||||
|
* - `null` values in `patch` delete the corresponding key from `base`
|
||||||
|
* - Arrays in `patch` replace (not concat) the corresponding array in `base`
|
||||||
|
* - Plain objects recurse
|
||||||
|
* - All other values replace
|
||||||
|
*
|
||||||
|
* Neither `base` nor `patch` is mutated.
|
||||||
|
*/
|
||||||
|
export function deepMergeSpec(
|
||||||
|
base: Record<string, unknown>,
|
||||||
|
patch: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const result: Record<string, unknown> = { ...base };
|
||||||
|
|
||||||
|
for (const key of Object.keys(patch)) {
|
||||||
|
const patchVal = patch[key];
|
||||||
|
|
||||||
|
// null → delete
|
||||||
|
if (patchVal === null) {
|
||||||
|
delete result[key];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseVal = result[key];
|
||||||
|
|
||||||
|
if (isPlainObject(patchVal) && isPlainObject(baseVal)) {
|
||||||
|
result[key] = deepMergeSpec(baseVal, patchVal);
|
||||||
|
} else {
|
||||||
|
result[key] = patchVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
+16
-41
@@ -1,4 +1,6 @@
|
|||||||
import type { Spec } from "./types";
|
import type { Spec } from "./types";
|
||||||
|
import type { EditMode } from "./edit-modes";
|
||||||
|
import { buildEditUserPrompt, isNonEmptySpec } from "./edit-modes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for building a user prompt.
|
* Options for building a user prompt.
|
||||||
@@ -12,37 +14,16 @@ export interface UserPromptOptions {
|
|||||||
state?: Record<string, unknown> | null;
|
state?: Record<string, unknown> | null;
|
||||||
/** Maximum length for the user's text prompt (applied before wrapping) */
|
/** Maximum length for the user's text prompt (applied before wrapping) */
|
||||||
maxPromptLength?: number;
|
maxPromptLength?: number;
|
||||||
|
/** Edit modes to offer when refining an existing spec. Default: `["patch"]`. */
|
||||||
|
editModes?: EditMode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether a spec is non-empty (has a root and at least one element).
|
|
||||||
*/
|
|
||||||
function isNonEmptySpec(spec: unknown): spec is Spec {
|
|
||||||
if (!spec || typeof spec !== "object") return false;
|
|
||||||
const s = spec as Record<string, unknown>;
|
|
||||||
return (
|
|
||||||
typeof s.root === "string" &&
|
|
||||||
typeof s.elements === "object" &&
|
|
||||||
s.elements !== null &&
|
|
||||||
Object.keys(s.elements as object).length > 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const PATCH_INSTRUCTIONS = `IMPORTANT: The current UI is already loaded. Output ONLY the patches needed to make the requested change:
|
|
||||||
- To add a new element: {"op":"add","path":"/elements/new-key","value":{...}}
|
|
||||||
- To modify an existing element: {"op":"replace","path":"/elements/existing-key","value":{...}}
|
|
||||||
- To remove an element: {"op":"remove","path":"/elements/old-key"}
|
|
||||||
- To update the root: {"op":"replace","path":"/root","value":"new-root-key"}
|
|
||||||
- To add children: update the parent element with new children array
|
|
||||||
|
|
||||||
DO NOT output patches for elements that don't need to change. Only output what's necessary for the requested modification.`;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a user prompt for AI generation.
|
* Build a user prompt for AI generation.
|
||||||
*
|
*
|
||||||
* Handles common patterns that every consuming app needs:
|
* Handles common patterns that every consuming app needs:
|
||||||
* - Truncating the user's prompt to a max length
|
* - Truncating the user's prompt to a max length
|
||||||
* - Including the current spec for refinement (patch-only mode)
|
* - Including the current spec for refinement (edit mode)
|
||||||
* - Including runtime state context
|
* - Including runtime state context
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@@ -53,12 +34,12 @@ DO NOT output patches for elements that don't need to change. Only output what's
|
|||||||
* // Refinement with existing spec
|
* // Refinement with existing spec
|
||||||
* buildUserPrompt({ prompt: "add a dark mode toggle", currentSpec: spec })
|
* buildUserPrompt({ prompt: "add a dark mode toggle", currentSpec: spec })
|
||||||
*
|
*
|
||||||
* // With state context
|
* // With multiple edit modes
|
||||||
* buildUserPrompt({ prompt: "show my data", state: { todos: [] } })
|
* buildUserPrompt({ prompt: "change title", currentSpec: spec, editModes: ["patch", "merge"] })
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function buildUserPrompt(options: UserPromptOptions): string {
|
export function buildUserPrompt(options: UserPromptOptions): string {
|
||||||
const { prompt, currentSpec, state, maxPromptLength } = options;
|
const { prompt, currentSpec, state, maxPromptLength, editModes } = options;
|
||||||
|
|
||||||
// Sanitize and optionally truncate the user's text
|
// Sanitize and optionally truncate the user's text
|
||||||
let userText = String(prompt || "");
|
let userText = String(prompt || "");
|
||||||
@@ -68,25 +49,19 @@ export function buildUserPrompt(options: UserPromptOptions): string {
|
|||||||
|
|
||||||
// --- Refinement mode: currentSpec is provided ---
|
// --- Refinement mode: currentSpec is provided ---
|
||||||
if (isNonEmptySpec(currentSpec)) {
|
if (isNonEmptySpec(currentSpec)) {
|
||||||
const parts: string[] = [];
|
const editPrompt = buildEditUserPrompt({
|
||||||
|
prompt: userText,
|
||||||
parts.push(
|
currentSpec,
|
||||||
`CURRENT UI STATE (already loaded, DO NOT recreate existing elements):`,
|
config: { modes: editModes ?? ["patch"] },
|
||||||
);
|
format: "json",
|
||||||
parts.push(JSON.stringify(currentSpec, null, 2));
|
});
|
||||||
parts.push("");
|
|
||||||
parts.push(`USER REQUEST: ${userText}`);
|
|
||||||
|
|
||||||
// Append state context if provided
|
// Append state context if provided
|
||||||
if (state && Object.keys(state).length > 0) {
|
if (state && Object.keys(state).length > 0) {
|
||||||
parts.push("");
|
return `${editPrompt}\n\nAVAILABLE STATE:\n${JSON.stringify(state, null, 2)}`;
|
||||||
parts.push(`AVAILABLE STATE:\n${JSON.stringify(state, null, 2)}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push("");
|
return editPrompt;
|
||||||
parts.push(PATCH_INSTRUCTIONS);
|
|
||||||
|
|
||||||
return parts.join("\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Fresh generation mode ---
|
// --- Fresh generation mode ---
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import type { EditMode } from "./edit-modes";
|
||||||
|
import { buildEditInstructions } from "./edit-modes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema builder primitives
|
* Schema builder primitives
|
||||||
@@ -142,6 +144,8 @@ export interface PromptOptions {
|
|||||||
* @deprecated `"chat"` — use `"inline"` instead.
|
* @deprecated `"chat"` — use `"inline"` instead.
|
||||||
*/
|
*/
|
||||||
mode?: "standalone" | "inline" | "generate" | "chat";
|
mode?: "standalone" | "inline" | "generate" | "chat";
|
||||||
|
/** Edit modes to document in the system prompt. Default: `["patch"]`. */
|
||||||
|
editModes?: EditMode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1049,6 +1053,12 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|||||||
lines.push("");
|
lines.push("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edit modes
|
||||||
|
const editModes = options.editModes;
|
||||||
|
if (editModes && editModes.length > 0) {
|
||||||
|
lines.push(buildEditInstructions({ modes: editModes }, "json"));
|
||||||
|
}
|
||||||
|
|
||||||
// Rules
|
// Rules
|
||||||
lines.push("RULES:");
|
lines.push("RULES:");
|
||||||
const baseRules =
|
const baseRules =
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/image
|
# @json-render/image
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/image",
|
"name": "@json-render/image",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.",
|
"description": "Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/jotai
|
# @json-render/jotai
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/jotai",
|
"name": "@json-render/jotai",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Jotai adapter for json-render StateStore",
|
"description": "Jotai adapter for json-render StateStore",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/mcp
|
# @json-render/mcp
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/mcp",
|
"name": "@json-render/mcp",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "MCP Apps integration for @json-render/core. Serve json-render UIs as interactive MCP Apps in Claude, ChatGPT, Cursor, and VS Code.",
|
"description": "MCP Apps integration for @json-render/core. Serve json-render UIs as interactive MCP Apps in Claude, ChatGPT, Cursor, and VS Code.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/react-email
|
# @json-render/react-email
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/react-email",
|
"name": "@json-render/react-email",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "React Email renderer for @json-render/core. JSON becomes HTML emails.",
|
"description": "React Email renderer for @json-render/core. JSON becomes HTML emails.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/react-native
|
# @json-render/react-native
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/react-native",
|
"name": "@json-render/react-native",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "React Native renderer for @json-render/core. JSON becomes React Native components.",
|
"description": "React Native renderer for @json-render/core. JSON becomes React Native components.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/react-pdf
|
# @json-render/react-pdf
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/react-pdf",
|
"name": "@json-render/react-pdf",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "React PDF renderer for @json-render/core. JSON becomes PDF documents.",
|
"description": "React PDF renderer for @json-render/core. JSON becomes PDF documents.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @internal/react-state
|
# @internal/react-state
|
||||||
|
|
||||||
|
## 0.8.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
|
||||||
## 0.8.7
|
## 0.8.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@internal/react-state",
|
"name": "@internal/react-state",
|
||||||
"version": "0.8.7",
|
"version": "0.8.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Shared React state context for json-render renderer packages",
|
"description": "Shared React state context for json-render renderer packages",
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# @json-render/react-three-fiber
|
# @json-render/react-three-fiber
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/react@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/react-three-fiber",
|
"name": "@json-render/react-three-fiber",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "React Three Fiber renderer for @json-render/core. JSON becomes 3D scenes.",
|
"description": "React Three Fiber renderer for @json-render/core. JSON becomes 3D scenes.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/react
|
# @json-render/react
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/react",
|
"name": "@json-render/react",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "React renderer for @json-render/core. JSON becomes React components.",
|
"description": "React renderer for @json-render/core. JSON becomes React components.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/redux
|
# @json-render/redux
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/redux",
|
"name": "@json-render/redux",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Redux adapter for json-render StateStore",
|
"description": "Redux adapter for json-render StateStore",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/remotion
|
# @json-render/remotion
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/remotion",
|
"name": "@json-render/remotion",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Remotion renderer for @json-render/core. JSON becomes video compositions.",
|
"description": "Remotion renderer for @json-render/core. JSON becomes video compositions.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# @json-render/shadcn
|
# @json-render/shadcn
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/react@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/shadcn",
|
"name": "@json-render/shadcn",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "shadcn/ui component library for @json-render/core. JSON becomes beautiful Tailwind-styled React components.",
|
"description": "shadcn/ui component library for @json-render/core. JSON becomes beautiful Tailwind-styled React components.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/solid
|
# @json-render/solid
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/solid",
|
"name": "@json-render/solid",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "SolidJS renderer for @json-render/core. JSON becomes Solid components.",
|
"description": "SolidJS renderer for @json-render/core. JSON becomes Solid components.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/svelte
|
# @json-render/svelte
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/svelte",
|
"name": "@json-render/svelte",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Svelte 5 renderer for @json-render/core. JSON becomes Svelte components.",
|
"description": "Svelte 5 renderer for @json-render/core. JSON becomes Svelte components.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/vue
|
# @json-render/vue
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/vue",
|
"name": "@json-render/vue",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Vue renderer for @json-render/core. JSON becomes Vue components.",
|
"description": "Vue renderer for @json-render/core. JSON becomes Vue components.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# @json-render/xstate
|
# @json-render/xstate
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
|
||||||
## 0.13.0
|
## 0.13.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/xstate",
|
"name": "@json-render/xstate",
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "XState Store adapter for json-render StateStore",
|
"description": "XState Store adapter for json-render StateStore",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# @json-render/yaml
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- a8afd8b: Add YAML wire format package and universal edit modes for surgical spec refinement.
|
||||||
|
|
||||||
|
### New:
|
||||||
|
- **`@json-render/yaml`** -- YAML wire format for json-render. Includes streaming YAML parser, `yamlPrompt()` for system prompts, and AI SDK transform (`pipeYamlRender`) as a drop-in alternative to JSONL streaming. Supports four fence types: `yaml-spec`, `yaml-edit`, `yaml-patch`, and `diff`.
|
||||||
|
- **Universal edit modes** in `@json-render/core` -- three strategies for multi-turn spec refinement: `patch` (RFC 6902), `merge` (RFC 7396), and `diff` (unified diff). New `editModes` option on `buildUserPrompt()` and `PromptOptions`. New helpers: `deepMergeSpec()`, `diffToPatches()`, `buildEditUserPrompt()`, `buildEditInstructions()`, `isNonEmptySpec()`.
|
||||||
|
|
||||||
|
### Improved:
|
||||||
|
- **Playground** -- format toggle (JSONL / YAML), edit mode picker (patch / merge / diff), and token usage display with prompt caching stats.
|
||||||
|
- **Prompt caching** -- generate API uses Anthropic ephemeral cache control for system prompts.
|
||||||
|
- **CI** -- lint, type-check, and test jobs now run in parallel.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# @json-render/yaml
|
||||||
|
|
||||||
|
YAML wire format for `@json-render/core`. Progressive rendering and surgical edits via streaming YAML.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/yaml @json-render/core yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Concepts
|
||||||
|
|
||||||
|
- **YAML wire format**: Uses `yaml-spec`, `yaml-edit`, `yaml-patch`, and `diff` code fences instead of JSONL
|
||||||
|
- **Streaming parser**: Incrementally parses YAML as it arrives, emitting JSON Patch operations
|
||||||
|
- **Edit modes**: Supports patch (RFC 6902), merge (RFC 7396), and unified diff for surgical edits
|
||||||
|
- **AI SDK transform**: Drop-in `TransformStream` that converts YAML fences into json-render patch data parts
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Generate a YAML System Prompt
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { yamlPrompt } from "@json-render/yaml";
|
||||||
|
import { catalog } from "./catalog";
|
||||||
|
|
||||||
|
const systemPrompt = yamlPrompt(catalog, {
|
||||||
|
mode: "standalone",
|
||||||
|
editModes: ["merge"],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stream YAML Specs (AI SDK)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { pipeYamlRender } from "@json-render/yaml";
|
||||||
|
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
|
||||||
|
|
||||||
|
const stream = createUIMessageStream({
|
||||||
|
execute: async ({ writer }) => {
|
||||||
|
writer.merge(pipeYamlRender(result.toUIMessageStream()));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return createUIMessageStreamResponse({ stream });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Streaming Parser (Low-Level)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createYamlStreamCompiler } from "@json-render/yaml";
|
||||||
|
|
||||||
|
const compiler = createYamlStreamCompiler<Spec>();
|
||||||
|
|
||||||
|
// Feed chunks as they arrive
|
||||||
|
const { result, newPatches } = compiler.push("root: main\n");
|
||||||
|
compiler.push("elements:\n main:\n type: Card\n");
|
||||||
|
|
||||||
|
// Flush remaining data
|
||||||
|
const { result: final } = compiler.flush();
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### `yamlPrompt(catalog, options?)`
|
||||||
|
|
||||||
|
Generate a YAML-format system prompt from any json-render catalog.
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `system` | `string` | `"You are a UI generator that outputs YAML."` | Custom system message intro |
|
||||||
|
| `mode` | `"standalone" \| "inline"` | `"standalone"` | Output mode |
|
||||||
|
| `customRules` | `string[]` | `[]` | Additional rules |
|
||||||
|
| `editModes` | `EditMode[]` | `["merge"]` | Edit modes to document |
|
||||||
|
|
||||||
|
### `createYamlTransform(options?)`
|
||||||
|
|
||||||
|
Creates a `TransformStream` that converts YAML spec/edit fences in AI SDK stream chunks into json-render patch data parts.
|
||||||
|
|
||||||
|
| Option | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `previousSpec` | `Spec` | Seed with a previous spec for multi-turn edit support |
|
||||||
|
|
||||||
|
### `pipeYamlRender(stream, options?)`
|
||||||
|
|
||||||
|
Convenience wrapper that pipes an AI SDK stream through the YAML transform. Drop-in replacement for `pipeJsonRender` from `@json-render/core`.
|
||||||
|
|
||||||
|
### `createYamlStreamCompiler(initial?)`
|
||||||
|
|
||||||
|
Create a streaming YAML compiler that incrementally parses YAML text and emits JSON Patch operations.
|
||||||
|
|
||||||
|
**Returns** `YamlStreamCompiler<T>` with methods:
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `push(chunk)` | Push a chunk of text. Returns `{ result, newPatches }` |
|
||||||
|
| `flush()` | Flush remaining buffer and return final result |
|
||||||
|
| `getResult()` | Get the current compiled result |
|
||||||
|
| `getPatches()` | Get all patches applied so far |
|
||||||
|
| `reset(initial?)` | Reset to initial state |
|
||||||
|
|
||||||
|
### Fence Constants
|
||||||
|
|
||||||
|
Exported constants for fence detection:
|
||||||
|
|
||||||
|
- `YAML_SPEC_FENCE` — `` ```yaml-spec ``
|
||||||
|
- `YAML_EDIT_FENCE` — `` ```yaml-edit ``
|
||||||
|
- `YAML_PATCH_FENCE` — `` ```yaml-patch ``
|
||||||
|
- `DIFF_FENCE` — `` ```diff ``
|
||||||
|
- `FENCE_CLOSE` — `` ``` ``
|
||||||
|
|
||||||
|
### Re-exports from `@json-render/core`
|
||||||
|
|
||||||
|
- `diffToPatches(oldObj, newObj)` — Generate RFC 6902 JSON Patch from object diff
|
||||||
|
- `deepMergeSpec(base, patch)` — RFC 7396 JSON Merge Patch
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "@json-render/yaml",
|
||||||
|
"version": "0.14.0",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"description": "YAML wire format for @json-render/core. Progressive rendering and surgical edits via streaming YAML.",
|
||||||
|
"keywords": [
|
||||||
|
"json",
|
||||||
|
"yaml",
|
||||||
|
"ui",
|
||||||
|
"ai",
|
||||||
|
"generative-ui",
|
||||||
|
"llm",
|
||||||
|
"streaming",
|
||||||
|
"progressive-rendering"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||||
|
"directory": "packages/yaml"
|
||||||
|
},
|
||||||
|
"homepage": "https://json-render.dev",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"dev": "tsup --watch",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "vitest run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@json-render/core": "workspace:*",
|
||||||
|
"diff": "^8.0.3",
|
||||||
|
"yaml": "^2.8.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@internal/typescript-config": "workspace:*",
|
||||||
|
"tsup": "^8.0.2",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"vitest": "^4.0.17",
|
||||||
|
"zod": "^4.3.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { diffToPatches } from "./diff";
|
||||||
|
|
||||||
|
describe("diffToPatches", () => {
|
||||||
|
it("returns empty array for identical objects", () => {
|
||||||
|
const obj = { a: 1, b: "hello" };
|
||||||
|
expect(diffToPatches(obj, obj)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects added keys", () => {
|
||||||
|
const patches = diffToPatches({}, { name: "Alice" });
|
||||||
|
expect(patches).toEqual([{ op: "add", path: "/name", value: "Alice" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects removed keys", () => {
|
||||||
|
const patches = diffToPatches({ name: "Alice" }, {});
|
||||||
|
expect(patches).toEqual([{ op: "remove", path: "/name" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects changed scalar values", () => {
|
||||||
|
const patches = diffToPatches({ name: "Alice" }, { name: "Bob" });
|
||||||
|
expect(patches).toEqual([{ op: "replace", path: "/name", value: "Bob" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recurses into nested objects", () => {
|
||||||
|
const oldObj = { user: { name: "Alice", age: 30 } };
|
||||||
|
const newObj = { user: { name: "Alice", age: 31 } };
|
||||||
|
const patches = diffToPatches(oldObj, newObj);
|
||||||
|
expect(patches).toEqual([{ op: "replace", path: "/user/age", value: 31 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replaces arrays atomically", () => {
|
||||||
|
const oldObj = { items: ["a", "b"] };
|
||||||
|
const newObj = { items: ["a", "b", "c"] };
|
||||||
|
const patches = diffToPatches(oldObj, newObj);
|
||||||
|
expect(patches).toEqual([
|
||||||
|
{ op: "replace", path: "/items", value: ["a", "b", "c"] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not emit patch for identical arrays", () => {
|
||||||
|
const oldObj = { items: [1, 2, 3] };
|
||||||
|
const newObj = { items: [1, 2, 3] };
|
||||||
|
expect(diffToPatches(oldObj, newObj)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles type changes (object → scalar)", () => {
|
||||||
|
const oldObj = { data: { nested: true } };
|
||||||
|
const newObj = { data: "flat" };
|
||||||
|
const patches = diffToPatches(
|
||||||
|
oldObj as Record<string, unknown>,
|
||||||
|
newObj as Record<string, unknown>,
|
||||||
|
);
|
||||||
|
expect(patches).toEqual([{ op: "replace", path: "/data", value: "flat" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles a complex spec diff", () => {
|
||||||
|
const oldSpec = {
|
||||||
|
root: "main",
|
||||||
|
elements: {
|
||||||
|
main: { type: "Card", props: { title: "Hello" }, children: [] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const newSpec = {
|
||||||
|
root: "main",
|
||||||
|
elements: {
|
||||||
|
main: {
|
||||||
|
type: "Card",
|
||||||
|
props: { title: "Hello" },
|
||||||
|
children: ["child-1"],
|
||||||
|
},
|
||||||
|
"child-1": {
|
||||||
|
type: "Text",
|
||||||
|
props: { content: "World" },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const patches = diffToPatches(oldSpec, newSpec);
|
||||||
|
expect(patches).toContainEqual({
|
||||||
|
op: "replace",
|
||||||
|
path: "/elements/main/children",
|
||||||
|
value: ["child-1"],
|
||||||
|
});
|
||||||
|
expect(patches).toContainEqual({
|
||||||
|
op: "add",
|
||||||
|
path: "/elements/child-1",
|
||||||
|
value: {
|
||||||
|
type: "Text",
|
||||||
|
props: { content: "World" },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("escapes JSON Pointer tokens (~ and /)", () => {
|
||||||
|
const patches = diffToPatches({}, { "a/b": 1, "c~d": 2 });
|
||||||
|
expect(patches).toContainEqual({
|
||||||
|
op: "add",
|
||||||
|
path: "/a~1b",
|
||||||
|
value: 1,
|
||||||
|
});
|
||||||
|
expect(patches).toContainEqual({
|
||||||
|
op: "add",
|
||||||
|
path: "/c~0d",
|
||||||
|
value: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { diffToPatches } from "@json-render/core";
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Diff
|
||||||
|
export { diffToPatches } from "./diff";
|
||||||
|
|
||||||
|
// Merge
|
||||||
|
export { deepMergeSpec } from "./merge";
|
||||||
|
|
||||||
|
// Streaming YAML compiler
|
||||||
|
export type { YamlStreamCompiler } from "./parser";
|
||||||
|
export { createYamlStreamCompiler } from "./parser";
|
||||||
|
|
||||||
|
// AI SDK transform
|
||||||
|
export {
|
||||||
|
createYamlTransform,
|
||||||
|
pipeYamlRender,
|
||||||
|
YAML_SPEC_FENCE,
|
||||||
|
YAML_EDIT_FENCE,
|
||||||
|
YAML_PATCH_FENCE,
|
||||||
|
DIFF_FENCE,
|
||||||
|
FENCE_CLOSE,
|
||||||
|
} from "./transform";
|
||||||
|
|
||||||
|
// Prompt generation
|
||||||
|
export type { YamlPromptOptions } from "./prompt";
|
||||||
|
export { yamlPrompt } from "./prompt";
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { deepMergeSpec } from "./merge";
|
||||||
|
|
||||||
|
describe("deepMergeSpec", () => {
|
||||||
|
it("adds new keys", () => {
|
||||||
|
const result = deepMergeSpec({ a: 1 }, { b: 2 });
|
||||||
|
expect(result).toEqual({ a: 1, b: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replaces scalar values", () => {
|
||||||
|
const result = deepMergeSpec({ a: 1 }, { a: 2 });
|
||||||
|
expect(result).toEqual({ a: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes keys set to null", () => {
|
||||||
|
const result = deepMergeSpec({ a: 1, b: 2 }, { b: null });
|
||||||
|
expect(result).toEqual({ a: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deep-merges nested objects", () => {
|
||||||
|
const base = { user: { name: "Alice", age: 30 } };
|
||||||
|
const patch = { user: { age: 31 } };
|
||||||
|
const result = deepMergeSpec(base, patch);
|
||||||
|
expect(result).toEqual({ user: { name: "Alice", age: 31 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replaces arrays (does not concat)", () => {
|
||||||
|
const base = { items: [1, 2, 3] };
|
||||||
|
const patch = { items: [4, 5] };
|
||||||
|
const result = deepMergeSpec(base, patch);
|
||||||
|
expect(result).toEqual({ items: [4, 5] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not mutate base or patch", () => {
|
||||||
|
const base = { a: { b: 1 } };
|
||||||
|
const patch = { a: { c: 2 } };
|
||||||
|
const baseCopy = JSON.parse(JSON.stringify(base));
|
||||||
|
const patchCopy = JSON.parse(JSON.stringify(patch));
|
||||||
|
deepMergeSpec(base, patch);
|
||||||
|
expect(base).toEqual(baseCopy);
|
||||||
|
expect(patch).toEqual(patchCopy);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles a spec-like edit merge", () => {
|
||||||
|
const base = {
|
||||||
|
root: "main",
|
||||||
|
elements: {
|
||||||
|
main: {
|
||||||
|
type: "Card",
|
||||||
|
props: { title: "Dashboard" },
|
||||||
|
children: ["metric-1"],
|
||||||
|
},
|
||||||
|
"metric-1": {
|
||||||
|
type: "Metric",
|
||||||
|
props: { label: "Revenue", value: "$1M" },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const patch = {
|
||||||
|
elements: {
|
||||||
|
main: {
|
||||||
|
props: { title: "Updated Dashboard" },
|
||||||
|
children: ["metric-1", "chart-1"],
|
||||||
|
},
|
||||||
|
"chart-1": {
|
||||||
|
type: "Chart",
|
||||||
|
props: { data: "revenue" },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = deepMergeSpec(base, patch);
|
||||||
|
expect(result.root).toBe("main");
|
||||||
|
expect(
|
||||||
|
(result.elements as Record<string, Record<string, unknown>>)["main"],
|
||||||
|
).toEqual({
|
||||||
|
type: "Card",
|
||||||
|
props: { title: "Updated Dashboard" },
|
||||||
|
children: ["metric-1", "chart-1"],
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
(result.elements as Record<string, Record<string, unknown>>)["metric-1"],
|
||||||
|
).toEqual({
|
||||||
|
type: "Metric",
|
||||||
|
props: { label: "Revenue", value: "$1M" },
|
||||||
|
children: [],
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
(result.elements as Record<string, Record<string, unknown>>)["chart-1"],
|
||||||
|
).toEqual({
|
||||||
|
type: "Chart",
|
||||||
|
props: { data: "revenue" },
|
||||||
|
children: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes an element via null", () => {
|
||||||
|
const base = {
|
||||||
|
elements: {
|
||||||
|
main: { type: "Card" },
|
||||||
|
old: { type: "Widget" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const patch = { elements: { old: null } };
|
||||||
|
const result = deepMergeSpec(base, patch);
|
||||||
|
expect(result.elements).toEqual({ main: { type: "Card" } });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { deepMergeSpec } from "@json-render/core";
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user