DEV Community

Serif COLAKEL
Serif COLAKEL

Posted on

Building an Advanced AI Chatbot with React and window.ai

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

  1. Demo
  2. Source Code
  3. YouTube Video
  4. Improvements Commit Link

AI Chatbot Demo YouTube Video

AI Chatbot Demo

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Setting up the Project
  4. Defining TypeScript Interfaces
  5. Creating the AIChatBot Component
  6. Implementing Chat Logic
  7. Rendering the Chat Interface
  8. Adding Advanced Features
  9. 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:

  1. Node.js and npm installed on your machine
  2. Basic knowledge of React, TypeScript, and modern JavaScript features
  3. Google Chrome Dev or Canary browser (version 127 or higher)
  4. 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
Enter fullscreen mode Exit fullscreen mode

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>;
      };
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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)
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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:

  1. Error handling and retry mechanism
  2. Typing indicators
  3. Message reactions
  4. 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Implementing user authentication
  2. Adding support for voice input and output
  3. Integrating with a backend to store conversation history
  4. 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!

Top comments (0)