Render Visual Interface in Chat

Learn how to generate text using the AI TOOLKIT and React Server Components.

3 min readrscgenerative user interfaceView source

We've now seen how a language model can call a function and render a component based on a conversation with the user. When we define multiple functions in `tools`, it is possible for the model to reason out the right functions to call based on whatever the user's intent is. This means that you can write a bunch of functions without the burden of implementing complex routing logic to run them.

Run it locally

$ npm install ai

Client

'use client';

import { useState } from 'react';

import { ClientMessage } from './actions';

import { useActions, useUIState } from '@ai-toolkit/rsc';

import { generateId } from 'ai-toolkit';

// Allow streaming responses up to 30 seconds

export const maxDuration = 30;

export default function Home() {

const [input, setInput] = useState<string>('');

const [conversation, setConversation] = useUIState();

const { continueConversation } = useActions();

return (

<div>

<div>

{conversation.map((message: ClientMessage) => (

<div key={message.id}>

{message.role}: {message.display}

</div>

))}

</div>

<div>

<input

type="text"

value={input}

onChange={event => {

setInput(event.target.value);

}}

/>

<button

onClick={async () => {

setConversation((currentConversation: ClientMessage[]) => [

...currentConversation,

{ id: generateId(), role: 'user', display: input },

]);

const message = await continueConversation(input);

setConversation((currentConversation: ClientMessage[]) => [

...currentConversation,

message,

]);

}}

>

Send Message

</button>

</div>

</div>

);

}

export async function Stock({ symbol, numOfMonths }) {

const data = await fetch(

https://api.example.com/stock/${symbol}/${numOfMonths},

);

return (

<div>

<div>{symbol}</div>

<div>

{data.timeline.map(data => (

<div>

<div>{data.date}</div>

<div>{data.value}</div>

</div>

))}

</div>

</div>

);

}

