Step-by-Step Tutorial: How to Build a Plant Identification AI Tool Website Using Next.js 14

In this tutorial, we will guide you through building a Plant Identification AI Tool using Next.js 14 and the Gemini AI API . The website will allow users to upload an image of a plant, and the AI will identify the plant species. We’ll use React components for modularity and ensure a clean structure with reusable components like NavbarFooterImageUpload, and PlantInfo.

Prerequisites

Before starting, ensure you have:

  1. Node.js installed (v16 or higher).
  2. Basic knowledge of React and Next.js .
  3. Gemini AI API key (sign up for free at Google AI Studio ).
  4. A code editor like VS Code .

Step 1: Set Up the Next.js Project

  • Open your terminal and run the following command to create a new Next.js project:
npx create-next-app@latest plant-identifier
  • Navigate into the project folder:
cd plant-identifier
  • Install any additional dependencies if needed:
npm install @google/generative-ai
  • Create a .env.local file in the root directory and add your Google Gemini API key:
GOOGLE_GEMINI_API_KEY=your_api_key_here

Step 2: Organize the Project Structure

Create the following folder structure in your project:

components/
  Navbar.jsx
  Footer.jsx
  ImageUpload.jsx
  PlantInfo.jsx
app/
  page.jsx
  layout.jsx
pages/
  api/
    gemini.js
public/
styles/

Step 3: Create Reusable Components

3.1 Navbar.jsx

This component will serve as the navigation bar for your website.

import Link from "next/link";

export default function Navbar() {
  return (
    <nav className='bg-green-600 text-white p-4'>
      <div className='container mx-auto flex justify-between items-center'>
        <Link href='/' className='text-2xl font-bold'>
          PlantID
        </Link>
        <ul className='flex space-x-4'>
          <li>
            <Link href='/' className='hover:text-green-200'>
              Home
            </Link>
          </li>
          <li>
            <Link href='/about' className='hover:text-green-200'>
              About
            </Link>
          </li>
          <li>
            <Link href='/contact' className='hover:text-green-200'>
              Contact
            </Link>
          </li>
          <li>
            <Link href='/gallery' className='hover:text-green-200'>
              Gallery
            </Link>
          </li>
        </ul>
      </div>
    </nav>
  );
}

3.2 Footer.jsx

This component will serve as the footer for your website.

import Link from "next/link";

export default function Footer() {
  return (
    <footer className='bg-green-800 text-white p-8'>
      <div className='container mx-auto grid grid-cols-1 md:grid-cols-3 gap-8'>
        <div>
          <h3 className='text-xl font-bold mb-2'>About PlantID</h3>
          <p>
            PlantID is your go-to resource for identifying and learning about
            various plants using AI technology.
          </p>
        </div>
        <div>
          <h3 className='text-xl font-bold mb-2'>Quick Links</h3>
          <ul className='space-y-2'>
            <li>
              <Link href='/privacy' className='hover:text-green-200'>
                Privacy Policy
              </Link>
            </li>
            <li>
              <Link href='/terms' className='hover:text-green-200'>
                Terms of Service
              </Link>
            </li>
            <li>
              <Link href='/faq' className='hover:text-green-200'>
                FAQ
              </Link>
            </li>
          </ul>
        </div>
        <div>
          <h3 className='text-xl font-bold mb-2'>Contact Us</h3>
          <p>Email: info@plantid.com</p>
          {/* <p>Phone: (123) 456-7890</p> */}
        </div>
      </div>
      <div className='text-center mt-8'>
        <p>&copy; {new Date().getFullYear()} PlantID. All rights reserved.</p>
      </div>
    </footer>
  );
}

3.3 ImageUpload.jsx

This component will handle the image upload functionality.

"use client";

import { useState, useRef } from "react";
import { GoogleGenerativeAI } from "@google/generative-ai";
import { FaUpload, FaCamera } from "react-icons/fa";

const API_KEY = process.env.NEXT_PUBLIC_GOOGLE_GEMINI_API_KEY;

