Real world Components, tutorials and guides for modern React.js & Next.js development.

Build an AI Chatbot with Gemini 2.0 Flash in Next.js

AIChatbotNext.jsGeminiGemini Flash

A simple React component and Next.js API route to create a chatbot with product preview cards using Gemini 2.0 Flash.

I've been experimenting with Google's Gemini 2.0 Flash model, and I have to say—it's seriously impressive. It’s blazing fast, cost-effective, and comes with massive context windows. So, I put together a quick tutorial on how to use it in a simple chatbot. I also dive into tokenization to swap out response IDs with preview cards, which looks pretty damn cool.

Here's what we'll build:

Find-a-Shoe

Hey there👋 I'm here to help you find the perfect shoe for your needs. What are you looking for?

Note: The above is only a demo to prevent misuse (and having a massive Google API bill).

A Quick Note About Gemini: At the time of writing, Gemini (especially new releases like Flash 2.0) can be under heavy load. You might experience occasional errors or slowdowns. Don't be discouraged – this is expected with new and popular AI models!

Why Gemini 2.0 Flash?

Gemini 2.0 Flash is a powerful and efficient language model that's perfect for chatbot applications. It's designed for speed and responsiveness, making it ideal for real-time conversations. Not to mention that it's far cheaper than other available models. Check out this Output speed vs. Price comparison.

Project Setup

This tutorial assumes you have a basic understanding of Next.js and have a development environment set up (Node.js, npm or yarn).

Getting Your Gemini API Key

  1. Go to: https://aistudio.google.com/
  2. Click on "Get API Key" in the top left corner.
  3. Follow the prompts to create a new project and generate an API key.

The Code: A Step-by-Step Guide

Let's dive into the code for our Chatbot component. We'll break down each part to understand how it works.

1. Sample data

The below is some sample data we'll be using in this demo. For a real world implementation we'd use results from an API.

  • SAMPLE_FAQ: An array of objects representing frequently asked questions and their answers. This data will be used to inform the chatbot's responses.
  • SAMPLE_PRODUCTS: An array of objects representing the products in our online shoe store. This data will be used to provide details about specific shoes.
  • INITIAL_MESSAGES: An array containing the initial welcome message from the chatbot.

Later we'll import and use this data.

// /data.js
// Sample FAQ data for our demo shoe store
const SAMPLE_FAQ = [
  {
    question: "What is your return policy?",
    answer:
      "We offer a 30-day return policy on unworn shoes in their original packaging.  See our full Return & Exchange policy for details and exclusions.",
  },
  {
    question: "How do I find my correct shoe size?",
    answer:
      "We have a comprehensive size guide available on our website. You can find it [link to size guide]. We recommend measuring your foot in socks at the end of the day for the most accurate measurement.",
  },
  {
    question: "Do you offer free shipping?",
    answer:
      "Yes! We offer free standard shipping on all orders over $75 within the continental United States.  Expedited shipping options are available for an additional fee.",
  },
  {
    question: "What payment methods do you accept?",
    answer:
      "We accept all major credit cards (Visa, Mastercard, American Express, Discover), PayPal, and Apple Pay.",
  },
  {
    question: "How do I care for my leather shoes?",
    answer:
      "For leather shoes, we recommend using a leather cleaner and conditioner regularly.  Avoid getting them excessively wet.  For suede, use a suede brush and protector spray.",
  },
  {
    question: "Are your shoes ethically sourced?",
    answer:
      "We are committed to ethical sourcing and work closely with our manufacturers to ensure fair labor practices and environmentally responsible production.  We are continuously working to improve our sustainability efforts.",
  },
  {
    question: "How long will it take to receive my order?",
    answer:
      "Standard shipping typically takes 3-7 business days within the continental US.  Expedited shipping options are available for faster delivery.",
  },
  {
    question: "Do you ship internationally?",
    answer:
      "Yes, we ship to select countries internationally. Shipping costs and delivery times vary depending on the destination. Please see our shipping policy for more details.",
  },
];
 