export async function Flight({ flightNumber }) {

const data = await fetch(https://api.example.com/flight/${flightNumber});

return (

<div>

<div>{flightNumber}</div>

<div>{data.status}</div>

<div>{data.source}</div>

<div>{data.destination}</div>

</div>

);

}

app/page.tsx
tsx
  1. 1'use client';
  2. 2import { useState } from 'react';
  3. 3import { ClientMessage } from './actions';
  4. 4import { useActions, useUIState } from '@ai-toolkit/rsc';
  5. 5import { generateId } from 'ai-toolkit';
  6. 6// Allow streaming responses up to 30 seconds
  7. 7export const maxDuration = 30;
  8. 8export default function Home() {
  9. 9 const [input, setInput] = useState<string>('');
  10. 10 const [conversation, setConversation] = useUIState();
  11. 11 const { continueConversation } = useActions();
  12. 12 return (
  13. 13 <div>
  14. 14 <div>
  15. 15 {conversation.map((message: ClientMessage) => (
  16. 16 <div key={message.id}>
  17. 17 {message.role}: {message.display}
  18. 18 </div>
  19. 19 ))}
  20. 20 </div>
  21. 21 <div>
  22. 22 <input
  23. 23 type="text"
  24. 24 value={input}
  25. 25 onChange={event => {
  26. 26 setInput(event.target.value);
  27. 27 }}
  28. 28 />
  29. 29 <button
  30. 30 onClick={async () => {
  31. 31 setConversation((currentConversation: ClientMessage[]) => [
  32. 32 ...currentConversation,
  33. 33 { id: generateId(), role: 'user', display: input },
  34. 34 ]);
  35. 35 const message = await continueConversation(input);
  36. 36 setConversation((currentConversation: ClientMessage[]) => [
  37. 37 ...currentConversation,
  38. 38 message,
  39. 39 ]);
  40. 40 }}
  41. 41 >
  42. 42 Send Message
  43. 43 </button>
  44. 44 </div>
  45. 45 </div>
  46. 46 );
  47. 47}
components/stock.tsx
tsx
  1. 1export async function Stock({ symbol, numOfMonths }) {
  2. 2 const data = await fetch(
  3. 3 `https://api.example.com/stock/${symbol}/${numOfMonths}`,
  4. 4 );
  5. 5 return (
  6. 6 <div>
  7. 7 <div>{symbol}</div>
  8. 8 <div>
  9. 9 {data.timeline.map(data => (
  10. 10 <div>
  11. 11 <div>{data.date}</div>
  12. 12 <div>{data.value}</div>
  13. 13 </div>
  14. 14 ))}
  15. 15 </div>
  16. 16 </div>
  17. 17 );
  18. 18}
components/flight.tsx
tsx
  1. 1export async function Flight({ flightNumber }) {
  2. 2 const data = await fetch(`https://api.example.com/flight/${flightNumber}`);
  3. 3 return (
  4. 4 <div>
  5. 5 <div>{flightNumber}</div>
  6. 6 <div>{data.status}</div>
  7. 7 <div>{data.source}</div>
  8. 8 <div>{data.destination}</div>
  9. 9 </div>
  10. 10 );
  11. 11}

Server

'use server';

import { getMutableAIState, streamUI } from '@ai-toolkit/rsc';

import { openai } from '@ai-toolkit/openai';

import { ReactNode } from 'react';

import { z } from 'zod';

import { generateId } from 'ai-toolkit';

import { Stock } from '@/components/stock';

import { Flight } from '@/components/flight';

export interface ServerMessage {

role: 'user' | 'assistant';

content: string;

}

export interface ClientMessage {

id: string;

role: 'user' | 'assistant';

display: ReactNode;

}

export async function continueConversation(

input: string,

): Promise<ClientMessage> {

'use server';

const history = getMutableAIState();

const result = await streamUI({

model: openai('gpt-3.5-turbo'),

messages: [...history.get(), { role: 'user', content: input }],

text: ({ content, done }) => {

if (done) {

history.done((messages: ServerMessage[]) => [

...messages,

{ role: 'assistant', content },

]);

}

return <div>{content}</div>;

},

tools: {

showStockInformation: {

description:

'Get stock information for symbol for the last numOfMonths months',

inputSchema: z.object({

symbol: z

.string()

.describe('The stock symbol to get information for'),

numOfMonths: z

.number()

.describe('The number of months to get historical information for'),

}),

generate: async ({ symbol, numOfMonths }) => {

history.done((messages: ServerMessage[]) => [

...messages,

{

role: 'assistant',

content: Showing stock information for ${symbol},

},

]);

return <Stock symbol={symbol} numOfMonths={numOfMonths} />;

},

},

showFlightStatus: {

description: 'Get the status of a flight',

inputSchema: z.object({

flightNumber: z

.string()

.describe('The flight number to get status for'),

}),

generate: async ({ flightNumber }) => {

history.done((messages: ServerMessage[]) => [

...messages,

{

role: 'assistant',

content: Showing flight status for ${flightNumber},

},

]);

return <Flight flightNumber={flightNumber} />;

},

},

},

});

return {

id: generateId(),

role: 'assistant',

display: result.value,

};

}

import { createAI } from '@ai-toolkit/rsc';

import { ServerMessage, ClientMessage, continueConversation } from './actions';

export const AI = createAI<ServerMessage[], ClientMessage[]>({

actions: {

continueConversation,

},

initialAIState: [],

initialUIState: [],

});

app/actions.tsx
tsx
  1. 1'use server';
  2. 2import { getMutableAIState, streamUI } from '@ai-toolkit/rsc';
  3. 3import { openai } from '@ai-toolkit/openai';
  4. 4import { ReactNode } from 'react';
  5. 5import { z } from 'zod';
  6. 6import { generateId } from 'ai-toolkit';
  7. 7import { Stock } from '@/components/stock';
  8. 8import { Flight } from '@/components/flight';
  9. 9export interface ServerMessage {
  10. 10 role: 'user' | 'assistant';
  11. 11 content: string;
  12. 12}
  13. 13export interface ClientMessage {
  14. 14 id: string;
  15. 15 role: 'user' | 'assistant';
  16. 16 display: ReactNode;
  17. 17}
  18. 18export async function continueConversation(
  19. 19 input: string,
  20. 20): Promise<ClientMessage> {
  21. 21 'use server';
  22. 22 const history = getMutableAIState();
  23. 23 const result = await streamUI({
  24. 24 model: openai('gpt-3.5-turbo'),
  25. 25 messages: [...history.get(), { role: 'user', content: input }],
  26. 26 text: ({ content, done }) => {
  27. 27 if (done) {
  28. 28 history.done((messages: ServerMessage[]) => [
  29. 29 ...messages,
  30. 30 { role: 'assistant', content },
  31. 31 ]);
  32. 32 }
  33. 33 return <div>{content}</div>;
  34. 34 },
  35. 35 tools: {
  36. 36 showStockInformation: {
  37. 37 description:
  38. 38 'Get stock information for symbol for the last numOfMonths months',
  39. 39 inputSchema: z.object({
  40. 40 symbol: z
  41. 41 .string()
  42. 42 .describe('The stock symbol to get information for'),
  43. 43 numOfMonths: z
  44. 44 .number()
  45. 45 .describe('The number of months to get historical information for'),
  46. 46 }),
  47. 47 generate: async ({ symbol, numOfMonths }) => {
  48. 48 history.done((messages: ServerMessage[]) => [
  49. 49 ...messages,
  50. 50 {
  51. 51 role: 'assistant',
  52. 52 content: `Showing stock information for ${symbol}`,
  53. 53 },
  54. 54 ]);
  55. 55 return <Stock symbol={symbol} numOfMonths={numOfMonths} />;
  56. 56 },
  57. 57 },
  58. 58 showFlightStatus: {
  59. 59 description: 'Get the status of a flight',
  60. 60 inputSchema: z.object({
  61. 61 flightNumber: z
  62. 62 .string()
  63. 63 .describe('The flight number to get status for'),
  64. 64 }),
  65. 65 generate: async ({ flightNumber }) => {
  66. 66 history.done((messages: ServerMessage[]) => [
  67. 67 ...messages,
  68. 68 {
  69. 69 role: 'assistant',
  70. 70 content: `Showing flight status for ${flightNumber}`,
  71. 71 },
  72. 72 ]);
  73. 73 return <Flight flightNumber={flightNumber} />;
  74. 74 },
  75. 75 },
  76. 76 },
  77. 77 });
  78. 78 return {
  79. 79 id: generateId(),
  80. 80 role: 'assistant',
  81. 81 display: result.value,
  82. 82 };
  83. 83}
app/ai.ts
typescript
  1. 1import { createAI } from '@ai-toolkit/rsc';
  2. 2import { ServerMessage, ClientMessage, continueConversation } from './actions';
  3. 3export const AI = createAI<ServerMessage[], ClientMessage[]>({
  4. 4 actions: {
  5. 5 continueConversation,
  6. 6 },
  7. 7 initialAIState: [],
  8. 8 initialUIState: [],
  9. 9});