export default function ImageUpload({
  setPlantInfo,
  setImageUrl,
}: {
  setPlantInfo: React.Dispatch<React.SetStateAction<any>>;
  setImageUrl: React.Dispatch<React.SetStateAction<string | null>>;
}) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");
  const fileInputRef = useRef<HTMLInputElement>(null);
  const cameraInputRef = useRef<HTMLInputElement>(null);

  const handleImage = async (file: File) => {
    if (!file) return;

    setLoading(true);
    setError("");

    try {
      if (!API_KEY) {
        throw new Error("API key is not set");
      }

      const genAI = new GoogleGenerativeAI(API_KEY);
      const imageData = await readFileAsBase64(file);
      setImageUrl(URL.createObjectURL(file));

      const model = genAI.getGenerativeModel({
        model: "gemini-1.5-flash",
      });

      const result = await model.generateContent([
        "Identify this plant and provide the following information: " +
          "Common Name, Scientific Name, Brief Description, Origin, Growth Habit, Sunlight Requirements, Water Requirements. " +
          "Format the response as a JSON object with these keys. Do not include any markdown formatting.",
        { inlineData: { data: imageData, mimeType: file.type } },
      ]);

      let plantInfo = result.response.text();
      console.log("AI Response:", plantInfo);

      plantInfo = plantInfo.replace(/```json|\```/g, "").trim();

      try {
        const parsedInfo = JSON.parse(plantInfo);
        setPlantInfo(parsedInfo);
      } catch (parseError) {
        console.error("Error parsing AI response:", parseError);
        setError("Error parsing AI response. Please try again.");
        setPlantInfo(null);
      }
    } catch (error: unknown) {
      console.error("Error identifying plant:", error);
      const errorMessage =
        error instanceof Error ? error.message : "Unknown error occurred";
      setError(`Error identifying plant: ${errorMessage}`);
      setPlantInfo(null);
    } finally {
      setLoading(false);
    }
  };

  const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      handleImage(file);
    }
  };

  const handleCameraCapture = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      handleImage(file);
    }
  };

  const readFileAsBase64 = (file: File): Promise<string> => {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve((reader.result as string).split(",")[1]);
      reader.onerror = (error) => reject(error);
      reader.readAsDataURL(file);
    });
  };

  const handleButtonClick = (inputRef: React.RefObject<HTMLInputElement>) => {
    if (inputRef.current) {
      inputRef.current.click();
    }
  };

  return (
    <div className='mb-8 flex flex-col items-center'>
      <div className='flex space-x-4 mb-4'>
        <button
          onClick={() => handleButtonClick(fileInputRef)}
          className='bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-6 rounded-full cursor-pointer transition-colors text-lg flex items-center'
          disabled={loading}
        >
          <FaUpload className='mr-2' /> Upload Image
        </button>
        <button
          onClick={() => handleButtonClick(cameraInputRef)}
          className='bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-full cursor-pointer transition-colors text-lg flex items-center'
          disabled={loading}
        >
          <FaCamera className='mr-2' /> Take Photo
        </button>
      </div>
      <input
        ref={fileInputRef}
        type='file'
        accept='image/*'
        onChange={handleFileUpload}
        className='hidden'
        disabled={loading}
      />
      <input
        ref={cameraInputRef}
        type='file'
        accept='image/*'
        capture='environment'
        onChange={handleCameraCapture}
        className='hidden'
        disabled={loading}
      />
      {loading && <p className='text-gray-600'>Analyzing image...</p>}
      {error && <p className='text-red-500 mt-2'>{error}</p>}
    </div>
  );
}

3.4 PlantInfo.jsx

This component will display the identified plant information.

import Image from "next/image";

interface PlantInfoProps {
  info: {
    "Common Name": string;
    "Scientific Name": string;
    "Brief Description": string;
    Origin: string;
    "Growth Habit": string;
    "Sunlight Requirements": string;
    "Water Requirements": string;
  };
  imageUrl: string;
}

export default function PlantInfo({ info, imageUrl }: PlantInfoProps) {
  return (
    <div className='bg-white rounded-lg shadow-lg p-8 max-w-4xl w-full'>
      <h2 className='text-3xl font-semibold mb-6 text-green-800'>
        {info["Common Name"]}
      </h2>
      <div className='flex flex-col md:flex-row gap-8'>
        <div className='md:w-1/2'>
          <Image
            src={imageUrl}
            alt={info["Common Name"]}
            width={400}
            height={400}
            className='rounded-lg shadow-md object-cover w-full h-auto'
          />
        </div>
        <div className='md:w-1/2'>
          <h3 className='text-xl font-semibold mb-2 text-green-700'>
            Scientific Name
          </h3>
          <p className='text-gray-700 mb-4 italic'>{info["Scientific Name"]}</p>
          <h3 className='text-xl font-semibold mb-2 text-green-700'>
            Description
          </h3>
          <p className='text-gray-700 mb-4'>{info["Brief Description"]}</p>
          <table className='w-full border-collapse'>
            <tbody>
              {[
                "Origin",
                "Growth Habit",
                "Sunlight Requirements",
                "Water Requirements",
              ].map((key) => (
                <tr key={key} className='border-b border-gray-200'>
                  <td className='py-2 pr-4 font-semibold text-green-600'>
                    {key}
                  </td>
                  <td className='py-2'>{info[key as keyof typeof info]}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
}

Step 4: Create the Default Page

The default page (page.jsx) will include the Navbar, ImageUpload, PlantInfo, and Footer components.

"use client";

import { useState, useMemo } from "react";
import ImageUpload from "./components/ImageUpload";
import PlantInfo from "./components/PlantInfo";
import {
  FaUpload,
  FaLeaf,
  FaInfoCircle,
  FaSeedling,
  FaTrash,
} from "react-icons/fa";
import { motion, AnimatePresence } from "framer-motion";

interface PlantData {
  "Common Name": string;
  "Scientific Name": string;
  "Brief Description": string;
  Origin: string;
  "Growth Habit": string;
  "Sunlight Requirements": string;
  "Water Requirements": string;
}

interface HowToUseCard {
  icon: JSX.Element;
  title: string;
  description: string;
}

export default function Home() {
  const [plantInfo, setPlantInfo] = useState<PlantData | null>(null);
  const [imageUrl, setImageUrl] = useState<string | null>("");

  const howToUseCards = useMemo<HowToUseCard[]>(
    () => [
      {
        icon: <FaUpload className='text-4xl mb-4 text-green-600' />,
        title: "Upload Image",
        description:
          "Take a clear photo of the plant you want to identify and upload it to our app.",
      },
      {
        icon: <FaLeaf className='text-4xl mb-4 text-green-600' />,
        title: "AI Analysis",
        description:
          "Our advanced AI analyzes the image to identify the plant species.",
      },
      {
        icon: <FaInfoCircle className='text-4xl mb-4 text-green-600' />,
        title: "Get Information",
        description:
          "Receive detailed information about the plant, including its name and characteristics.",
      },
      {
        icon: <FaSeedling className='text-4xl mb-4 text-green-600' />,
        title: "Learn More",
        description:
          "Discover care tips, growing conditions, and interesting facts about the identified plant.",
      },
    ],
    []
  );

  const handleReset = () => {
    setPlantInfo(null);
    setImageUrl("");
  };

  const InfoCard = ({ icon, title, description }: HowToUseCard) => (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      className='bg-white rounded-lg shadow-md p-6 flex flex-col items-center text-center hover:shadow-lg transition-shadow'
    >
      {icon}
      <h3 className='text-xl font-semibold mb-2 text-green-800'>{title}</h3>
      <p className='text-gray-600'>{description}</p>
    </motion.div>
  );

  return (
    <div className='bg-gradient-to-b from-green-100 to-green-300 min-h-screen py-12'>
      <div className='container mx-auto px-4'>
        <motion.h1
          initial={{ opacity: 0, y: -20 }}
          animate={{ opacity: 1, y: 0 }}
          className='text-5xl font-bold mb-4 text-green-800 text-center'
        >
          Plant Identifier
        </motion.h1>
        <motion.p
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          className='text-xl text-green-700 mb-8 text-center max-w-2xl mx-auto'
        >
          Discover the wonders of nature! Upload an image or take a photo of a
          plant, and let our AI identify it for you.
        </motion.p>

        <div className='flex justify-center mb-12'>
          <ImageUpload setPlantInfo={setPlantInfo} setImageUrl={setImageUrl} />
        </div>

        <AnimatePresence>
          {plantInfo && imageUrl && (
            <motion.div
              initial={{ opacity: 0, scale: 0.9 }}
              animate={{ opacity: 1, scale: 1 }}
              exit={{ opacity: 0, scale: 0.9 }}
              className='flex flex-col items-center mt-12'
            >
              <PlantInfo info={plantInfo} imageUrl={imageUrl} />
              <button
                onClick={handleReset}
                className='mt-4 flex items-center px-4 py-2 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors'
              >
                <FaTrash className='mr-2' /> Clear Results
              </button>
            </motion.div>
          )}
        </AnimatePresence>

        {(!plantInfo || !imageUrl) && (
          <motion.p
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            className='text-gray-600 mt-4 text-center'
          >
            Upload an image or take a photo to see plant information.
          </motion.p>
        )}

        <div className='mb-12'>
          <h2 className='text-3xl font-bold mb-4 text-green-800 text-center'>
            How It Works
          </h2>
          <p className='text-lg text-green-700 mb-8 text-center max-w-2xl mx-auto'>
            Our plant identification app is easy to use and provides valuable
            information about the plants you discover.
          </p>
          <div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6'>
            {howToUseCards.map((card, index) => (
              <InfoCard key={index} {...card} />
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

Step 5: Create the Layout

The layout.jsx file will wrap all pages with a consistent layout.

import "./globals.css";
import type { Metadata } from "next";
import { Poppins } from "next/font/google";
import Navbar from "./components/Navbar";
import Footer from "./components/Footer";

const poppins = Poppins({
  weight: ["400", "600", "700"],
  subsets: ["latin"],
  display: "swap",
});

export const metadata: Metadata = {
  title: "PlantID - Identify Plants with AI",
  description:
    "Upload plant images and get instant identification using AI technology.",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='en'>
      <body className={`${poppins.className} flex flex-col min-h-screen`}>
        <Navbar />
        <main className='flex-grow'>{children}</main>
        <Footer />
      </body>
    </html>
  );
}

Step 6: Add Backend API Route

Create an API route to handle plant identification requests using the Gemini AI API .

  1. Create a new file at pages/api/gemini.js.
  2. Add the following code:
import { GoogleGenerativeAI } from "@google/generative-ai";

export default async function handler(req, res) {
  if (req.method === "POST") {
    try {
      const genAI = new GoogleGenerativeAI(process.env.GOOGLE_GEMINI_API_KEY);
      const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });

      const result = await model.generateContent([
        "Identify this plant and provide its name, scientific name, and brief description.",
        {
          inlineData: { data: req.body.imageData, mimeType: req.body.mimeType },
        },
      ]);

      res.status(200).json({ plantInfo: result.response.text() });
    } catch (error) {
      console.error("Error in API route:", error);
      res.status(500).json({ error: error.message });
    }
  } else {
    res.setHeader("Allow", ["POST"]);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

Step 7: Run the Application

  1. Start the development server:
npm run dev

2. Open your browser and navigate to http://localhost:3000.

Conclusion

You now have a fully functional Plant Identification AI Tool built with Next.js 14 and powered by the Gemini AI API ! Users can upload an image of a plant, and the tool will identify the plant species and provide relevant details. You can further enhance this project by adding features like user authentication, history tracking, or integrating additional APIs.

Let me know if you need help with deployment or any other enhancements! 🌱

Leave a Comment

Scroll to Top