Moving to ai/rsc

Concepts

To go beyond text, we'll be using a concept we explored in an earlier section, tools. Remember, tools are like programs we can give to the model, and the model can decide as and when to use based on the context of the conversation.

With ai/rsc's streamUI function, you can provide tools that return React components, rather than arbitrary values.

With streamUI, the model becomes a dynamic router that is able to understand the users' intention and display the relevant UI as required.

Implementation

In this section, you will update your chatbot to use the ai/rsc library.

Create a Server Action at app/actions.tsx and add the following code:

app/actions.tsx
"use server";

import { createAI, getMutableAIState, streamUI } from "ai/rsc";
import { openai } from "@ai-sdk/openai";
import { ReactNode } from "react";
import { z } from "zod";
import { nanoid } from "nanoid";

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: {}
  });

  return {
    id: nanoid(),
    role: "assistant",
    display: result.value,
  };
}

export const AI = createAI<ServerMessage[], ClientMessage[]>({
  actions: {
    continueConversation,
  },
  initialAIState: [],
  initialUIState: [],
});

Open your root layout (app/layout.tsx) and wrap the children with the AI provider created in the previous step.

app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { AI } from "./actions"; 

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <AI>{children}</AI>
      </body>
    </html>
  );
}

Replace your root page (app/page.tsx) with the following code.

app/page.tsx
"use client";

import { useState } from "react";
import { ClientMessage } from "./actions";
import { useActions, useUIState } from "ai/rsc";
import { nanoid } from "nanoid";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";

export default function Home() {
  const [input, setInput] = useState<string>("");
  const [conversation, setConversation] = useUIState();
  const { continueConversation } = useActions();

  return (
    <div className="max-w-xl mx-auto space-y-4">
      <h1 className="font-semibold text-2xl my-2">Vercel Chatbot!</h1>
      <div className="space-y-4">
        {conversation.map((message: ClientMessage) => (
          <div key={message.id} className="border-t border-border pt-2">
            <div className="font-semibold uppercase text-xs text-neutral-400">
              {message.role}
            </div>
            <div>{message.display}</div>
          </div>
        ))}
      </div>

      <div className="">
        <form
          className="flex"
          onSubmit={async (e) => {
            e.preventDefault();
            setInput("");
            setConversation((currentConversation: ClientMessage[]) => [
              ...currentConversation,
              { id: nanoid(), role: "user", display: input },
            ]);

            const message = await continueConversation(input);

            setConversation((currentConversation: ClientMessage[]) => [
              ...currentConversation,
              message,
            ]);
          }}
        >
          <Input
            autoFocus
            placeholder="Say hello!"
            type="text"
            className="min-w-48"
            value={input}
            onChange={(event) => {
              setInput(event.target.value);
            }}
          />
          <Button>Send Message</Button>
        </form>
      </div>
    </div>
  );
}

Make sure your dev server is running.

npm run dev

Head to localhost:3000 and send a message to your chatbot!