// Sample product data for our demo shoe store
const SAMPLE_PRODUCTS = [
  {
    id: "1001",
    name: "Cloudfoam Racer TR21",
    brand: "Adidas",
    category: "Running Shoes",
    description:
      "Lightweight running shoe with Cloudfoam cushioning for superior comfort. Ideal for everyday runs and gym workouts.",
    price: 79.99,
    imageUrl:
      "https://placehold.co/300x200/000000/FFFFFF?text=Adidas+Running+Shoe", // Replace with actual image URL
    colors: ["Black/White", "Grey/Blue", "Navy/Red"],
    sizes: [7, 8, 9, 10, 11, 12],
    rating: 4.5,
    numReviews: 125,
  },
  {
    id: "1002",
    name: "Chuck Taylor All Star",
    brand: "Converse",
    category: "Casual Sneakers",
    description:
      "The iconic Chuck Taylor All Star. A timeless classic for everyday style.",
    price: 60.0,
    imageUrl:
      "https://placehold.co/300x200/000000/FFFFFF?text=Converse+Chuck+Taylor", // Replace with actual image URL
    colors: ["Black", "White", "Red", "Navy"],
    sizes: [5, 6, 7, 8, 9, 10, 11],
    rating: 4.7,
    numReviews: 280,
  },
  {
    id: "1003",
    name: "Air Max 90",
    brand: "Nike",
    category: "Sneakers",
    description:
      "The Nike Air Max 90 stays true to its OG running roots with the iconic Waffle sole, stitched overlays and classic TPU accents. Classic looks meet classic comfort.",
    price: 130.0,
    imageUrl: "https://placehold.co/300x200/000000/FFFFFF?text=Nike+Air+Max+90", // Replace with actual image URL
    colors: ["White/Red/Black", "Grey/Blue", "Black/White"],
    sizes: [8, 9, 10, 11, 12, 13],
    rating: 4.8,
    numReviews: 350,
  },
  {
    id: "1004",
    name: "Arizona Sandal",
    brand: "Birkenstock",
    category: "Sandals",
    description:
      "The classic Birkenstock Arizona sandal. Known for its comfort and support.",
    price: 99.95,
    imageUrl:
      "https://placehold.co/300x200/000000/FFFFFF?text=Birkenstock+Arizona", // Replace with actual image URL
    colors: ["Brown", "Black", "White", "Taupe"],
    sizes: [36, 37, 38, 39, 40, 41, 42], // EU sizing
    rating: 4.6,
    numReviews: 185,
  },
  {
    id: "1005",
    name: "Newton Ridge Plus II",
    brand: "Columbia",
    category: "Hiking Boots",
    description:
      "Durable and waterproof hiking boots for all-terrain adventures.",
    price: 110.0,
    imageUrl:
      "https://placehold.co/300x200/000000/FFFFFF?text=Columbia+Hiking+Boots", // Replace with actual image URL
    colors: ["Brown/Green", "Black/Grey"],
    sizes: [7, 8, 9, 10, 11, 12, 13],
    rating: 4.4,
    numReviews: 90,
  },
  {
    id: "1006",
    name: "Classic Leather",
    brand: "Reebok",
    category: "Casual Sneakers",
    description:
      "The Reebok Classic Leather is a timeless icon, offering simple style and all-day comfort.",
    price: 75.0,
    imageUrl:
      "https://placehold.co/300x200/000000/FFFFFF?text=Reebok+Classic+Leather", // Replace with actual image URL
    colors: ["White/Grey", "Black/White", "Navy/White"],
    sizes: [6, 7, 8, 9, 10, 11, 12],
    rating: 4.3,
    numReviews: 112,
  },
  {
    id: "1007",
    name: "Gel-Kayano 28",
    brand: "ASICS",
    category: "Running Shoes",
    description:
      "The GEL-KAYANO® 28 shoe creates a stable stride that moves you towards a balanced mindset. Featuring a lower-profile external heel counter, this piece cradles your foot with improved rearfoot support.  FLYTEFOAM™ Blast cushioning keeps the shoe lightweight, while creating a springy rebound.",
    price: 160.0,
    imageUrl:
      "https://placehold.co/300x200/000000/FFFFFF?text=ASICS+Gel-Kayano+28", // Replace with actual image URL
    colors: ["Blue/White", "Grey/Black", "Red/Black"],
    sizes: [7, 7.5, 8, 8.5, 9, 9.5, 10, 10.5, 11, 11.5, 12],
    rating: 4.9,
    numReviews: 410,
  },
];
 
