| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- import { pageMetadata } from "@/lib/page-metadata"
- export const metadata = pageMetadata("docs/streaming")
- # Streaming
- Progressively render UI as AI generates it.
- ## SpecStream Format
- json-render uses **SpecStream**, a JSONL-based streaming format where each line is a JSON patch operation that progressively builds your spec:
- ```json
- {"op":"add","path":"/root","value":"root"}
- {"op":"add","path":"/elements/root","value":{"type":"Card","props":{"title":"Dashboard"},"children":["metric-1","metric-2"]}}
- {"op":"add","path":"/elements/metric-1","value":{"type":"Metric","props":{"label":"Revenue"}}}
- {"op":"add","path":"/elements/metric-2","value":{"type":"Metric","props":{"label":"Users"}}}
- ```
- ## Patch Operations (RFC 6902)
- SpecStream uses [RFC 6902 JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations:
- - `add` — Add a value at a path (creates or replaces for objects, inserts for arrays)
- - `remove` — Remove the value at a path
- - `replace` — Replace an existing value at a path
- - `move` — Move a value from one path to another (requires `from`)
- - `copy` — Copy a value from one path to another (requires `from`)
- - `test` — Assert that a value at a path equals the given value
- ## Path Format
- Paths follow JSON Pointer (RFC 6901) into the spec object:
- ```bash
- /root -> Root element key (string)
- /elements/card-1 -> Element with key "card-1"
- /elements/card-1/props -> Props of card-1
- /elements/card-1/children -> Children of card-1
- ```
- ## Server-Side Setup
- Ensure your API route streams properly:
- ```typescript
- import { streamText } from 'ai';
- import { catalog } from '@/lib/catalog';
- export async function POST(req: Request) {
- const { prompt } = await req.json();
-
- const result = streamText({
- model: 'anthropic/claude-haiku-4.5',
- system: catalog.prompt(),
- prompt,
- });
- // Return as a streaming response
- return result.toTextStreamResponse();
- }
- ```
- ## Low-Level SpecStream API
- For custom or framework-agnostic streaming implementations, use the SpecStream compiler from `@json-render/core` directly:
- ```typescript
- import { createSpecStreamCompiler } from '@json-render/core';
- // Create a compiler for your spec type
- const compiler = createSpecStreamCompiler<MySpec>();
- const decoder = new TextDecoder();
- // Process streaming chunks from AI
- async function processStream(reader: ReadableStreamDefaultReader<Uint8Array>) {
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
- // Decode the Uint8Array chunk to a string
- const chunk = decoder.decode(value, { stream: true });
- const { result, newPatches } = compiler.push(chunk);
-
- if (newPatches.length > 0) {
- // Update UI with partial result
- setSpec(result);
- }
- }
-
- // Get final compiled result
- return compiler.getResult();
- }
- ```
- ### One-Shot Compilation
- For non-streaming scenarios, compile entire SpecStream at once:
- ```typescript
- import { compileSpecStream } from '@json-render/core';
- const jsonl = `{"op":"add","path":"/root","value":"card-1"}
- {"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Hello"},"children":[]}}`;
- const spec = compileSpecStream<Spec>(jsonl);
- // { root: "card-1", elements: { "card-1": { type: "Card", props: { title: "Hello" }, children: [] } } }
- ```
- ## Usage with React
- `@json-render/react` provides the `useUIStream` hook, which wraps the low-level compiler in a React-friendly API with state management, error handling, and abort support.
- ### useUIStream Hook
- ```tsx
- import { useUIStream } from '@json-render/react';
- function App() {
- const {
- spec, // Current UI spec state
- isStreaming, // True while streaming
- error, // Any error that occurred
- send, // Function to start generation
- clear, // Function to reset spec and error
- } = useUIStream({
- api: '/api/generate',
- onComplete: (spec) => {}, // Optional: called when streaming completes
- onError: (error) => {}, // Optional: called when an error occurs
- });
- }
- ```
- ### Progressive Rendering
- The Renderer automatically updates as the spec changes:
- ```tsx
- function App() {
- const { spec, isStreaming } = useUIStream({ api: '/api/generate' });
- return (
- <div>
- {isStreaming && <LoadingIndicator />}
- <Renderer spec={spec} registry={registry} loading={isStreaming} />
- </div>
- );
- }
- ```
- ### Aborting Streams
- Calling `send` again automatically aborts the previous request. Use `clear` to reset the spec and error state:
- ```tsx
- function App() {
- const { isStreaming, send, clear } = useUIStream({
- api: '/api/generate',
- });
- return (
- <div>
- <button onClick={() => send('Create dashboard')}>
- Generate
- </button>
- <button onClick={clear}>Reset</button>
- </div>
- );
- }
- ```
- See the [@json-render/react API reference](/docs/api/react) for full `useUIStream` documentation.
|