Cloudinary Blog

Building a Smart AI Image Search Tool Using React - Part 2: UI Components and Server Setup

Building a Smart AI Image Search Tool Using React Part 2

In our first article, we built a part of the front-end of our image search tool with the focus mainly on the parent App.js stateful component.

In this article - part two of a series - we will continue developing a AI image Search App, in which users can search for content in an image, not just the description. The app is built with React for UI interaction, Cloudinary for image upload and management and Algolia for search.

We will show how to complete the app. I know you must be stoked, I am too. We will be building the stateless components of our app and these components without state will mostly receive props from the parent component as with parent-child communication existing in React. Also the server-side of our app will be built and deployed to Webtask. You can read more about webtasks here.

Webinar
Marketing Without Barriers Through Dynamic Asset Management

Installation

All build tools and dependencies have been installed in the first part so we can move on with building.

The Custom Stateless Components

The stateless components are the ImageList component and the modal component. These display the list of images fetched from Algolia and the upload modal interface respectively. The action of buttons on the interface have already been configured in the parent App.js component and will be passed down to the child stateless components via props as earlier stated.

The ImageList Component

This component holds the images saved on the webtask server. In configuring the component, navigate to the ImageList.js file in the components folder and configure it like this:

Copy to clipboard
import React from 'react';
import {Image, CloudinaryContext, Transformation} from 'cloudinary-react';
import './ImageList.css';
export default ({ hit }) => {
  return (
    <div className="card">
      <div className="card-content">
        <Image publicId={hit.public_id}>
            <Transformation format=auto quality=auto />
         </Image>
        <p>{hit.description}</p>
      </div>
    </div>
  );
};

First, we import all dependencies and its CSS file. Since the component is stateless, props are just passed as parameter to the export function which returns JSX elements. The Cloudinary React component exposes Image and Transformation which helps with fetching the image from the Cloudinary server and setting transformations respectively. The interesting thing is that the transformation sets format and quality to auto. This renders the best optimized format for a give client (browser) and also optimizes the image intelligently.

The elements are styled with Bulma classes to image cards. It can be seen that the image src is the URL received from the hit prop and also its description. Style the ImageList cards with the ImageList.css file.

Copy to clipboard
.card {
  background: #f5f5f5;
  display: inline-block;
  margin: 0 0 1em;
  width: 100%;
  cursor: pointer;
}

The Modal Component

The Modal component is the interface handling uploads, description creation and saving. Previously we created the Modal.js file in the components folder and its accompanying CSS file. Here we configure this empty file. In Modal.js, import all required dependencies and modules:

Copy to clipboard
import React from 'react';
import classNames from 'classnames';
import './Modal.css';

classnames is a module (which we installed earlier) that is used to dynamically assign classes to an element in React. In the Modal component, create a function with a parameter ‘isActive’ and assign it to the variable modalClass. In the function, the classNames function is called and passed an object containing keys, modal and is-active. is-active is the CSS class to be manipulated.

Copy to clipboard
...
const modalClass = isActive =>
  classNames({
    modal: true,
    'is-active': isActive
  });
...

Next, we render the image upload interface and a close button:

Copy to clipboard
...
export default ({
  isActive,
  toggleModal,
  onDrop,
  saveImage,
  preview,
  description,
  pending,
  handleDescriptionChange,
  handleDropZoneClick
}) => {
  return (
    <div className={modalClass(isActive)}>
      <div className="modal-background" onClick={toggleModal} />
      <div className="modal-content">
        <div className="modal-card">
          <header className="modal-card-head">
            <p className="modal-card-title">Upload an image!</p>
            <button
              onClick={toggleModal}
              className="delete"
              aria-label="close"
            />
          </header>
        </div>
      </div>
      <button
        onClick={toggleModal}
        className="modal-close is-large"
        aria-label="close"
      />
    </div>
  );
};

Note the way the props are passed down to the components from the parent as parameters. The toggleModal function is used to open and close the modal box as can be seen in both buttons at the top and bottom of the box. Since we have created just the Modal header, let’s create the description, upload and save interface. Just after the closing header tag, create the description, upload and save features with:

Copy to clipboard
...
<section className="modal-card-body">
  <div className="columns">
    <div className="column">
      <div className="field">
        <label className="label">Short Description</label>
        <div className="control">
          <textarea
            className="textarea"
            value={description}
            onChange={handleDescriptionChange}
            placeholder="Textarea"
          />
        </div>
        <button
          onClick={saveImage}
          className="button is-info is-margin-top"
        >
          Save
        </button>
      </div>
    </div>
    <div className="column upload-column">
      {!preview ? (
          <button
          onClick={handleDropZoneClick}
          className="button is-info is-margin-top"
        >
          Upload
        </button>
      ) : (
        <img src={preview} alt="Preview" />
      )}
      <div
        className="mainImageLoading"
        style={{ display: pending ? 'block' : 'none' }}
      />
    </div>
  </div>
</section>
...

