Generative UI with Vercel AI SDK

2 months ago
vercel, ai

Generative 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

javascript
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 tool
  • parameters: - a zod schema or JSON schema outlining the paramets that the LLM needs to extract from the user's input
  • execute / generate: - an optional function that is invoked with the arguments derived from the tool call

An example below using weather:

javascript
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:

typescript
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:

- Vercel AI SDK