// A welcome message for the chatbot
const INITIAL_MESSAGES = [
  {
    role: "assistant",
    content:
      "Hey there👋 I'm here to help you find the perfect shoe for your needs. What are you looking for?",
  },
];

2. CardPreview Component

This component is responsible for rendering a preview card for a specific shoe product. It takes a productId as a prop, finds the corresponding product in the SAMPLE_PRODUCTS array, and displays its image, name, category, price, and a short description.

// /components/CardPreview.jsx
function CardPreview({ productId }) {
  const product = SAMPLE_PRODUCTS.find((p) => p.id === productId);
  if (!product) return null;
 
  return (
    <div className="border-2 cursor-pointer bg-white p-4 mt-2 mb-4 flex gap-4 max-w-[400px] shadow-[6px_6px_0_0_#000] active:shadow-[1px_1px_0_0_#000] hover:shadow-[8px_8px_0_0_#000] transition-all duration-300">
      <img
        src={product.imageUrl}
        alt={product.name}
        className="w-24 h-24 object-cover"
      />
      <div className="flex-1">
        <h3 className="font-bold">{product.name}</h3>
        <p className="text-sm text-gray-600">{product.category}</p>
        <p className="font-semibold">${product.price.toFixed(2)}</p>
        <p className="text-sm text-gray-700 mt-1">
          {product.description.slice(0, 100)}
          {product.description.length > 100 ? "..." : ""}
        </p>
      </div>
    </div>
  );
}

3. MessageContent Component

This component is responsible for rendering the content of a chat message. It's designed to detect product IDs embedded in the chatbot's response (in the format %shoeId%) and replace them with the CardPreview component. This allows the chatbot to display interactive product cards within the conversation.

// /components/MessageContent.jsx
function MessageContent({ content }) {
  // Split content by product ID pattern
  const parts = content.split(/(%[\d]+%)/);
 
  return (
    <>
      {parts.map((part, index) => {
        // Check if this part matches the product ID pattern
        const match = part.match(/^%([\d]+)%$/);
        if (match) {
          // If it's a product ID, render the CardPreview
          return <CardPreview key={index} productId={match[1]} />;
        }
        // Otherwise render the text
        return <span key={index}>{part}</span>;
      })}
    </>
  );
}

4. Chatbot Component (Main Component)

This is the main component that orchestrates the chatbot functionality.

  • State Management:
    • messages: An array of objects representing the chat messages. Each message has a role (either "user" or "assistant") and content.
    • input: The text currently entered in the input field.
    • isLoading: A boolean indicating whether the chatbot is currently processing a request.
  • Refs:
    • messagesContainerRef: A ref used to scroll the chat container to the bottom when new messages are added.
    • useEffect Hook: This hook is used to automatically scroll the chat container to the bottom whenever the messages state changes.
    • sendMessage Function: This is the core function that sends the user's message to the Gemini API and handles the response.
// /components/Chatbot.jsx
import { useState, useRef, useEffect } from "react";
 