An input text area is used to handle description input with its value bound to description and an event listener in onChange is on hand to pick up any entries in the description field as specified in the handleDescriptionChange function. The saveImage function is assigned to a button which triggers once clicked. Now here’s a tricky part, conditional rendering is used when displaying either the upload button or the uploaded image. The pending state comes in handy here and if pending is false (no image is uploaded), the upload button is visible and triggers the handleDropZoneClick when clicked. f pending is true, the uploaded image is displayed.

Update the Modal.css file to this. Now that the user interface is done, let’s create the server to handle uploads.

Configure Server.js

In the root directory of the project, create a file server.js, in here we will configure the server. In the server file, import all required dependencies with:

Copy to clipboard
const Express = require('express');
const Webtask = require('webtask-tools');
const bodyParser = require('body-parser');
const cloudinary = require('cloudinary');
const multipart = require('connect-multiparty');
var algoliasearch = require('algoliasearch');

As stated earlier, we will be making use of an express server to make the process easier. Let’s configure the server to use the required modules with:

Copy to clipboard
const app = Express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// Multipart middleware to
// parse files
const multipartMiddleware = multipart();
// Replace credentials
// with your Cloudinary credentials
cloudinary.config({
  cloud_name: 'cloud_name',
  api_key: 'key',
  api_secret: 'secret'
});
// Configure Algolia
// with your Algolia credentials
var algoliaClient = algoliasearch(
  'id',
  'key'
);
var algoliaIndex = algoliaClient.initIndex('index');

Images are uploaded via Cloudinary. Configure the upload API endpoint next with:

Copy to clipboard
app.post('/upload', multipartMiddleware, function(req, res) {
  // Upload image to cloudinary
  cloudinary.v2.uploader.upload(
    // File to upload
    req.files.image.path,
    // AWS tagging transformation
    // Activate here by selecting a plan:
    // https://cloudinary.com/console/addons#aws_rek_tagging
    { categorization: 'aws_rek_tagging' },
    // Callback function
    function(err, result) {
      if (err) return res.send(err);
      res.json({ data: result });
    }
  );
});

The uploaded files are saved to an Algolia index where search and retrieval occurs. This makes use of a post request and you configure it with:

Copy to clipboard
app.post('/save', function(req, res) {
  // index record
  console.log(req.body)
  algoliaIndex.addObject(req.body, function(err, content) {
    if (err) return res.send(err);
    res.json(content);
  });
});

Lastly, the app is exported to a Webtask with:

Copy to clipboard
module.exports = Webtask.fromExpress(app)

This creates a webtask for our server. Read more about webtasks and creating a webtask for a serverless architecture here.

Launch the app on the local server by running:

Copy to clipboard
npm run start

Smart Search

Conclusion

Now we have a AI image search tool that searches uploaded images and looks for a pattern queried using the Algolia search tool. Cloudinary, which is used to upload images, provides a robust upload widget that makes it even better and improves the developer experience. Feel free to play around with the app, suggest improvements or make improvements to the source code here.

Recent Blog Posts

Automatically Translating Videos for an International Audience

No matter your business focus—public service, B2B integration, recruitment—multimedia, in particular video, is remarkably effective in communicating with the audience. Before, making video accessible to diverse viewers involved tasks galore, such as eliciting the service of production studios to manually dub, transcribe, and add subtitles. Those operations were costly and slow, especially for globally destined content.

Read more
Cloudinary Helps Minted Manage Its Image-Generation Pipeline at Scale

David first shared his thoughts at our ImageCon coverence last October and this case study is an abbreviated version of Minted’s success using Cloudinary.

Over time, Faithful renderings of the creations of the illustrators, textile designers, painters, packaging designers, marketers, and stay-at-home moms, all of whom are core contributors of the Minted world, was getting harder and harder. Legacy technology wasn’t cutting it any more—and it was time for Cloudinary to step in.

Read more
Highlights on ImageCon 2021 and a Preview of ImageCon 2022

New year, same trend! Visual media will continue to play a monumental role in driving online conversions. To keep up with visual-experience trends and best practices, Cloudinary holds an annual conference called ImageCon, a one-of-a-kind event that helps attendees create the most engaging visual experiences possible.

Read more

New for DAM: Media Library Extension for Chrome

By Sharon Yelenik
A New Media Library Chrome Extension for Cloudinary DAM

With the introduction of the Media Library Extension, a Chrome-browser add-on that streamlines the access to, search for, and management of images and videos, Cloudinary offers yet another effective tool for its Digital Asset Management (DAM) solution. Let’s have a look at how most teams are currently working with media assets and how the new add-on not only boosts efficiency, but also renders the process a pleasure to work with.

Read more
New Features Supercharge Cloudinary’s Digital Asset Management Solution.

Today, I’m thrilled to announce the launch of Apps for Digital Asset Management and a Media Library Extension for the Chrome browser, which enables easy, flexible integration with all web-based applications in addition to making asset discovery more robust and accessible to all.

Read more
Scale and Automate Workflows With Modern Digital Asset Management Systems

With building, growing, and maintaining a strong digital presence being a top priority for all brands, high-quality visual content is paramount. In fact, consumers are 40 times more likely to share visual content on social networks than on other forums. Plus, a recent study from Wyzowl found that 84% of consumers made purchase decisions after watching a video, which explains why many brands are adding more and more visual media to their sites.

Read more