$npx -y skills add OthmanAdi/openui-forge --skill openui-forge-vercelOpenUI generative UI with Vercel AI SDK. streamText, toUIMessageStreamResponse, and tools support.
| 1 | # OpenUI Forge — Vercel AI SDK |
| 2 | |
| 3 | Build generative UI apps with OpenUI + Vercel AI SDK. Native streaming with `streamText` and `toUIMessageStreamResponse()`. |
| 4 | |
| 5 | ## Activation Triggers |
| 6 | |
| 7 | - "openui vercel", "openui vercel ai", "openui ai sdk" |
| 8 | - "generative ui vercel", "vercel ai streaming ui" |
| 9 | - "useChat openui", "streamText openui" |
| 10 | |
| 11 | ## Prerequisites |
| 12 | |
| 13 | - Node.js >= 22 (24 LTS recommended), React >= 18.3.1 (19+ recommended) |
| 14 | - `OPENAI_API_KEY` environment variable set |
| 15 | - Next.js project (App Router) |
| 16 | |
| 17 | ## Quick Start |
| 18 | |
| 19 | 1. Install dependencies: |
| 20 | ```bash |
| 21 | npm install @openuidev/react-ui @openuidev/react-lang lucide-react zod ai @ai-sdk/openai @ai-sdk/react |
| 22 | ``` |
| 23 | Pin to the AI SDK v6 line: `ai@^6`, `@ai-sdk/openai@^3`, `@ai-sdk/react@^3`. |
| 24 | 2. Add the CSS import to `app/layout.tsx`: |
| 25 | ```tsx |
| 26 | import "@openuidev/react-ui/components.css"; |
| 27 | ``` |
| 28 | 3. Create the API route and frontend page below |
| 29 | 4. Run `npm run dev` and test |
| 30 | |
| 31 | ## Full Code |
| 32 | |
| 33 | ### Backend: `app/api/chat/route.ts` |
| 34 | |
| 35 | ```typescript |
| 36 | import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib"; |
| 37 | import { convertToModelMessages, streamText } from "ai"; |
| 38 | import { openai } from "@ai-sdk/openai"; |
| 39 | |
| 40 | export async function POST(req: Request) { |
| 41 | const { messages } = await req.json(); |
| 42 | |
| 43 | const systemPrompt = openuiChatLibrary.prompt({ |
| 44 | preamble: "You are a helpful assistant that generates interactive UIs.", |
| 45 | additionalRules: ["Always use Stack as root when combining multiple components."], |
| 46 | }); |
| 47 | |
| 48 | // AI SDK v6: convert the UI message stream into model messages before passing to the model. |
| 49 | const modelMessages = await convertToModelMessages(messages); |
| 50 | |
| 51 | const result = streamText({ |
| 52 | model: openai(process.env.OPENAI_MODEL ?? "gpt-5.5"), |
| 53 | system: systemPrompt, |
| 54 | messages: modelMessages, |
| 55 | }); |
| 56 | |
| 57 | return result.toUIMessageStreamResponse(); |
| 58 | } |
| 59 | ``` |
| 60 | |
| 61 | ### Backend with Tools: `app/api/chat/route.ts` |
| 62 | |
| 63 | ```typescript |
| 64 | import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib"; |
| 65 | import { convertToModelMessages, streamText, tool, stepCountIs } from "ai"; |
| 66 | import { openai } from "@ai-sdk/openai"; |
| 67 | import { z } from "zod"; |
| 68 | |
| 69 | export async function POST(req: Request) { |
| 70 | const { messages } = await req.json(); |
| 71 | |
| 72 | const systemPrompt = openuiChatLibrary.prompt({ |
| 73 | preamble: "You are a helpful assistant that generates interactive UIs. Use tools to fetch data before rendering.", |
| 74 | }); |
| 75 | |
| 76 | // AI SDK v6: convert the UI message stream into model messages before passing to the model. |
| 77 | const modelMessages = await convertToModelMessages(messages); |
| 78 | |
| 79 | const result = streamText({ |
| 80 | model: openai(process.env.OPENAI_MODEL ?? "gpt-5.5"), |
| 81 | system: systemPrompt, |
| 82 | messages: modelMessages, |
| 83 | tools: { |
| 84 | getWeather: tool({ |
| 85 | description: "Get current weather for a city", |
| 86 | inputSchema: z.object({ |
| 87 | city: z.string().describe("City name"), |
| 88 | }), |
| 89 | execute: async ({ city }) => { |
| 90 | return { city, temp: 22, condition: "sunny" }; |
| 91 | }, |
| 92 | }), |
| 93 | }, |
| 94 | // AI SDK v6: stopWhen replaces the removed `maxSteps` option. |
| 95 | stopWhen: stepCountIs(3), |
| 96 | }); |
| 97 | |
| 98 | return result.toUIMessageStreamResponse(); |
| 99 | } |
| 100 | ``` |
| 101 | |
| 102 | ### Frontend (useChat + Renderer): `app/chat/page.tsx` |
| 103 | |
| 104 | Drive the conversation with `useChat` from `@ai-sdk/react`, then render each assistant message with a per-message `<Renderer>` from `@openuidev/react-lang`. The Renderer takes the assistant text as `response`, the component library as `library` (NOT `componentLibrary`), an `isStreaming` flag for the in-flight message, and an `onAction` handler for built-in actions like continuing the conversation. |
| 105 | |
| 106 | ```tsx |
| 107 | "use client"; |
| 108 | import { useChat } from "@ai-sdk/react"; |
| 109 | import { Renderer, BuiltinActionType } from "@openuidev/react-lang"; |
| 110 | import type { ActionEvent } from "@openuidev/react-lang"; |
| 111 | import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib"; |
| 112 | import { useState } from "react"; |
| 113 | |
| 114 | export default function ChatPage() { |
| 115 | const [input, setInput] = useState(""); |
| 116 | const { messages, sendMessage, status } = useChat(); |
| 117 | const isLoading = status === "submitted" || status === "streaming"; |
| 118 | |
| 119 | const handleSend = (text: string) => { |
| 120 | const trimmed = text.trim(); |
| 121 | if (!trimmed || isLoading) return; |
| 122 | setInput(""); |
| 123 | sendMessage({ text: trimmed }); |
| 124 | }; |
| 125 | |
| 126 | const handleAction = (event: ActionEvent) => { |
| 127 | if (event.type === BuiltinActionType.ContinueConversation && event.humanFriendlyMessage) { |
| 128 | handleSend(event.humanFriendlyMessage); |
| 129 | } |
| 130 | }; |
| 131 | |
| 132 | return ( |
| 133 | <div> |
| 134 | {messages.map((message, i) => { |
| 135 | const isLast = i === messages.length - 1; |
| 136 | |
| 137 | if (message.role === "user") { |
| 138 | const text = message.parts |
| 139 | .filter((p): p is { type: "text"; text: string } => p.type === "text") |
| 140 | .map((p) => p.text) |
| 141 | .join(""); |
| 142 | return <div key={message.id}>{text}</div>; |