page.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import { Code } from "@/components/code";
  2. export const metadata = {
  3. title: "Streaming | json-render",
  4. };
  5. export default function StreamingPage() {
  6. return (
  7. <article>
  8. <h1 className="text-3xl font-bold mb-4">Streaming</h1>
  9. <p className="text-muted-foreground mb-8">
  10. Progressively render UI as AI generates it.
  11. </p>
  12. <h2 className="text-xl font-semibold mt-12 mb-4">SpecStream Format</h2>
  13. <p className="text-sm text-muted-foreground mb-4">
  14. json-render uses <strong>SpecStream</strong>, a JSONL-based streaming
  15. format where each line is a JSON patch operation that progressively
  16. builds your spec:
  17. </p>
  18. <Code lang="json">{`{"op":"add","path":"/root","value":"root"}
  19. {"op":"add","path":"/elements/root","value":{"type":"Card","props":{"title":"Dashboard"},"children":["metric-1","metric-2"]}}
  20. {"op":"add","path":"/elements/metric-1","value":{"type":"Metric","props":{"label":"Revenue"}}}
  21. {"op":"add","path":"/elements/metric-2","value":{"type":"Metric","props":{"label":"Users"}}}`}</Code>
  22. <h2 className="text-xl font-semibold mt-12 mb-4">useUIStream Hook</h2>
  23. <p className="text-sm text-muted-foreground mb-4">
  24. The hook handles parsing and state management:
  25. </p>
  26. <Code lang="tsx">{`import { useUIStream } from '@json-render/react';
  27. function App() {
  28. const {
  29. spec, // Current UI spec state
  30. isStreaming, // True while streaming
  31. error, // Any error that occurred
  32. send, // Function to start generation
  33. clear, // Function to reset spec and error
  34. } = useUIStream({
  35. api: '/api/generate',
  36. onComplete: (spec) => {}, // Optional: called when streaming completes
  37. onError: (error) => {}, // Optional: called when an error occurs
  38. });
  39. }`}</Code>
  40. <h2 className="text-xl font-semibold mt-12 mb-4">
  41. Patch Operations (RFC 6902)
  42. </h2>
  43. <p className="text-sm text-muted-foreground mb-4">
  44. SpecStream uses{" "}
  45. <a
  46. href="https://datatracker.ietf.org/doc/html/rfc6902"
  47. className="underline"
  48. target="_blank"
  49. rel="noopener noreferrer"
  50. >
  51. RFC 6902 JSON Patch
  52. </a>{" "}
  53. operations:
  54. </p>
  55. <ul className="list-disc list-inside text-sm text-muted-foreground space-y-2 mb-4">
  56. <li>
  57. <code className="text-foreground">add</code> — Add a value at a path
  58. (creates or replaces for objects, inserts for arrays)
  59. </li>
  60. <li>
  61. <code className="text-foreground">remove</code> — Remove the value at
  62. a path
  63. </li>
  64. <li>
  65. <code className="text-foreground">replace</code> — Replace an existing
  66. value at a path
  67. </li>
  68. <li>
  69. <code className="text-foreground">move</code> — Move a value from one
  70. path to another (requires{" "}
  71. <code className="text-foreground">from</code>)
  72. </li>
  73. <li>
  74. <code className="text-foreground">copy</code> — Copy a value from one
  75. path to another (requires{" "}
  76. <code className="text-foreground">from</code>)
  77. </li>
  78. <li>
  79. <code className="text-foreground">test</code> — Assert that a value at
  80. a path equals the given value
  81. </li>
  82. </ul>
  83. <h2 className="text-xl font-semibold mt-12 mb-4">Path Format</h2>
  84. <p className="text-sm text-muted-foreground mb-4">
  85. Paths use a key-based format for elements:
  86. </p>
  87. <Code lang="bash">{`/root -> Root element
  88. /root/children -> Children of root
  89. /elements/card-1 -> Element with key "card-1"
  90. /elements/card-1/children -> Children of card-1`}</Code>
  91. <h2 className="text-xl font-semibold mt-12 mb-4">Server-Side Setup</h2>
  92. <p className="text-sm text-muted-foreground mb-4">
  93. Ensure your API route streams properly:
  94. </p>
  95. <Code lang="typescript">{`import { streamText } from 'ai';
  96. import { catalog } from '@/lib/catalog';
  97. export async function POST(req: Request) {
  98. const { prompt } = await req.json();
  99. const result = streamText({
  100. model: 'anthropic/claude-haiku-4.5',
  101. system: catalog.prompt(),
  102. prompt,
  103. });
  104. // Return as a streaming response
  105. return result.toTextStreamResponse();
  106. }`}</Code>
  107. <h2 className="text-xl font-semibold mt-12 mb-4">
  108. Progressive Rendering
  109. </h2>
  110. <p className="text-sm text-muted-foreground mb-4">
  111. The Renderer automatically updates as the spec changes:
  112. </p>
  113. <Code lang="tsx">{`function App() {
  114. const { spec, isStreaming } = useUIStream({ api: '/api/generate' });
  115. return (
  116. <div>
  117. {isStreaming && <LoadingIndicator />}
  118. <Renderer spec={spec} registry={registry} loading={isStreaming} />
  119. </div>
  120. );
  121. }`}</Code>
  122. <h2 className="text-xl font-semibold mt-12 mb-4">Aborting Streams</h2>
  123. <p className="text-sm text-muted-foreground mb-4">
  124. Calling <code className="text-foreground">send</code> again
  125. automatically aborts the previous request. Use{" "}
  126. <code className="text-foreground">clear</code> to reset the spec and
  127. error state:
  128. </p>
  129. <Code lang="tsx">{`function App() {
  130. const { isStreaming, send, clear } = useUIStream({
  131. api: '/api/generate',
  132. });
  133. return (
  134. <div>
  135. <button onClick={() => send('Create dashboard')}>
  136. Generate
  137. </button>
  138. <button onClick={clear}>Reset</button>
  139. </div>
  140. );
  141. }`}</Code>
  142. <h2 className="text-xl font-semibold mt-12 mb-4">
  143. Low-Level SpecStream API
  144. </h2>
  145. <p className="text-sm text-muted-foreground mb-4">
  146. For custom streaming implementations, use the SpecStream compiler
  147. directly:
  148. </p>
  149. <Code lang="typescript">{`import { createSpecStreamCompiler } from '@json-render/core';
  150. // Create a compiler for your spec type
  151. const compiler = createSpecStreamCompiler<MySpec>();
  152. // Process streaming chunks from AI
  153. async function processStream(reader: ReadableStreamDefaultReader) {
  154. while (true) {
  155. const { done, value } = await reader.read();
  156. if (done) break;
  157. const { result, newPatches } = compiler.push(value);
  158. if (newPatches.length > 0) {
  159. // Update UI with partial result
  160. setSpec(result);
  161. }
  162. }
  163. // Get final compiled result
  164. return compiler.getResult();
  165. }`}</Code>
  166. <h3 className="text-lg font-semibold mt-8 mb-4">One-Shot Compilation</h3>
  167. <p className="text-sm text-muted-foreground mb-4">
  168. For non-streaming scenarios, compile entire SpecStream at once:
  169. </p>
  170. <Code lang="typescript">{`import { compileSpecStream } from '@json-render/core';
  171. const jsonl = \`{"op":"add","path":"/root","value":{"type":"Card"}}
  172. {"op":"add","path":"/root/props","value":{"title":"Hello"}}\`;
  173. const spec = compileSpecStream<MySpec>(jsonl);
  174. // { root: { type: "Card", props: { title: "Hello" } } }`}</Code>
  175. </article>
  176. );
  177. }