page.mdx 6.4 KB

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