page.mdx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. export const metadata = { title: "Generation Modes" }
  2. # Generation Modes
  3. json-render supports two modes for AI-generated UI: **Generate mode** for standalone UI and **Chat mode** for inline UI within a conversation.
  4. The mode controls how the AI formats its output and how your app processes the stream. The underlying JSONL patch format is the same in both modes.
  5. <GenerationModesDiagram />
  6. ## Generate Mode (Standalone)
  7. In generate mode, the AI outputs **only JSONL patches** — no prose, no markdown. The entire response is a UI spec.
  8. This is the default mode and is ideal for:
  9. - Playground and builder tools
  10. - Form generators
  11. - Dashboard builders
  12. - Any UI where the generated interface is the whole response
  13. ### Setup
  14. ```typescript
  15. import { streamText } from "ai";
  16. // Generate mode is the default (no mode option needed)
  17. const systemPrompt = catalog.prompt({
  18. customRules: [
  19. "Use Card as root for forms and small UIs.",
  20. "Use Grid for multi-column layouts.",
  21. ],
  22. });
  23. const result = streamText({
  24. model: "anthropic/claude-haiku-4.5",
  25. system: systemPrompt,
  26. prompt: userPrompt,
  27. });
  28. ```
  29. ### Client
  30. On the client, use `useUIStream` from `@json-render/react` or the lower-level `createSpecStreamCompiler` from `@json-render/core` to compile the JSONL stream into a spec:
  31. ```tsx
  32. import { useUIStream } from "@json-render/react";
  33. function Playground() {
  34. const { spec, isStreaming, send } = useUIStream({
  35. api: "/api/generate",
  36. });
  37. return (
  38. <Renderer
  39. spec={spec}
  40. registry={registry}
  41. loading={isStreaming}
  42. />
  43. );
  44. }
  45. ```
  46. ### Example output
  47. The AI outputs only JSONL — one patch per line, no surrounding text:
  48. ```
  49. {"op":"add","path":"/root","value":"card-1"}
  50. {"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Sign In"},"children":["email","password","submit"]}}
  51. {"op":"add","path":"/elements/email","value":{"type":"Input","props":{"label":"Email","name":"email","type":"email"}}}
  52. {"op":"add","path":"/elements/password","value":{"type":"Input","props":{"label":"Password","name":"password","type":"password"}}}
  53. {"op":"add","path":"/elements/submit","value":{"type":"Button","props":{"label":"Sign In"}}}
  54. ```
  55. ## Chat Mode (Inline)
  56. In chat mode, the AI responds **conversationally first**, then outputs JSONL patches on their own lines. Text-only replies are allowed when no UI is needed (e.g. greetings, clarifying questions).
  57. This is ideal for:
  58. - AI chatbots with rich UI responses
  59. - Copilot experiences
  60. - Educational assistants
  61. - Any conversational interface where generated UI is embedded in chat messages
  62. ### Setup
  63. ```typescript
  64. import { streamText } from "ai";
  65. import { pipeJsonRender } from "@json-render/core";
  66. import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
  67. // Enable chat mode
  68. const systemPrompt = catalog.prompt({ mode: "chat" });
  69. const result = streamText({
  70. model: yourModel,
  71. system: systemPrompt,
  72. messages,
  73. });
  74. // In your API route, pipe the stream through pipeJsonRender
  75. // to separate text from JSONL patches
  76. const stream = createUIMessageStream({
  77. execute: async ({ writer }) => {
  78. writer.merge(pipeJsonRender(result.toUIMessageStream()));
  79. },
  80. });
  81. return createUIMessageStreamResponse({ stream });
  82. ```
  83. `pipeJsonRender` inspects each line of the AI's response. Lines that parse as JSONL patches are emitted as `data-spec` parts (which the renderer picks up). Everything else is passed through as text.
  84. ### Client
  85. On the client, use `useJsonRenderMessage` from `@json-render/react` to extract the spec from a chat message's parts:
  86. ```tsx
  87. import { useChat } from "@ai-sdk/react";
  88. import { useJsonRenderMessage } from "@json-render/react";
  89. function Chat() {
  90. const { messages, input, handleInputChange, handleSubmit } = useChat();
  91. return (
  92. <div>
  93. {messages.map((msg) => (
  94. <ChatMessage key={msg.id} message={msg} />
  95. ))}
  96. {/* input form */}
  97. </div>
  98. );
  99. }
  100. function ChatMessage({ message }) {
  101. const { spec } = useJsonRenderMessage(message.parts);
  102. return (
  103. <div>
  104. {/* Render text parts */}
  105. {message.parts
  106. .filter((p) => p.type === "text")
  107. .map((p, i) => <p key={i}>{p.text}</p>)}
  108. {/* Render the generated UI inline */}
  109. {spec && (
  110. <Renderer
  111. spec={spec}
  112. registry={registry}
  113. />
  114. )}
  115. </div>
  116. );
  117. }
  118. ```
  119. ### Example output
  120. The AI writes a brief explanation, then JSONL patches on their own lines:
  121. ```
  122. Here's a dashboard showing the latest crypto prices:
  123. {"op":"add","path":"/root","value":"dashboard"}
  124. {"op":"add","path":"/state/prices","value":[{"name":"Bitcoin","price":98450},{"name":"Ethereum","price":3120}]}
  125. {"op":"add","path":"/elements/dashboard","value":{"type":"Grid","props":{"columns":"2"},"children":["btc","eth"]}}
  126. {"op":"add","path":"/elements/btc","value":{"type":"Metric","props":{"label":"Bitcoin","value":{"$state":"/prices/0/price"}}}}
  127. {"op":"add","path":"/elements/eth","value":{"type":"Metric","props":{"label":"Ethereum","value":{"$state":"/prices/1/price"}}}}
  128. ```
  129. If the user asks a simple question ("what does BTC stand for?"), the AI replies with text only — no JSONL.
  130. ## Quick Comparison
  131. <div className="my-6 overflow-x-auto">
  132. <table className="mdx-table w-full text-sm border-collapse">
  133. <thead>
  134. <tr>
  135. <th />
  136. <th>Generate</th>
  137. <th>Chat</th>
  138. </tr>
  139. </thead>
  140. <tbody>
  141. <tr>
  142. <td>Output format</td>
  143. <td>JSONL only</td>
  144. <td>Text + JSONL</td>
  145. </tr>
  146. <tr>
  147. <td>Text-only replies</td>
  148. <td>No</td>
  149. <td>Yes</td>
  150. </tr>
  151. <tr>
  152. <td>System prompt</td>
  153. <td><code>{"catalog.prompt()"}</code></td>
  154. <td><code>{'catalog.prompt({ mode: "chat" })'}</code></td>
  155. </tr>
  156. <tr>
  157. <td>Stream utility</td>
  158. <td><code>{"useUIStream"}</code></td>
  159. <td><code>{"pipeJsonRender"}</code>{" + "}<code>{"useJsonRenderMessage"}</code></td>
  160. </tr>
  161. <tr>
  162. <td>Typical use case</td>
  163. <td>Playground, builders</td>
  164. <td>Chatbots, copilots</td>
  165. </tr>
  166. </tbody>
  167. </table>
  168. </div>
  169. Both modes use the same JSONL patch format (RFC 6902) and the same catalog/registry system. The only difference is whether the AI is allowed to include prose alongside the patches.
  170. ## Next
  171. - Learn about the [JSONL streaming format](/docs/streaming)
  172. - See the [AI SDK integration](/docs/ai-sdk) for setup with the Vercel AI SDK