page.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  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":"set","path":"/root","value":{"key":"root","type":"Card","props":{"title":"Dashboard"}}}
  19. {"op":"set","path":"/root/children/0","value":{"key":"metric-1","type":"Metric","props":{"label":"Revenue"}}}
  20. {"op":"set","path":"/root/children/1","value":{"key":"metric-2","type":"Metric","props":{"label":"Users"}}}`}</Code>
  21. <h2 className="text-xl font-semibold mt-12 mb-4">useUIStream Hook</h2>
  22. <p className="text-sm text-muted-foreground mb-4">
  23. The hook handles parsing and state management:
  24. </p>
  25. <Code lang="tsx">{`import { useUIStream } from '@json-render/react';
  26. function App() {
  27. const {
  28. spec, // Current UI spec state
  29. isStreaming, // True while streaming
  30. error, // Any error that occurred
  31. send, // Function to start generation
  32. abort, // Function to cancel streaming
  33. } = useUIStream({
  34. api: '/api/generate',
  35. onChunk: (chunk) => {}, // Optional: called for each chunk
  36. onFinish: (spec) => {}, // Optional: called when complete
  37. });
  38. }`}</Code>
  39. <h2 className="text-xl font-semibold mt-12 mb-4">Patch Operations</h2>
  40. <p className="text-sm text-muted-foreground mb-4">
  41. Supported operations:
  42. </p>
  43. <ul className="list-disc list-inside text-sm text-muted-foreground space-y-2 mb-4">
  44. <li>
  45. <code className="text-foreground">set</code> — Set the value at a path
  46. (creates if needed)
  47. </li>
  48. <li>
  49. <code className="text-foreground">add</code> — Add to an array at a
  50. path
  51. </li>
  52. <li>
  53. <code className="text-foreground">replace</code> — Replace value at a
  54. path
  55. </li>
  56. <li>
  57. <code className="text-foreground">remove</code> — Remove value at a
  58. path
  59. </li>
  60. </ul>
  61. <h2 className="text-xl font-semibold mt-12 mb-4">Path Format</h2>
  62. <p className="text-sm text-muted-foreground mb-4">
  63. Paths use a key-based format for elements:
  64. </p>
  65. <Code lang="bash">{`/root -> Root element
  66. /root/children -> Children of root
  67. /elements/card-1 -> Element with key "card-1"
  68. /elements/card-1/children -> Children of card-1`}</Code>
  69. <h2 className="text-xl font-semibold mt-12 mb-4">Server-Side Setup</h2>
  70. <p className="text-sm text-muted-foreground mb-4">
  71. Ensure your API route streams properly:
  72. </p>
  73. <Code lang="typescript">{`import { streamText } from 'ai';
  74. import { catalog } from '@/lib/catalog';
  75. export async function POST(req: Request) {
  76. const { prompt } = await req.json();
  77. const result = streamText({
  78. model: 'anthropic/claude-haiku-4.5',
  79. system: catalog.prompt(),
  80. prompt,
  81. });
  82. // Return as a streaming response
  83. return result.toTextStreamResponse();
  84. }`}</Code>
  85. <h2 className="text-xl font-semibold mt-12 mb-4">
  86. Progressive Rendering
  87. </h2>
  88. <p className="text-sm text-muted-foreground mb-4">
  89. The Renderer automatically updates as the spec changes:
  90. </p>
  91. <Code lang="tsx">{`function App() {
  92. const { spec, isStreaming } = useUIStream({ api: '/api/generate' });
  93. return (
  94. <div>
  95. {isStreaming && <LoadingIndicator />}
  96. <Renderer spec={spec} registry={registry} loading={isStreaming} />
  97. </div>
  98. );
  99. }`}</Code>
  100. <h2 className="text-xl font-semibold mt-12 mb-4">Aborting Streams</h2>
  101. <Code lang="tsx">{`function App() {
  102. const { isStreaming, send, abort } = useUIStream({
  103. api: '/api/generate',
  104. });
  105. return (
  106. <div>
  107. <button onClick={() => send('Create dashboard')}>
  108. Generate
  109. </button>
  110. {isStreaming && (
  111. <button onClick={abort}>Cancel</button>
  112. )}
  113. </div>
  114. );
  115. }`}</Code>
  116. <h2 className="text-xl font-semibold mt-12 mb-4">
  117. Low-Level SpecStream API
  118. </h2>
  119. <p className="text-sm text-muted-foreground mb-4">
  120. For custom streaming implementations, use the SpecStream compiler
  121. directly:
  122. </p>
  123. <Code lang="typescript">{`import { createSpecStreamCompiler } from '@json-render/core';
  124. // Create a compiler for your spec type
  125. const compiler = createSpecStreamCompiler<MySpec>();
  126. // Process streaming chunks from AI
  127. async function processStream(reader: ReadableStreamDefaultReader) {
  128. while (true) {
  129. const { done, value } = await reader.read();
  130. if (done) break;
  131. const { result, newPatches } = compiler.push(value);
  132. if (newPatches.length > 0) {
  133. // Update UI with partial result
  134. setSpec(result);
  135. }
  136. }
  137. // Get final compiled result
  138. return compiler.getResult();
  139. }`}</Code>
  140. <h3 className="text-lg font-semibold mt-8 mb-4">One-Shot Compilation</h3>
  141. <p className="text-sm text-muted-foreground mb-4">
  142. For non-streaming scenarios, compile entire SpecStream at once:
  143. </p>
  144. <Code lang="typescript">{`import { compileSpecStream } from '@json-render/core';
  145. const jsonl = \`{"op":"set","path":"/root","value":{"type":"Card"}}
  146. {"op":"set","path":"/root/props","value":{"title":"Hello"}}\`;
  147. const spec = compileSpecStream<MySpec>(jsonl);
  148. // { root: { type: "Card", props: { title: "Hello" } } }`}</Code>
  149. </article>
  150. );
  151. }