page.mdx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. export const metadata = { title: "Streaming" }
  2. # Streaming
  3. Progressively render UI as AI generates it.
  4. ## SpecStream Format
  5. json-render uses **SpecStream**, a JSONL-based streaming format where each line is a JSON patch operation that progressively builds your spec:
  6. ```json
  7. {"op":"add","path":"/root","value":"root"}
  8. {"op":"add","path":"/elements/root","value":{"type":"Card","props":{"title":"Dashboard"},"children":["metric-1","metric-2"]}}
  9. {"op":"add","path":"/elements/metric-1","value":{"type":"Metric","props":{"label":"Revenue"}}}
  10. {"op":"add","path":"/elements/metric-2","value":{"type":"Metric","props":{"label":"Users"}}}
  11. ```
  12. ## useUIStream Hook
  13. The hook handles parsing and state management:
  14. ```tsx
  15. import { useUIStream } from '@json-render/react';
  16. function App() {
  17. const {
  18. spec, // Current UI spec state
  19. isStreaming, // True while streaming
  20. error, // Any error that occurred
  21. send, // Function to start generation
  22. clear, // Function to reset spec and error
  23. } = useUIStream({
  24. api: '/api/generate',
  25. onComplete: (spec) => {}, // Optional: called when streaming completes
  26. onError: (error) => {}, // Optional: called when an error occurs
  27. });
  28. }
  29. ```
  30. ## Patch Operations (RFC 6902)
  31. SpecStream uses [RFC 6902 JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations:
  32. - `add` — Add a value at a path (creates or replaces for objects, inserts for arrays)
  33. - `remove` — Remove the value at a path
  34. - `replace` — Replace an existing value at a path
  35. - `move` — Move a value from one path to another (requires `from`)
  36. - `copy` — Copy a value from one path to another (requires `from`)
  37. - `test` — Assert that a value at a path equals the given value
  38. ## Path Format
  39. Paths use a key-based format for elements:
  40. ```bash
  41. /root -> Root element
  42. /root/children -> Children of root
  43. /elements/card-1 -> Element with key "card-1"
  44. /elements/card-1/children -> Children of card-1
  45. ```
  46. ## Server-Side Setup
  47. Ensure your API route streams properly:
  48. ```typescript
  49. import { streamText } from 'ai';
  50. import { catalog } from '@/lib/catalog';
  51. export async function POST(req: Request) {
  52. const { prompt } = await req.json();
  53. const result = streamText({
  54. model: 'anthropic/claude-haiku-4.5',
  55. system: catalog.prompt(),
  56. prompt,
  57. });
  58. // Return as a streaming response
  59. return result.toTextStreamResponse();
  60. }
  61. ```
  62. ## Progressive Rendering
  63. The Renderer automatically updates as the spec changes:
  64. ```tsx
  65. function App() {
  66. const { spec, isStreaming } = useUIStream({ api: '/api/generate' });
  67. return (
  68. <div>
  69. {isStreaming && <LoadingIndicator />}
  70. <Renderer spec={spec} registry={registry} loading={isStreaming} />
  71. </div>
  72. );
  73. }
  74. ```
  75. ## Aborting Streams
  76. Calling `send` again automatically aborts the previous request. Use `clear` to reset the spec and error state:
  77. ```tsx
  78. function App() {
  79. const { isStreaming, send, clear } = useUIStream({
  80. api: '/api/generate',
  81. });
  82. return (
  83. <div>
  84. <button onClick={() => send('Create dashboard')}>
  85. Generate
  86. </button>
  87. <button onClick={clear}>Reset</button>
  88. </div>
  89. );
  90. }
  91. ```
  92. ## Low-Level SpecStream API
  93. For custom streaming implementations, use the SpecStream compiler directly:
  94. ```typescript
  95. import { createSpecStreamCompiler } from '@json-render/core';
  96. // Create a compiler for your spec type
  97. const compiler = createSpecStreamCompiler<MySpec>();
  98. // Process streaming chunks from AI
  99. async function processStream(reader: ReadableStreamDefaultReader) {
  100. while (true) {
  101. const { done, value } = await reader.read();
  102. if (done) break;
  103. const { result, newPatches } = compiler.push(value);
  104. if (newPatches.length > 0) {
  105. // Update UI with partial result
  106. setSpec(result);
  107. }
  108. }
  109. // Get final compiled result
  110. return compiler.getResult();
  111. }
  112. ```
  113. ### One-Shot Compilation
  114. For non-streaming scenarios, compile entire SpecStream at once:
  115. ```typescript
  116. import { compileSpecStream } from '@json-render/core';
  117. const jsonl = `{"op":"add","path":"/root","value":{"type":"Card"}}
  118. {"op":"add","path":"/root/props","value":{"title":"Hello"}}`;
  119. const spec = compileSpecStream<MySpec>(jsonl);
  120. // { root: { type: "Card", props: { title: "Hello" } } }
  121. ```