🔧 Building an Advanced AI Chatbot with React and window.ai
Nachrichtenbereich: 🔧 Programmierung
🔗 Quelle: dev.to
In this comprehensive tutorial, we'll walk through the process of creating a sophisticated AI-powered chatbot using React, TypeScript, and the window.ai API. This chatbot can handle queries from different sectors such as Engineering, Insurance, and Customer Service, providing a versatile and powerful tool for various applications.
Links
AI Chatbot Demo YouTube Video
Table of Contents
- Introduction
- Prerequisites
- Setting up the Project
- Defining TypeScript Interfaces
- Creating the AIChatBot Component
- Implementing Chat Logic
- Rendering the Chat Interface
- Adding Advanced Features
- Conclusion
Introduction
The AI Chatbot we're building is a React component that leverages the window.ai API to provide intelligent responses to user queries. It supports multiple sectors, manages conversation history, and offers a user-friendly interface for seamless interaction.
Prerequisites
Before we start, ensure you have:
- Node.js and npm installed on your machine
- Basic knowledge of React, TypeScript, and modern JavaScript features
- Google Chrome Dev or Canary browser (version 127 or higher)
- A code editor (e.g., Visual Studio Code)
Setting up the Project
Let's start by creating a new React project with TypeScript:
npm create vite@latest my-ai-chatbot --template react-ts
cd ai-chatbot
Follow this Shadcn UI Guide Shadcn UI Guide to install the Shadcn UI components.
Defining TypeScript Interfaces
First, let's define the TypeScript interfaces for our AI chatbot. Create a new file called ai.d.ts
in your src
folder:
// src/ai.d.ts
export interface AITextSessionOptions {
maxTokens: number;
temperature: number;
tokensLeft: number;
tokensSoFar: number;
topK: number;
}
export interface AITextSession {
prompt(input: string): Promise<string>;
promptStreaming(input: string): AsyncIterable<string>;
destroy(): void;
}
export interface Capabilities {
available: "readily" | "after-download" | "no";
defaultTemperature: number;
defaultTopK: number;
maxTopK: number;
}
declare global {
interface Window {
ai: {
assistant: {
capabilities: () => Promise<Capabilities>;
create: (
options?: Partial<AITextSessionOptions>
) => Promise<AITextSession>;
};
};
}
}
These interfaces define the structure of the AI text session, its options, and the capabilities of the AI assistant. We also extend the global Window
interface to include the ai
property, which will be provided by the window.ai API.
Creating the AIChatBot Component
Now, let's create the main component for our AI chatbot. Create a new file called AIChatBot.tsx
in your src
folder:
// src/AIChatBot.tsx
import React, { useState, useEffect, useRef } from "react";
import {
Button,
Input,
Card,
CardContent,
CardHeader,
CardTitle,
Avatar,
AvatarFallback,
} from "./ui-components";
import { Popover, PopoverContent, PopoverTrigger } from "./ui-components";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "./ui-components";
import { ScrollArea } from "./ui-components";
import {
UserIcon,
SendIcon,
XIcon,
TrashIcon,
EllipsisVertical,
Clock,
Bot,
MessageCircle,
FactoryIcon,
} from "lucide-react";
type Conversation = {
message: string;
isUser: boolean;
isError?: boolean;
id: string;
date: string;
};
type AICapabilities = {
isChrome: boolean;
message: string;
state?: {
available: "readily" | "after-download" | "no";
};
};
const sectors = [
{
name: "Engineering",
description: "Technical support for engineering queries.",
},
{
name: "Insurance",
description: "Assistance with insurance-related queries.",
},
{
name: "Customer Service",
description: "General customer service support.",
},
];
const AIChatBot: React.FC = () => {
const [conversation, setConversation] = useState<Conversation[]>([]);
const [capabilities, setCapabilities] = useState<AICapabilities>();
const [message, setMessage] = useState("");
const [isPending, setIsPending] = useState(false);
const [sector, setSector] = useState("");
const chatContainerRef = useRef<HTMLDivElement>(null);
// ... We'll add more code here in the following sections
};
export default AIChatBot;
This sets up the basic structure of our AIChatBot component, including the necessary imports and type definitions.
Implementing Chat Logic
Now, let's add the core functionality to our chatbot:
// ... (previous code)
const AIChatBot: React.FC = () => {
// ... (state variables)
const generateId = () => crypto.randomUUID();
const getCapabilities = async (): Promise<AICapabilities> => {
try {
let isChrome = false;
let message = "";
const state = await window.ai?.assistant.capabilities();
if (navigator.userAgent.includes("Chrome")) {
isChrome = (navigator as any).userAgentData?.brands.some(
(brandInfo: { brand: string }) => brandInfo.brand === "Google Chrome"
);
if (!isChrome) {
message = "Your browser is not supported. Please use Google Chrome Dev or Canary.";
}
} else {
message = "Your browser is not supported. Please use Google Chrome Dev or Canary.";
}
const version = getChromeVersion();
if (version < 127) {
message = "Your browser is not supported. Please update to 127 version or greater.";
}
if (!("ai" in window)) {
message = "Prompt API is not available, check your configuration in chrome://flags/#prompt-api-for-gemini-nano";
}
if (state?.available !== "readily") {
message = "Built-in AI is not ready, check your configuration in chrome://flags/#optimization-guide-on-device-model";
}
return { isChrome, message, state };
} catch (error) {
console.error(error);
return {
isChrome: false,
message: "An error occurred",
state: undefined,
};
}
};
const getChromeVersion = () => {
const raw = navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./);
return raw ? parseInt(raw[2], 10) : 0;
};
const getPrompt = (message: string, oldConversations?: Conversation[]) => {
return `
You are a customer service representative and you are answering questions from our customers. Please follow the rules below and consider the customer's previous conversations to assist the customer.
Previous conversations of the customer:
\${oldConversations?.map(
(conv) =>
`\${conv.isUser ? "Customer Question: " : "Your Answer: "}\${
conv.message
}`
)}
1. Create your response using only HTML elements.
2. You can use the following HTML tags to create your response: <p>, <h1>, <h2>, <h3>, <ul>, <li>, <strong>, <em>, <a>, <code>, <pre>, <img>.
3. Do not use style or class attributes in your response.
4. Create your response within a single root element, such as a <div> tag.
5. Use the href attribute for links and use "#" instead of actual URLs.
6. Respond to the customer's question politely, professionally, and helpfully.
7. If you do not have information about the question asked, kindly state this and direct the customer to another resource that can help.
Here is the customer's question:
\${message}
Please respond to this question in accordance with the rules above and finish the sentence.
`;
};
const onFinish = async () => {
if (!window?.ai || !message) return;
const id = generateId();
try {
setIsPending(true);
setConversation((prev) => [
...prev,
{ message, isUser: true, id, date: new Date().toISOString() },
]);
const session = await window.ai.assistant.create();
const prompt = getPrompt(message, conversation);
const response = await session.prompt(prompt);
setConversation((prev) => [
...prev,
{
message: response,
isUser: false,
id,
date: new Date().toISOString(),
},
]);
session.destroy();
setMessage("");
} catch (error) {
setConversation((prev) => [
...prev,
{
message: "An error occurred.",
isUser: false,
isError: true,
id,
date: new Date().toISOString(),
},
]);
} finally {
setIsPending(false);
}
};
useEffect(() => {
const prepareCapabilities = () => {
getCapabilities().then(setCapabilities);
};
prepareCapabilities();
const interval = setInterval(prepareCapabilities, 60000); // Check every minute
return () => clearInterval(interval);
}, []);
useEffect(() => {
chatContainerRef.current?.scrollTo({
top: chatContainerRef.current.scrollHeight,
behavior: "smooth",
});
}, [conversation]);
// ... (we'll add the return statement in the next section)
};
This section implements the core logic of our chatbot, including:
- Generating unique IDs for messages
- Checking the capabilities of the AI assistant
- Creating prompts for the AI
- Handling the chat interaction (sending messages and receiving responses)
- Managing the conversation state
- Automatically scrolling to the latest message
Rendering the Chat Interface
Now, let's add the JSX to render our chat interface:
// ... (previous code)
const AIChatBot: React.FC = () => {
// ... (previous code)
const handleDeleteAll = () => {
setConversation([]);
setSector("");
};
const getResponseTime = (id: string) => {
const userMessage = conversation.find((conv) => conv.id === id);
const aiMessage = conversation.find(
(conv) => conv.id === id && !conv.isUser
);
if (!userMessage || !aiMessage) return;
const userDate = new Date(userMessage.date);
const aiDate = new Date(aiMessage.date);
const diff = aiDate.getTime() - userDate.getTime();
return ` - Response Time: \${diff}ms (\${diff / 1000}s)`;
};
const renderContent = () => {
if (!sector) {
return (
<div className="flex flex-col items-center justify-center flex-1 h-full my-auto gap-y-4 min-h-[400px]">
<MessageCircle className="w-12 h-12 text-gray-500" />
<p className="px-4 text-sm text-center text-gray-500">
Select a sector to start a conversation. Our AI Chatbot can assist
you with various fields such as Engineering, Insurance, and Customer
Service. Choose the relevant sector to get tailored support and
solutions.
</p>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-x-2">
<Bot className="w-5 h-5" />
Select Sector
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{sectors.map((sector) => (
<DropdownMenuItem
key={sector.name}
onClick={() => setSector(sector.name)}
>
<FactoryIcon className="w-4 h-4 mr-2" />
<span>{sector.name}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
return (
<>
<ScrollArea className="h-[400px] px-2" viewportRef={chatContainerRef}>
<div className="h-full space-y-4">
{conversation?.length > 0 ? (
conversation.map((item, index) => (
<div
key={index}
className={`flex gap-2 \${
item.isUser ? "flex-row-reverse" : "flex-row"
}`}
>
<Avatar>
<AvatarFallback>
{item.isUser ? <UserIcon /> : "AI"}
</AvatarFallback>
</Avatar>
<div>
<div
dangerouslySetInnerHTML={{ __html: item.message }}
className={`rounded-lg p-2 text-xs \${
item.isUser
? "bg-primary text-primary-foreground"
: "bg-muted"
} \${
item.isError &&
"bg-destructive text-destructive-foreground"
}`}
/>
{item.date && (
<div
title={new Date(item.date).toLocaleString()}
className={`flex items-center w-full mt-1 text-xs text-gray-500 \${
item.isUser ? "justify-end" : "justify-start"
}`}
>
<Clock className="inline-block w-4 h-4 mr-1" />
{new Date(item.date).toLocaleTimeString()}
{!item.isUser && getResponseTime(item.id)}
</div>
)}
</div>
{item.isError && (
<Button
variant="ghost"
size="icon"
onClick={() => {
setConversation((prev) =>
prev.filter((conv) => conv.id !== item.id)
);
}}
>
<TrashIcon className="w-4 h-4" />
</Button>
)}
</div>
))
) : (
<div className="flex flex-col items-center justify-center flex-1 h-full my-auto gap-y-4">
<MessageCircle className="w-12 h-12 text-gray-500" />
<p className="text-sm text-gray-500">
Start a conversation with AI Chatbot.
</p>
</div>
)}
</div>
</ScrollArea>
<div className="flex items-center p-2 space-x-2">
<Input
disabled={isPending}
placeholder="Type your message"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyUp={(e) => e.key === "Enter" && onFinish()}
/>
<DropdownMenu>
<div className="flex flex-row ">
<Button
size="icon"
variant="ghost"
disabled={isPending || !message}
onClick={onFinish}
>
<SendIcon className="w-4 h-4 text-primary hover:text-primary/90" />
</Button>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="ghost" disabled={isPending}>
<EllipsisVertical className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
</div>
<DropdownMenuContent>
<DropdownMenuItem
disabled={isPending || conversation.length === 0}
onClick={handleDeleteAll}
>
<TrashIcon className="w-4 h-4 mr-2" />
<span>Delete all messages</span>
</DropdownMenuItem>
<DropdownMenuItem disabled={isPending} onClick={handleDeleteAll}>
<FactoryIcon className="w-4 h-4 mr-2" />
<span>Change sector</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</>
);
};
return (
<Card className="w-[400px] overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between p-2 m-2 space-y-0 rounded-lg bg-primary">
<CardTitle className="text-sm font-medium text-white dark:text-black">
AI Chatbot
</CardTitle>
<Button
variant="ghost"
size="sm"
onClick={() => {
/* Close function */
}}
className="text-white dark:text-black dark:hover:text-white"
>
<XIcon className="w-4 h-4" />
</Button>
</CardHeader>
<CardContent className="p-0 mb-2">{renderContent()}</CardContent>
</Card>
);
};
export default AIChatBot;
This section renders the chat interface, including:
- A dropdown to select the sector
- The conversation history with user and AI messages
- An input field for the user to type messages
- Buttons to send messages, delete the conversation, and change sectors
Adding Advanced Features
To make our chatbot more robust and user-friendly, we can add some advanced features:
- Error handling and retry mechanism
- Typing indicators
- Message reactions
- File uploads
Here's an example of how we could implement a typing indicator:
// Add this to your imports
import { Loader } from "lucide-react";
// Add this state variable
const [isTyping, setIsTyping] = useState(false);
// Modify the onFinish function
const onFinish = async () => {
if (!window?.ai || !message) return;
const id = generateId();
try {
setIsPending(true);
setIsTyping(true);
setConversation((prev) => [
...prev,
{ message, isUser: true, id, date: new Date().toISOString() },
]);
const session = await window.ai.assistant.create();
const prompt = getPrompt(message, conversation);
const response = await session.prompt(prompt);
setIsTyping(false);
setConversation((prev) => [
...prev,
{
message: response,
isUser: false,
id,
date: new Date().toISOString(),
},
]);
session.destroy();
setMessage("");
} catch (error) {
setIsTyping(false);
setConversation((prev) => [
...prev,
{
message: "An error occurred.",
isUser: false,
isError: true,
id,
date: new Date().toISOString(),
},
]);
} finally {
setIsPending(false);
}
};
// Add this to your renderContent function, just before the closing ScrollArea tag
{
isTyping && (
<div className="flex items-center space-x-2 text-gray-500">
<Loader className="w-4 h-4 animate-spin" />
<span>AI is typing...</span>
</div>
);
}
This addition will show a typing indicator while the AI is generating a response, providing better feedback to the user.
Conclusion
In this tutorial, we've built a sophisticated AI Chatbot using React, TypeScript, and the window.ai API. Our chatbot can handle multiple sectors, maintain conversation history, and provide a user-friendly interface for interaction.
Some potential improvements and extensions could include:
- Implementing user authentication
- Adding support for voice input and output
- Integrating with a backend to store conversation history
- Implementing more advanced AI features like sentiment analysis or language translation
Remember to thoroughly test your chatbot and ensure it complies with all relevant privacy and data protection regulations before deploying it in a production environment.
Happy coding!
...