export default function Chatbot() {
  // State for managing chat messages, user input, and loading state
  const [messages, setMessages] = useState(INITIAL_MESSAGES);
  const [input, setInput] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  // Ref for auto-scrolling to the latest message
  const messagesContainerRef = useRef(null);
 
  /**
   * Scrolls the messages container to the bottom
   */
  const scrollToBottom = () => {
    if (messagesContainerRef.current) {
      messagesContainerRef.current.scroll({
        top: messagesContainerRef.current.scrollHeight,
        behavior: "smooth",
      });
    }
  };
 
  /**
   * Sends a message to the Gemini API and handles the response streaming
   * (Next.js API Route Implementation - RECOMMENDED FOR PRODUCTION)
   * @param {string} userInput - The message text from the user
   */
  async function sendMessage(userInput) {
    // Don't process empty messages
    if (!userInput.trim()) return;
 
    // Add user's message to the chat
    setMessages((prev) => [...prev, { role: "user", content: userInput }]);
    setInput(""); // Clear input field
    setIsLoading(true); // Show loading state
 
    try {
      // Set up Gemini API request
      const url = `/api/chat`; //  POST route we'll create
 
      // Make API request to Gemini
      const response = await fetch(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          message: userInput, // Send the user's message
        }),
      });
 
      if (!response.ok) throw new Error("Network response was not ok");
 
      // Parse the response
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buffer = "";
 
      while (true) {
        const { done, value } = await reader.read();
        if (done) {
          break;
        }
        buffer += decoder.decode(value);
        setMessages((prev) => [
          ...prev,
          { role: "assistant", content: buffer },
        ]);
      }
    } catch (error) {
      // Handle any errors that occur during the API call
      console.error("Error:", error);
      setMessages((prev) => [
        ...prev,
        {
          role: "assistant",
          content: "Sorry, I encountered an error. Please try again.",
        },
      ]);
    } finally {
      // Reset loading state whether the request succeeded or failed
      setIsLoading(false);
    }
  }
 
  return (
    <div className="flex flex-col h-[500px] w-full max-w-2xl mx-auto border-2">
      {/* Chat messages container */}
      <div
        ref={messagesContainerRef}
        className="flex-1 overflow-y-auto p-4 space-y-4"
      >
        {messages.map((message, index) => (
          <div
            key={index}
            className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}
          >
            <div
              className={`max-w-[80%] p-3 ${
                message.role === "user"
                  ? "bg-black text-white"
                  : "bg-gray-100 text-gray-800"
              }`}
            >
              {/* Render message content with product previews for assistant messages */}
              {message.role === "assistant" ? (
                <MessageContent content={message.content} />
              ) : (
                message.content
              )}
            </div>
          </div>
        ))}
      </div>
 
      {/* Chat input form */}
      <div className="border-t-2 p-4">
        <form
          onSubmit={(e) => {
            e.preventDefault();
            sendMessage(input);
          }}
          className="flex flex-col sm:flex-row gap-2"
        >
          <input
            type="text"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="Ask about our shoes..."
            className="flex-1 p-2 border-2"
            disabled={isLoading}
          />
          <button
            type="submit"
            disabled={isLoading}
            className="px-4 py-2 cursor-pointer bg-black text-white hover:bg-black/90 disabled:bg-black/50"
          >
            Send
          </button>
        </form>
      </div>
    </div>
  );
}
Inside sendMessage:
  1. Input Validation: Checks if the user input is empty.
  2. Update Chat State: Adds the user's message to the messages array and clears the input field.
  3. API Request:
    • Sets the API endpoint URL to /api/chat, a POST route in your application.
    • Uses fetch to make a POST request to the API route. The request includes the user's message.
  4. Handle API Response:
    • Handles streaming the response from the API route. It reads the response body chunk by chunk, decodes it, and updates the messages state with the accumulated response.
  5. Error Handling: Catches any errors that occur during the API call and displays an error message to the user.
  6. Loading State: Sets the isLoading state to true before the API call and false after, regardless of whether the call succeeds or fails.

5. Rendering the Chat Interface

The return statement in the Chatbot component renders the chat interface. It includes:

  • A container for the chat messages.
  • A loop that iterates over the messages array and displays each message.
  • An input field for the user to type their messages.
  • A "Send" button that triggers the sendMessage function.

