Stream Updates to Visual Interfaces

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

2 min readrscstreaminggenerative user interfaceView source

In our previous example we've been streaming react components from the server to the client. By streaming the components, we open up the possibility to update these components based on state changes that occur in the server.

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>

);

}

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}

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';

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

deploy: {

description: 'Deploy repository to vercel',

inputSchema: z.object({

repositoryName: z

.string()

.describe('The name of the repository, example: vercel/ai-chatbot'),

}),

generate: async function* ({ repositoryName }) {

yield <div>Cloning repository {repositoryName}...</div>; // [!code highlight:5]

await new Promise(resolve => setTimeout(resolve, 3000));

yield <div>Building repository {repositoryName}...</div>;

await new Promise(resolve => setTimeout(resolve, 2000));

return <div>{repositoryName} deployed!</div>;

},

},

},

});

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. 7export interface ServerMessage {
  8. 8 role: 'user' | 'assistant';
  9. 9 content: string;
  10. 10}
  11. 11export interface ClientMessage {
  12. 12 id: string;
  13. 13 role: 'user' | 'assistant';
  14. 14 display: ReactNode;
  15. 15}
  16. 16export async function continueConversation(
  17. 17 input: string,
  18. 18): Promise<ClientMessage> {
  19. 19 'use server';
  20. 20 const history = getMutableAIState();
  21. 21 const result = await streamUI({
  22. 22 model: openai('gpt-3.5-turbo'),
  23. 23 messages: [...history.get(), { role: 'user', content: input }],
  24. 24 text: ({ content, done }) => {
  25. 25 if (done) {
  26. 26 history.done((messages: ServerMessage[]) => [
  27. 27 ...messages,
  28. 28 { role: 'assistant', content },
  29. 29 ]);
  30. 30 }
  31. 31 return <div>{content}</div>;
  32. 32 },
  33. 33 tools: {
  34. 34 deploy: {
  35. 35 description: 'Deploy repository to vercel',
  36. 36 inputSchema: z.object({
  37. 37 repositoryName: z
  38. 38 .string()
  39. 39 .describe('The name of the repository, example: vercel/ai-chatbot'),
  40. 40 }),
  41. 41 generate: async function* ({ repositoryName }) {
  42. 42 yield <div>Cloning repository {repositoryName}...</div>; // [!code highlight:5]
  43. 43 await new Promise(resolve => setTimeout(resolve, 3000));
  44. 44 yield <div>Building repository {repositoryName}...</div>;
  45. 45 await new Promise(resolve => setTimeout(resolve, 2000));
  46. 46 return <div>{repositoryName} deployed!</div>;
  47. 47 },
  48. 48 },
  49. 49 },
  50. 50 });
  51. 51 return {
  52. 52 id: generateId(),
  53. 53 role: 'assistant',
  54. 54 display: result.value,
  55. 55 };
  56. 56}
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});