Initial commit
This commit is contained in:
276
docs/examples/chatbot.md
Normal file
276
docs/examples/chatbot.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# Chatbot
|
||||
|
||||
URL: /examples/chatbot
|
||||
|
||||
---
|
||||
|
||||
title: Chatbot
|
||||
description: An example of how to use the AI Elements to build a chatbot.
|
||||
|
||||
---
|
||||
|
||||
An example of how to use the AI Elements to build a chatbot.
|
||||
|
||||
<Preview path="chatbot" type="block" className="p-0" />
|
||||
|
||||
## Tutorial
|
||||
|
||||
Let's walk through how to build a chatbot using AI Elements and AI SDK. Our example will include reasoning, web search with citations, and a model picker.
|
||||
|
||||
### Setup
|
||||
|
||||
First, set up a new Next.js repo and cd into it by running the following command (make sure you choose to use Tailwind the project setup):
|
||||
|
||||
```bash title="Terminal"
|
||||
npx create-next-app@latest ai-chatbot && cd ai-chatbot
|
||||
```
|
||||
|
||||
Run the following command to install AI Elements. This will also set up shadcn/ui if you haven't already configured it:
|
||||
|
||||
```bash title="Terminal"
|
||||
npx ai-elements@latest
|
||||
```
|
||||
|
||||
Now, install the AI SDK dependencies:
|
||||
|
||||
```package-install
|
||||
npm i ai @ai-sdk/react zod
|
||||
```
|
||||
|
||||
In order to use the providers, let's configure an AI Gateway API key. Create a `.env.local` in your root directory and navigate [here](https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%2Fapi-keys&title=Get%20your%20AI%20Gateway%20key) to create a token, then paste it in your `.env.local`.
|
||||
|
||||
We're now ready to start building our app!
|
||||
|
||||
### Client
|
||||
|
||||
In your `app/page.tsx`, replace the code with the file below.
|
||||
|
||||
Here, we use the `PromptInput` component with its compound components to build a rich input experience with file attachments, model picker, and action menu. The input component uses the new `PromptInputMessage` type for handling both text and file attachments.
|
||||
|
||||
The whole chat lives in a `Conversation`. We switch on `message.parts` and render the respective part within `Message`, `Reasoning`, and `Sources`. We also use `status` from `useChat` to stream reasoning tokens, as well as render `Loader`.
|
||||
|
||||
```tsx title="app/page.tsx"
|
||||
"use client";
|
||||
|
||||
import { Conversation, ConversationContent, ConversationScrollButton } from "@/components/ai-elements/conversation";
|
||||
import { Message, MessageContent } from "@/components/ai-elements/message";
|
||||
import {
|
||||
PromptInput,
|
||||
PromptInputActionAddAttachments,
|
||||
PromptInputActionMenu,
|
||||
PromptInputActionMenuContent,
|
||||
PromptInputActionMenuTrigger,
|
||||
PromptInputAttachment,
|
||||
PromptInputAttachments,
|
||||
PromptInputBody,
|
||||
PromptInputButton,
|
||||
PromptInputHeader,
|
||||
type PromptInputMessage,
|
||||
PromptInputModelSelect,
|
||||
PromptInputModelSelectContent,
|
||||
PromptInputModelSelectItem,
|
||||
PromptInputModelSelectTrigger,
|
||||
PromptInputModelSelectValue,
|
||||
PromptInputSubmit,
|
||||
PromptInputTextarea,
|
||||
PromptInputFooter,
|
||||
PromptInputTools,
|
||||
} from "@/components/ai-elements/prompt-input";
|
||||
import { Action, Actions } from "@/components/ai-elements/actions";
|
||||
import { Fragment, useState } from "react";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { Response } from "@/components/ai-elements/response";
|
||||
import { CopyIcon, GlobeIcon, RefreshCcwIcon } from "lucide-react";
|
||||
import { Source, Sources, SourcesContent, SourcesTrigger } from "@/components/ai-elements/sources";
|
||||
import { Reasoning, ReasoningContent, ReasoningTrigger } from "@/components/ai-elements/reasoning";
|
||||
import { Loader } from "@/components/ai-elements/loader";
|
||||
|
||||
const models = [
|
||||
{
|
||||
name: "GPT 4o",
|
||||
value: "openai/gpt-4o",
|
||||
},
|
||||
{
|
||||
name: "Deepseek R1",
|
||||
value: "deepseek/deepseek-r1",
|
||||
},
|
||||
];
|
||||
|
||||
const ChatBotDemo = () => {
|
||||
const [input, setInput] = useState("");
|
||||
const [model, setModel] = useState<string>(models[0].value);
|
||||
const [webSearch, setWebSearch] = useState(false);
|
||||
const { messages, sendMessage, status, regenerate } = useChat();
|
||||
|
||||
const handleSubmit = (message: PromptInputMessage) => {
|
||||
const hasText = Boolean(message.text);
|
||||
const hasAttachments = Boolean(message.files?.length);
|
||||
|
||||
if (!(hasText || hasAttachments)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendMessage(
|
||||
{
|
||||
text: message.text || "Sent with attachments",
|
||||
files: message.files,
|
||||
},
|
||||
{
|
||||
body: {
|
||||
model: model,
|
||||
webSearch: webSearch,
|
||||
},
|
||||
}
|
||||
);
|
||||
setInput("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6 relative size-full h-screen">
|
||||
<div className="flex flex-col h-full">
|
||||
<Conversation className="h-full">
|
||||
<ConversationContent>
|
||||
{messages.map((message) => (
|
||||
<div key={message.id}>
|
||||
{message.role === "assistant" && message.parts.filter((part) => part.type === "source-url").length > 0 && (
|
||||
<Sources>
|
||||
<SourcesTrigger count={message.parts.filter((part) => part.type === "source-url").length} />
|
||||
{message.parts
|
||||
.filter((part) => part.type === "source-url")
|
||||
.map((part, i) => (
|
||||
<SourcesContent key={`${message.id}-${i}`}>
|
||||
<Source key={`${message.id}-${i}`} href={part.url} title={part.url} />
|
||||
</SourcesContent>
|
||||
))}
|
||||
</Sources>
|
||||
)}
|
||||
{message.parts.map((part, i) => {
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
return (
|
||||
<Fragment key={`${message.id}-${i}`}>
|
||||
<Message from={message.role}>
|
||||
<MessageContent>
|
||||
<Response>{part.text}</Response>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
{message.role === "assistant" && i === messages.length - 1 && (
|
||||
<Actions className="mt-2">
|
||||
<Action onClick={() => regenerate()} label="Retry">
|
||||
<RefreshCcwIcon className="size-3" />
|
||||
</Action>
|
||||
<Action onClick={() => navigator.clipboard.writeText(part.text)} label="Copy">
|
||||
<CopyIcon className="size-3" />
|
||||
</Action>
|
||||
</Actions>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
case "reasoning":
|
||||
return (
|
||||
<Reasoning
|
||||
key={`${message.id}-${i}`}
|
||||
className="w-full"
|
||||
isStreaming={
|
||||
status === "streaming" && i === message.parts.length - 1 && message.id === messages.at(-1)?.id
|
||||
}
|
||||
>
|
||||
<ReasoningTrigger />
|
||||
<ReasoningContent>{part.text}</ReasoningContent>
|
||||
</Reasoning>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
{status === "submitted" && <Loader />}
|
||||
</ConversationContent>
|
||||
<ConversationScrollButton />
|
||||
</Conversation>
|
||||
|
||||
<PromptInput onSubmit={handleSubmit} className="mt-4" globalDrop multiple>
|
||||
<PromptInputHeader>
|
||||
<PromptInputAttachments>{(attachment) => <PromptInputAttachment data={attachment} />}</PromptInputAttachments>
|
||||
</PromptInputHeader>
|
||||
<PromptInputBody>
|
||||
<PromptInputTextarea onChange={(e) => setInput(e.target.value)} value={input} />
|
||||
</PromptInputBody>
|
||||
<PromptInputFooter>
|
||||
<PromptInputTools>
|
||||
<PromptInputActionMenu>
|
||||
<PromptInputActionMenuTrigger />
|
||||
<PromptInputActionMenuContent>
|
||||
<PromptInputActionAddAttachments />
|
||||
</PromptInputActionMenuContent>
|
||||
</PromptInputActionMenu>
|
||||
<PromptInputButton variant={webSearch ? "default" : "ghost"} onClick={() => setWebSearch(!webSearch)}>
|
||||
<GlobeIcon size={16} />
|
||||
<span>Search</span>
|
||||
</PromptInputButton>
|
||||
<PromptInputModelSelect
|
||||
onValueChange={(value) => {
|
||||
setModel(value);
|
||||
}}
|
||||
value={model}
|
||||
>
|
||||
<PromptInputModelSelectTrigger>
|
||||
<PromptInputModelSelectValue />
|
||||
</PromptInputModelSelectTrigger>
|
||||
<PromptInputModelSelectContent>
|
||||
{models.map((model) => (
|
||||
<PromptInputModelSelectItem key={model.value} value={model.value}>
|
||||
{model.name}
|
||||
</PromptInputModelSelectItem>
|
||||
))}
|
||||
</PromptInputModelSelectContent>
|
||||
</PromptInputModelSelect>
|
||||
</PromptInputTools>
|
||||
<PromptInputSubmit disabled={!input && !status} status={status} />
|
||||
</PromptInputFooter>
|
||||
</PromptInput>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatBotDemo;
|
||||
```
|
||||
|
||||
### Server
|
||||
|
||||
Create a new route handler `app/api/chat/route.ts` and paste in the following code. We're using `perplexity/sonar` for web search because by default the model returns search results. We also pass `sendSources` and `sendReasoning` to `toUIMessageStreamResponse` in order to receive as parts on the frontend. The handler now also accepts file attachments from the client.
|
||||
|
||||
```ts title="app/api/chat/route.ts"
|
||||
import { streamText, UIMessage, convertToModelMessages } from "ai";
|
||||
|
||||
// Allow streaming responses up to 30 seconds
|
||||
export const maxDuration = 30;
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const {
|
||||
messages,
|
||||
model,
|
||||
webSearch,
|
||||
}: {
|
||||
messages: UIMessage[];
|
||||
model: string;
|
||||
webSearch: boolean;
|
||||
} = await req.json();
|
||||
|
||||
const result = streamText({
|
||||
model: webSearch ? "perplexity/sonar" : model,
|
||||
messages: convertToModelMessages(messages),
|
||||
system: "You are a helpful assistant that can answer questions and help with tasks",
|
||||
});
|
||||
|
||||
// send sources and reasoning back to the client
|
||||
return result.toUIMessageStreamResponse({
|
||||
sendSources: true,
|
||||
sendReasoning: true,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
You now have a working chatbot app with file attachment support! The chatbot can handle both text and file inputs through the action menu. Feel free to explore other components like [`Tool`](/elements/components/tool) or [`Task`](/elements/components/task) to extend your app, or view the other examples.
|
||||
Reference in New Issue
Block a user