Creating the /api/chat Route

To protect your API key and handle the Gemini API request securely, you should create an API route in your Next.js application. Here's an example of how to create a POST /api/chat route:

// pages/api/chat.js
import { StreamingTextResponse } from 'ai';
import { INITIAL_MESSAGES, SAMPLE_PRODUCTS, SAMPLE_FAQ } from "../data";
 
export async function POST(req) {
    if (!process.env.GOOGLE_API_KEY) {
        throw new Error('GOOGLE_API_KEY is not set');
    }
 
    const { message } = await req.json();
    const GEMINI_API_KEY = process.env.GOOGLE_API_KEY;
    const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`;
 
    const systemPrompt = `You are a helpful assistant for an online shoe store. Use the following product catalog and FAQ information to assist customers:
 
Product Catalog:
${JSON.stringify(SAMPLE_PRODUCTS, null, 2)}
 
FAQ Information:
${JSON.stringify(SAMPLE_FAQ, null, 2)}
 
Important: When referring to specific shoes, include the shoe ID in the format %shoeId%. For example, if discussing the Nike Air Max 90, include %1003% in your response.
 
Please provide helpful, friendly responses about our shoes, sizing, shipping, returns, and any other customer inquiries. If a customer asks about specific shoes, make sure to reference them using the ID format mentioned above.`;
 
    const fullPrompt = `${systemPrompt}\n\nCustomer: ${message}\nAssistant:`;
 
    const response = await fetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            contents: [{
                parts: [{
                    text: fullPrompt
                }]
            })
    });
 
    const data = await response.json();
    const text = data?.candidates?.[0]?.content?.parts?.[0]?.text;
 
    if (!text) {
        throw new Error('No response from Gemini');
    }
 
    // Create a stream from the response text
    const stream = new ReadableStream({
        start(controller) {
            const encoder = new TextEncoder();
            controller.enqueue(encoder.encode(text));
            controller.close();
        },
    });
 
    return new StreamingTextResponse(stream);
}

The Full Code

If you're looking for a quick copy paste solution, here you go :]

Chatbot.jsx

"use client";
 
import { useState, useRef } from "react";
 
// Product ID's returned from the Gemini API response (detailed in prompt) will be replaced with this CardPreview component
function CardPreview({ productId }) {
  const product = SAMPLE_PRODUCTS.find((p) => p.id === productId);
  if (!product) return null;
 
  return (
    <div className="border-2 cursor-pointer bg-white p-4 mt-4 mb-6 flex gap-4 max-w-[400px] shadow-[6px_6px_0_0_#000] active:shadow-[1px_1px_0_0_#000] hover:shadow-[8px_8px_0_0_#000] transition-all duration-300">
      <img
        src={product.imageUrl}
        alt={product.name}
        className="w-24 h-24 object-cover"
      />
      <div className="flex-1">
        <h3 className="font-bold">{product.name}</h3>
        <p className="text-sm text-gray-600">{product.category}</p>
        <p className="font-semibold">${product.price.toFixed(2)}</p>
        <p className="text-sm text-gray-700 mt-1">
          {product.description.slice(0, 100)}
          {product.description.length > 100 ? "..." : ""}
        </p>
      </div>
    </div>
  );
}
 
// Uses regex to find product ID's in the Gemini API response and replace them with the product CardPreview component
function MessageContent({ content }) {
  // Split content by product ID pattern
  const parts = content.split(/(%[\d]+%)/);
 
  return (
    <>
      {parts.map((part, index) => {
        // Check if this part matches the product ID pattern
        const match = part.match(/^%([\d]+)%$/);
        if (match) {
          // If it's a product ID, render the CardPreview
          return <CardPreview key={index} productId={match[1]} />;
        }
        // Otherwise render the text
        return <span key={index}>{part}</span>;
      })}
    </>
  );
}
 
// Main component for the chatbot
export default function Chatbot() {
  // State for managing chat messages, user input, and loading state
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState(DEMO_USER_MESSAGE[0].content);
  const [isLoading, setIsLoading] = useState(false);
  // Ref for auto-scrolling to the latest message
  const messagesContainerRef = useRef(null);
 
  /**
   * Scrolls the messages container to the bottom
   */
  const scrollToBottom = () => {
    if (messagesContainerRef.current) {
      messagesContainerRef.current.scroll({
        top: messagesContainerRef.current.scrollHeight,
        behavior: "smooth",
      });
    }
  };
 
  /**
   * Sends a message to the Gemini API and handles the response streaming
   * (Next.js API Route Implementation - RECOMMENDED FOR PRODUCTION)
   * @param {string} userInput - The message text from the user
   */
  async function sendMessage(userInput) {
    // Don't process empty messages
    if (!userInput.trim()) return;
 
    // Add user's message to the chat
    setMessages((prev) => [...prev, { role: "user", content: userInput }]);
    setInput(""); // Clear input field
    setIsLoading(true); // Show loading state
 
    try {
      // Set up Gemini API request
      const url = `/api/chat`; //  POST route we'll create
 
      // Make API request to Gemini
      const response = await fetch(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          message: userInput, // Send the user's message
        }),
      });
 
      if (!response.ok) throw new Error("Network response was not ok");
 
      // Parse the response
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buffer = "";
 
      while (true) {
        const { done, value } = await reader.read();
        if (done) {
          break;
        }
        buffer += decoder.decode(value);
        setMessages((prev) => [
          ...prev,
          { role: "assistant", content: buffer },
        ]);
      }
    } catch (error) {
      // Handle any errors that occur during the API call
      console.error("Error:", error);
      setMessages((prev) => [
        ...prev,
        {
          role: "assistant",
          content: "Sorry, I encountered an error. Please try again.",
        },
      ]);
    } finally {
      // Reset loading state whether the request succeeded or failed
      setIsLoading(false);
    }
  }
 
  return (
    <div className="flex flex-col h-[500px] w-full max-w-2xl mx-auto border-2">
      {/* Chat messages container */}
      <div
        ref={messagesContainerRef}
        className="flex-1 overflow-y-auto p-4 space-y-4"
      >
        {messages.map((message, index) => (
          <div
            key={index}
            className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}
          >
            <div
              className={`max-w-[80%] p-3 whitespace-pre-line ${
                message.role === "user"
                  ? "bg-black text-white"
                  : "bg-gray-100 text-gray-800"
              }`}
            >
              {/* Render message content with product previews for assistant messages */}
              {message.role === "assistant" ? (
                <MessageContent content={message.content} />
              ) : (
                message.content
              )}
            </div>
          </div>
        ))}
      </div>
 
      {/* Chat input form */}
      <div className="border-t-2 p-4">
        <form
          onSubmit={(e) => {
            e.preventDefault();
            sendMessage(input);
          }}
          className="flex flex-col sm:flex-row gap-2"
        >
          <input
            type="text"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="Ask about our shoes..."
            className="flex-1 p-2 border-2"
            disabled={isLoading}
          />
          <button
            type="submit"
            disabled={isLoading}
            className="px-4 py-2 cursor-pointer bg-black text-white hover:bg-black/90 disabled:bg-black/50 font-mono uppercase"
          >
            Send
          </button>
        </form>
      </div>
    </div>
  );
}

/api/chat/route.js

npm install ai
// pages/api/chat.js
import { StreamingTextResponse } from 'ai';
import { INITIAL_MESSAGES, SAMPLE_PRODUCTS, SAMPLE_FAQ } from "../data";
 
export async function POST(req) {
    if (!process.env.GOOGLE_API_KEY) {
        throw new Error('GOOGLE_API_KEY is not set');
    }
 
    const { message } = await req.json();
    const GEMINI_API_KEY = process.env.GOOGLE_API_KEY;
    const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`;
 
    const systemPrompt = `You are a helpful assistant for an online shoe store. Use the following product catalog and FAQ information to assist customers:
 
Product Catalog:
${JSON.stringify(SAMPLE_PRODUCTS, null, 2)}
 
FAQ Information:
${JSON.stringify(SAMPLE_FAQ, null, 2)}
 
Important: When referring to specific shoes, include the shoe ID in the format %shoeId%. For example, if discussing the Nike Air Max 90, include %1003% in your response.
 
Please provide helpful, friendly responses about our shoes, sizing, shipping, returns, and any other customer inquiries. If a customer asks about specific shoes, make sure to reference them using the ID format mentioned above.`;
 
    const fullPrompt = `${systemPrompt}\n\nCustomer: ${message}\nAssistant:`;
 
    const response = await fetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            contents: [{
                parts: [{
                    text: fullPrompt
                }]
            })
    });
 
    const data = await response.json();
    const text = data?.candidates?.[0]?.content?.parts?.[0]?.text;
 
    if (!text) {
        throw new Error('No response from Gemini');
    }
 
    // Create a stream from the response text
    const stream = new ReadableStream({
        start(controller) {
            const encoder = new TextEncoder();
            controller.enqueue(encoder.encode(text));
            controller.close();
        },
    });
 
    return new StreamingTextResponse(stream);
}

