Generative UI with Vercel AI SDK
2 months agoGenerative UI Support
The web development landscape is evolving quickly, especially in the realm of GenAI applications. Vercel's latest release of AI SDK 3 introduces "Generative UI" technology, allowing developers to fetch realtime data and seamlessly stream custom React components directly from large language models (LLMs). This means developers and products can now go beyond simple text-based chatbots and deliver dynamic, component-rich experiences seamlessly integrated into your applications. By providing a set of functions, the LLM intelligently selects the appropriate ones to call based on user input. Vercel AI SDK simplifies the complex process of connecting your application code with an LLM.
Below I will discuss a brief introduction to what has been released and how I am currently implementing it on this very site.
Getting Started
I will take the example from Vercel's announcement here
1import { render } from 'ai/rsc'
2import OpenAI from 'openai'
3
4const openai = new OpenAI()
5
6async function submitMessage(userInput) {
7 'use server'
8
9 return render({
10 provider: openai,
11 model: 'gpt-4',
12 messages: [
13 { role: 'system', content: 'You are an assistant' },
14 { role: 'user', content: userInput }
15 ],
16 text: ({ content }) => <p>{content}</p>,
17 })
18}
The render function is a versatile tool for generating streamable UIs from an LLM response.
By default, it streams the LLM response's text content wrapped in a React Fragment. You can customize the React component used for text responses by specifying the text
key.
Additionally, the function supports mapping OpenAI-compatible models with Function Calls to React Server Components through the tools
key. Each tool can include a nested render function to return React components, allowing you to associate each tool with a specific UI component. If you use a generator signature, you can yield React Nodes, which will be sent as separate updates to the client. This feature is particularly useful for managing loading states and facilitating agentic, multi-step interactions.
Generative Tools
A tool is an object that the LLM can invoke to carry out a particular function (e.g. fetching weather in a particular location, checking the flight status of a flight)
Each tool consists of three parameters:
description:
- a textual summary that guides the selection of the toolparameters:
- azod
schema orJSON schema
outlining the paramets that the LLM needs to extract from the user's inputexecute / generate:
- an optional function that is invoked with the arguments derived from the tool call
An example below using weather:
1import { render } from 'ai/rsc'
2import OpenAI from 'openai'
3import { z } from 'zod'
4
5const openai = new OpenAI()
6
7async function submitMessage(userInput) { // 'What is the weather in San Francisco?'
8 'use server'
9
10 return render({
11 provider: openai,
12 model: 'gpt-4-0125-preview',
13 messages: [
14 { role: 'system', content: 'You are a helpful assistant' },
15 { role: 'user', content: userInput }
16 ],
17 text: ({ content }) => <p>{content}</p>,
18 tools: {
19 get_city_weather: {
20 description: 'Get the current weather for a city',
21 parameters: z.object({
22 city: z.string().describe('the city')
23 }).required(),
24 render: async function* ({ city }) {
25 yield <Spinner/>
26 const weather = await getWeather(city)
27 return <Weather info={weather} />
28 }
29 }
30 }
31 })
32}
The results transcend basic text and static data, providing interactive and engaging experiences through the streaming of custom React components. This unlocks endless possibilities.
Implementing Generative UI on this website
If you have not already headed over to the home page, you’ll find that I've integrated these interactive features directly into this website. Explore how custom React components can enhance your experience with real-time, dynamic content such as Spotify API & Spotify embed songs.
For example with Spotify, you can ask questions such as what I am currently playing, what songs I have recently been listening to, what are my top songs, and top artists.
Below, you’ll find a more detailed example code demonstrating how AIState and UIState are utilised to build this functionality. For flexible customisation, I store the prompt in Sanity. The provided code shows how parameters are validated with Zod schema, fed into the Spotify API, and rendered as embedded Spotify players.
Here's a glimpse into the setup:
1import 'server-only'
2
3import {
4 createAI,
5 getMutableAIState,
6 streamUI,
7 createStreamableValue
8} from 'ai/rsc'
9import { openai } from '@ai-sdk/openai'
10import { Spotify } from 'react-spotify-embed'
11import { BotCard, BotMessage } from '@/components/message'
12import { z } from 'zod'
13import { nanoid } from '@/lib/utils'
14import { SpinnerMessage } from '@/components/message'
15import { Message } from '@/lib/types'
16import { loadSettings } from '@/sanity/loader/loadQuery'
17import { fetchTopTracks } from '../hooks/use-spotify'
18import { createClient } from '../hooks/use-supabase'
19
20async function submitUserMessage(content: string) {
21 'use server'
22
23 const aiState = getMutableAIState<typeof AI>()
24 const settings = await loadSettings()
25
26 aiState.update({
27 ...aiState.get(),
28 messages: [
29 ...aiState.get().messages,
30 {
31 id: nanoid(),
32 role: 'user',
33 content
34 }
35 ]
36 })
37
38 let textStream: undefined | ReturnType<typeof createStreamableValue<string>>
39 let textNode: undefined | React.ReactNode
40
41 const result = await streamUI({
42 model: openai('gpt-3.5-turbo'),
43 initial: <SpinnerMessage />,
44 system: settings.data?.prompt,
45 messages: [
46 ...aiState.get().messages.map((message: any) => ({
47 role: message.role,
48 content: message.content,
49 name: message.name
50 }))
51 ],
52 text: ({ content, done, delta }) => {
53 if (!textStream) {
54 textStream = createStreamableValue('')
55 textNode = <BotMessage content={textStream.value} />
56 }
57
58 if (done) {
59 textStream.done()
60 aiState.done({
61 ...aiState.get(),
62 messages: [
63 ...aiState.get().messages,
64 {
65 id: nanoid(),
66 role: 'assistant',
67 content
68 }
69 ]
70 })
71 } else {
72 textStream.update(delta)
73 }
74
75 return textNode
76 },
77 tools: {
78 spotifyTopPlayerSongs: {
79 description:
80 "Show Tim's top Spotify songs based on his spotify listening history.",
81 parameters: z.object({
82 term: z
83 .string()
84 .default('long_term')
85 .describe(
86 `The length of time to analyze - should be one of 'long_term', 'medium_term', 'short_term', long_term (calculated from ~1 year of data), medium_term (approximately last 6 months), short_term (approximately last 4 weeks). Default to long_term`
87 ),
88 limit: z
89 .number()
90 .int()
91 .min(1)
92 .max(5)
93 .describe('The number of songs to show')
94 }),
95 generate: async function* ({ term = 'long_term', limit = 5 }) {
96 yield <SpinnerMessage />
97 const { items } = await fetchTopTracks(term, limit)
98 return (
99 <BotCard>
100 <div className="flex flex-col items-start gap-2 w-full">
101 {items
102 .slice(0, limit)
103 .map(
104 (
105 item: { external_urls: { spotify: string } },
106 index: number
107 ) => (
108 <Spotify
109 key={index}
110 link={item.external_urls.spotify}
111 wide
112 />
113 )
114 )}
115 </div>
116 </BotCard>
117 )
118 }
119 }
120 }
121 })
122
123 return {
124 id: nanoid(),
125 display: result.value
126 }
127}
128
129export type AIState = {
130 chatId: string
131 messages: Message[]
132}
133
134export type UIState = {
135 id: string
136 display: React.ReactNode
137}[]
138
139export const AI = createAI<AIState, UIState>({
140 actions: {
141 submitUserMessage
142 },
143 initialUIState: [],
144 initialAIState: { chatId: nanoid(), messages: [] },
145 onSetAIState: async ({ state }) => {
146 'use server'
147 try {
148 const supabase = createClient()
149 const { chatId, messages } = state
150 await supabase.from('chats').upsert({
151 id: chatId,
152 messages,
153 updated_at: new Date()
154 })
155 } catch (error) {
156 console.error(error)
157 }
158 }
159})
Going further with Vercel AI SDK
Integrating LLMs with the Vercel AI SDK extends far beyond static text, unlocking a world of possibilities for enhancing and creating new applications. The shift from simple text responses to interactive React components represents a significant leap in user experience, making interactions not only more engaging but also more dynamic and contextually relevant.
This approach opens up a multitude of opportunities, such as integrating with databases for real-time updates, utilizing embeddings for smarter, more contextual responses, and developing sophisticated components for tasks like booking plane tickets or selecting seating. Each of these applications benefits from a richer, more interactive user interface that goes beyond traditional methods.
As technology continues to advance, adopting these innovative techniques will be crucial for developing compelling and functional digital experiences. By leveraging the full capabilities of the Vercel AI SDK, we can create more personalized, responsive, and immersive interactions that truly resonate with users.
Resources: