DEV Community

Cover image for Build an Interior AI Clone Using HTMX and ExpressJS

Build an Interior AI Clone Using HTMX and ExpressJS

Interior Design AI SAAS products have been all the hype ever since Pieter Levels launched Interior AI. He now makes $40k in Monthly Recurring Revenue(MRR), and this has inspired several competitors such as RoomGPT, Decorify, and RoomAI. I even have my own version of an AI Interior Design app called DesignMate, which runs solely on WhatsApp with the idea of making it more accessible, targeting mostly African and Asian markets.

In this tutorial, I will show you how to easily build an Interior AI clone using the internet's latest sensation HTMX and ExpressJS. For those not familiar with HTMX, short for HTML extensions, it is a lightweight javascript library that allows you to access modern browser features using just HMTL, doing away with a lot of unnecessary javascript. No more complex React, Angular, or Vue, just simple HTML. Read on and find out how

What is HTMX?

HTMX is a JavaScript library commonly referred to as anti-JavaScript due to its unique approach to JavaScript development. It enhances HTML to give you access to AJAX, Web sockets, CSS animations, and Server-Sent Events through custom HTML attributes. HTMX simplifies development while giving control back to the server to drive the UI, reducing the complexity of your web applications while maintaining Single-Page Application functionality.

Compared to traditional front-end frameworks, HTMX offers a simpler and more efficient way to build dynamic web applications. Additionally, HTMX is just 14kb when gzipped, which means it loads fast and improves your website's performance.

Below is a simple demo of using HTMX:

  <script src="https://unpkg.com/htmx.org@2.0.0"></script>
  <!-- have a button POST a click via AJAX -->
  <button hx-get="/clicked" hx-target="response">
    Click Me
  </button>

  <div class="response"></div>
Enter fullscreen mode Exit fullscreen mode
  • The first line shows how to import HTMX. You can do this through the CDN or download the file and add it to your project. There is no unnecessary build step❌.
  • The button element is enhanced by 2 new attributes hx-get and hx-target.
  • The hx-get specifies the endpoint to call when the button is clicked. Similar functionality is also accessible through hx-post,hx-put, and hx-delete.
  • The hx-target specifies where HTMX will place the response from the server, in this case we are looking for the response class and swapping it out with the response. HTMX API responses are often HTML, so this would add a new element to the DOM where the div is. You can also add an hx-swap to specify how the response will be swapped in, by default it replaces the innerHTML but can be set to replace the outerHTML or be added beforeend or afterend.

It is important to note that, unlike conventional HTML responses, the server response doesn't trigger a page reload and will only result in a partial update on the DOM. This gives HTMX the same experience as SPA frameworks like React and Angular

To learn more about HTMX, you can visit their official site or try this course by Traversy Media

How It Will Work

Our Interior AI clone application integrates several modern technologies to deliver a seamless user experience for generating AI-enhanced 2D renders of interior designs. Here’s a brief look at how everything works:

FrontEnd with HTMX and TailwindCSS:
The web app has a simple frontend that uses HTMX and is styled with TailwindCSS. It has a simple form that allows the user to upload an image of a room and fill out details specifying the room type, theme, and color scheme. HTMX handles the form submission, on form submit, HTMX sends an AJAX request to the server, carrying the form data without needing a page reload.

Back-End with Express:
When the form is submitted, the Express server takes over. It processes the incoming form data and manages file uploads using Multer. The server reads the uploaded image and form fields. It uses the form fields to construct a prompt based on the user's input, combining the room type, theme, and color scheme to guide the AI model in generating the render. After that it sends the image and the prompt to replicate, a third party for processing.

AI Image Processing with Replicate:
Replicate is a platform that hosts machine learning models, making integrating AI capabilities into your applications easy. For this project, we will use the ControlNet-Hough model, an image-to-image model that excels at interior design, to generate photorealistic 2D renders. Replicate receives the image and prompt and uses the ControlNet-Hough model to process the image and give us an output of the new AI-generated high-quality, realistic interior room design.

Returning and Displaying Results:
The Replicate API returns an array of URLs pointing to the generated renders. The Express server receives these URLs, formats them into HTML, and sends them back to the client. HTMX then dynamically updates the front-end, replacing placeholder content with the newly generated images. This process ensures that the user can see the results of their submission almost instantaneously without needing to refresh the page, just like with Single-Page Applications (SPAs).

This approach is described in the image below:

System Workflow

Setting Up the Project

Before you begin, ensure you have Node.js and npm (Node Package Manager) installed on your machine. You can download and install them from the official Node.js website.

  1. Initialize the Project:
   mkdir interior-ai-clone
   cd interior-ai-clone
   npm init -y
Enter fullscreen mode Exit fullscreen mode
  1. Install Dependencies:
   npm install express multer replicate
Enter fullscreen mode Exit fullscreen mode
  • Express: A minimal and flexible Node.js web application framework. Multer: A middleware for handling multipart/form-data, which is primarily used for uploading files. It simplifies the process of handling file uploads in Node.js applications. Replicate: A Nodejs client for replicate.com, allowing us to interact with various AI models.
  1. Project Structure:
   interior-ai-clone/
   β”œβ”€β”€ public/
   β”‚   └── index.html
   β”œβ”€β”€ uploads/
   β”œβ”€β”€ server.js
   β”œβ”€β”€ package.json
   └── package-lock.json
Enter fullscreen mode Exit fullscreen mode

Building the Front-End with HTMX

Your front-end will be simple but powerful, leveraging HTMX for dynamic interactions and TailwindCSS for styling.

public/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI Interior Design App</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://unpkg.com/htmx.org@1.5.0"></script>
    <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap" rel="stylesheet">
</head>
<body class="bg-gray-100 font-[Poppins]"></body>">
    <div class="container mx-auto p-8">
        <h1 class="text-3xl font-semibold text-center mb-8 text-gray-800">AI Interior Design Generator</h1>
        <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
            <!-- Form Section -->
            <div class="bg-white p-6 rounded-lg shadow-lg">
                <form id="design-form" hx-encoding='multipart/form-data' hx-post="/generate" hx-target="#design-results" hx-trigger="submit"  hx-indicator="#spinner" class="space-y-6">
                    <div>
                        <label for="room-image" class="block text-sm font-medium text-gray-700 mb-2">Upload Room Layout</label>
                        <div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
                            <div class="space-y-1 text-center">
                                <svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
                                    <path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
                                </svg>
                                <div class="flex text-sm text-gray-600">
                                    <label for="room-image" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
                                        <span>Upload a file</span>
                                        <input id="room-image" name="room-image" type="file" accept="image/*" class="sr-only" required onchange="previewImage(event)">
                                    </label>
                                    <p class="pl-1">of your room</p>
                                </div>
                                <p class="text-xs text-gray-500">PNG, JPG, GIF up to 10MB</p>
                            </div>
                        </div>
                        <img id="image-preview" class="mt-4 w-full h-48 object-cover rounded-md hidden" src="#" alt="Image Preview">
                    </div>
                    <div>
                        <label for="room-type" class="block text-sm font-medium text-gray-700 mb-2">Room Type</label>
                        <input type="text" id="room-type" name="room-type" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm p-2">
                    </div>
                    <div>
                        <label for="room-theme" class="block text-sm font-medium text-gray-700 mb-2">Room Theme</label>
                        <select id="room-theme" name="room-theme" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm p-2">
                            <option value="">Select a theme</option>
                            <option value="modern">Modern</option>
                            <option value="classic">Classic</option>
                            <option value="minimalist">Minimalist</option>
                            <option value="industrial">Industrial</option>
                        </select>
                    </div>
                    <div>
                        <label for="color-scheme" class="block text-sm font-medium text-gray-700 mb-2">Color Scheme</label>
                        <select id="color-scheme" name="color-scheme" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm p-2">
                            <option value="">Select a color scheme</option>
                            <option value="light">Light</option>
                            <option value="dark">Dark</option>
                            <option value="neutral">Neutral</option>
                            <option value="vibrant">Vibrant</option>
                        </select>
                    </div>
                    <div>
                        <button type="submit" class="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-150 ease-in-out flex items-center justify-center">
                            <span class="htmx-indicator">Generating...</span>
                            <span class="htmx-request-content">Generate Design</span>
                            <svg id="spinner" class="htmx-indicator ml-2 h-5 w-5 text-white animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                            </svg>
                        </button>
                    </div>
                </form>
            </div>
            <!-- Results Section -->
            <div id="design-results" class="grid grid-cols-2 gap-8">
                <div class="bg-gray-200 h-56 flex items-center justify-center rounded-md shadow-md">Image Placeholder 1</div>
                <div class="bg-gray-200 h-56 flex items-center justify-center rounded-md shadow-md">Image Placeholder 2</div>
                <div class="bg-gray-200 h-56 flex items-center justify-center rounded-md shadow-md">Image Placeholder 3</div>
                <div class="bg-gray-200 h-56 flex items-center justify-center rounded-md shadow-md">Image Placeholder 4</div>
            </div>
        </div>
    </div>

    <script>
        function previewImage(event) {
            const reader = new FileReader();
            reader.onload = function() {
                const output = document.getElementById('image-preview');
                output.src = reader.result;
                output.classList.remove('hidden');
            };
            reader.readAsDataURL(event.target.files[0]);
        }
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Output of the code

In the above HTML code, you include HTMX with a simple script tag, allowing you to use its powerful features without any additional setup. HTMX's small file size ensures fast load times, making your application more responsive.

The hx-post attribute in the form element specifies that when the form is submitted, the input should be posted to the /generate endpoint on our server. The hx-target attribute lets HTMX know where to place the response from the server when it is returned. In our example, it will look for the design-result class and then replace the entire HTML, with the HTML from our server. The server is responsible for taking our input, processing it, and then returning well-formatted HTML with our new image results.

Creating the Back-End with Express

The back-end will handle file uploads, process the image with the Replicate API, and return the generated images.

server.js:

const express = require('express');
const multer = require('multer');
const Replicate = require('replicate');
const fs = require('fs').promises;
const path = require('path');

const app = express();
const upload = multer({ dest: 'uploads/' });

const replicate = new Replicate({
    auth: 'your_replicate_api_key'
});

app.use(express.static('public'));
app.use(express.urlencoded({ extended: true }));

// your endpoint definitions go here

app.listen(3000, () => {
    console.log('Server running on http://localhost:3000');
});
Enter fullscreen mode Exit fullscreen mode
app.post('/generate', upload.single('room-image'), async (req, res) => {
    try {
        const { roomType, roomTheme, colorScheme } = req.body;
        const imagePath = req.file.path;

        const data = await fs.readFile(imagePath);
        const base64Image = `data:image/jpeg;base64,${data.toString('base64')}`;

        const prompt = `a ${colorScheme} ${roomTheme} ${roomType}`;
        const input = {
            image: base64Image,
            prompt: prompt,
            num_samples: 4
        };

        const output = await replicate.run("jagilley/controlnet-hough:854e8727697a057c525cdb45ab037f64ecca770a1769cc52287c2e56472a247b", { input });

        res.send(output.map(img => `<img src="${img}" class="w-full h-48 object-cover rounded-md shadow-md mt-4">`).join(''));
        await fs.unlink(imagePath); // Clean up uploaded file
    } catch (error) {
        console.error(error);
        res.status(500).send('Error generating render.');
    }
});

Enter fullscreen mode Exit fullscreen mode

The /generate endpoint in your Express server handles the form submission from the front-end. Here's a breakdown of how it works:

  1. File Upload Handling:

    • The multer middleware handles file uploads, saving the uploaded image in the uploads directory.
  2. Reading the Uploaded File:

    • The file is read and converted to a base64-encoded string, which is required for the Replicate API.
  3. Generating the Prompt:

    • The roomType, roomTheme, and colorScheme fields from the form are combined to create a prompt for the AI model.
  4. Calling the Replicate API:

    • The Replicate client is used to run the ControlNet-Hough model with the provided image and prompt. The model generates 2D renders of the room based on the input.
  5. Returning the Results:

    • The generated images are returned to the client and displayed dynamically using HTMX.

Now that you have everything set, run the project using the following command: node index.js. That's it, you now have a fully functional AI Interior Design website that is simple, load fast and didn't make your head spinπŸ˜…. That is the beauty of HTMX, it greatly simplifies web development. If everything is set, you should get the following output:

You can view the entire codebase for this project here.

Conclusion

Building an Interior AI clone using HTMX and ExpressJS demonstrates the power and simplicity of HTMX in modern web development. HTMX reduces the complexity and overhead associated with traditional front-end frameworks like React by enabling dynamic interactions directly in HTML. This approach not only simplifies your codebase but also enhances maintainability and performance.

Through this project, you learned how to set up a Node.js environment, use HTMX for front-end development, and implement a back-end with Express to handle file uploads and image processing using the Replicate API. This combination of technologies offers a robust solution for creating interactive and efficient web applications. Go on and use them to build and explore new possibilities!

Top comments (1)

Collapse
 
ngaandasowa profile image
Ngaavongwe Ndasowampange

This post is a game-changer for anyone looking to build an interior AI clone! The step-by-step guide on using HTMX and ExpressJS is clear, concise, and easy to follow. Your expertise shines through in the way you break down complex concepts into manageable chunks. The code examples and explanations are top-notch, making it easy to implement the techniques. "Whether you're a seasoned developer or just starting out, this article is a must-read." Kudos to you, Michael, for sharing your knowledge and helping the community build innovative AI solutions!