data.js

// /data.js
// Sample FAQ data for our demo shoe store
const SAMPLE_FAQ = [
  {
    question: "What is your return policy?",
    answer:
      "We offer a 30-day return policy on unworn shoes in their original packaging.  See our full Return & Exchange policy for details and exclusions.",
  },
  {
    question: "How do I find my correct shoe size?",
    answer:
      "We have a comprehensive size guide available on our website. You can find it [link to size guide]. We recommend measuring your foot in socks at the end of the day for the most accurate measurement.",
  },
  {
    question: "Do you offer free shipping?",
    answer:
      "Yes! We offer free standard shipping on all orders over $75 within the continental United States.  Expedited shipping options are available for an additional fee.",
  },
  {
    question: "What payment methods do you accept?",
    answer:
      "We accept all major credit cards (Visa, Mastercard, American Express, Discover), PayPal, and Apple Pay.",
  },
  {
    question: "How do I care for my leather shoes?",
    answer:
      "For leather shoes, we recommend using a leather cleaner and conditioner regularly.  Avoid getting them excessively wet.  For suede, use a suede brush and protector spray.",
  },
  {
    question: "Are your shoes ethically sourced?",
    answer:
      "We are committed to ethical sourcing and work closely with our manufacturers to ensure fair labor practices and environmentally responsible production.  We are continuously working to improve our sustainability efforts.",
  },
  {
    question: "How long will it take to receive my order?",
    answer:
      "Standard shipping typically takes 3-7 business days within the continental US.  Expedited shipping options are available for faster delivery.",
  },
  {
    question: "Do you ship internationally?",
    answer:
      "Yes, we ship to select countries internationally. Shipping costs and delivery times vary depending on the destination. Please see our shipping policy for more details.",
  },
];
 
// Sample product data for our demo shoe store
const SAMPLE_PRODUCTS = [
  {
    id: "1001",
    name: "Cloudfoam Racer TR21",
    brand: "Adidas",
    category: "Running Shoes",
    description:
      "Lightweight running shoe with Cloudfoam cushioning for superior comfort. Ideal for everyday runs and gym workouts.",
    price: 79.99,
    imageUrl:
      "https://placehold.co/300x200/000000/FFFFFF?text=Adidas+Running+Shoe", // Replace with actual image URL
    colors: ["Black/White", "Grey/Blue", "Navy/Red"],
    sizes: [7, 8, 9, 10, 11, 12],
    rating: 4.5,
    numReviews: 125,
  },
  {
    id: "1002",
    name: "Chuck Taylor All Star",
    brand: "Converse",
    category: "Casual Sneakers",
    description:
      "The iconic Chuck Taylor All Star. A timeless classic for everyday style.",
    price: 60.0,
    imageUrl:
      "https://placehold.co/300x200/000000/FFFFFF?text=Converse+Chuck+Taylor", // Replace with actual image URL
    colors: ["Black", "White", "Red", "Navy"],
    sizes: [5, 6, 7, 8, 9, 10, 11],
    rating: 4.7,
    numReviews: 280,
  },
  {
    id: "1003",
    name: "Air Max 90",
    brand: "Nike",
    category: "Sneakers",
    description:
      "The Nike Air Max 90 stays true to its OG running roots with the iconic Waffle sole, stitched overlays and classic TPU accents. Classic looks meet classic comfort.",
    price: 130.0,
    imageUrl: "https://placehold.co/300x200/000000/FFFFFF?text=Nike+Air+Max+90", // Replace with actual image URL
    colors: ["White/Red/Black", "Grey/Blue", "Black/White"],
    sizes: [8, 9, 10, 11, 12, 13],
    rating: 4.8,
    numReviews: 350,
  },
  {
    id: "1004",
    name: "Arizona Sandal",
    brand: "Birkenstock",
    category: "Sandals",
    description:
      "The classic Birkenstock Arizona sandal. Known for its comfort and support.",
    price: 99.95,
    imageUrl:
      "https://placehold.co/300x200/000000/FFFFFF?text=Birkenstock+Arizona", // Replace with actual image URL
    colors: ["Brown", "Black", "White", "Taupe"],
    sizes: [36, 37, 38, 39, 40, 41, 42], // EU sizing
    rating: 4.6,
    numReviews: 185,
  },
  {
    id: "1005",
    name: "Newton Ridge Plus II",
    brand: "Columbia",
    category: "Hiking Boots",
    description:
      "Durable and waterproof hiking boots for all-terrain adventures.",
    price: 110.0,
    imageUrl:
      "https://placehold.co/300x200/000000/FFFFFF?text=Columbia+Hiking+Boots", // Replace with actual image URL
    colors: ["Brown/Green", "Black/Grey"],
    sizes: [7, 8, 9, 10, 11, 12, 13],
    rating: 4.4,
    numReviews: 90,
  },
  {
    id: "1006",
    name: "Classic Leather",
    brand: "Reebok",
    category: "Casual Sneakers",
    description:
      "The Reebok Classic Leather is a timeless icon, offering simple style and all-day comfort.",
    price: 75.0,
    imageUrl:
      "https://placehold.co/300x200/000000/FFFFFF?text=Reebok+Classic+Leather", // Replace with actual image URL
    colors: ["White/Grey", "Black/White", "Navy/White"],
    sizes: [6, 7, 8, 9, 10, 11, 12],
    rating: 4.3,
    numReviews: 112,
  },
  {
    id: "1007",
    name: "Gel-Kayano 28",
    brand: "ASICS",
    category: "Running Shoes",
    description:
      "The GEL-KAYANO® 28 shoe creates a stable stride that moves you towards a balanced mindset. Featuring a lower-profile external heel counter, this piece cradles your foot with improved rearfoot support.  FLYTEFOAM™ Blast cushioning keeps the shoe lightweight, while creating a springy rebound.",
    price: 160.0,
    imageUrl:
      "https://placehold.co/300x200/000000/FFFFFF?text=ASICS+Gel-Kayano+28", // Replace with actual image URL
    colors: ["Blue/White", "Grey/Black", "Red/Black"],
    sizes: [7, 7.5, 8, 8.5, 9, 9.5, 10, 10.5, 11, 11.5, 12],
    rating: 4.9,
    numReviews: 410,
  },
];
 
// A welcome message for the chatbot
const INITIAL_MESSAGES = [
  {
    role: "assistant",
    content:
      "Hey there👋 I'm here to help you find the perfect shoe for your needs. What are you looking for?",
  },
];
© 2025 cdnkr