In this tutorial, we will review how to create a feeless front end for a Lilypad module. To do so, we will:
Create a new NextJS project.
Create a CLI Wrapper to run the module.
Create a server to handle requests from the browser.
Create our user interface to allow a user to enter a prompt to run the module.
Run the front end.
Install Next JS
Create a new Next JS project by running the following:
npxcreate-next-app@latest
Prepare your CLI Wrapper
In your project folder, create a new file named .env to manage your environment variables. Paste in the following, and replace “<your-private-key-here>” with the private key of the wallet that will be charged for all of the jobs.
WEB3_PRIVATE_KEY=<your-private-key-here>
To avoid publishing your private key online, add .env to your .gitignore file.
Install dotenv to access your environment variables:
npminstalldotenv
In your app folder, create a folder for pages, then a folder for API, and in that folder, create a file named cliWrapper.js. Paste the following code into cliWrapper.js:
// Dotenv will allow us to access our environment variables so we can access our private key.require('dotenv').config({ path: __dirname +'/../../../.env' });// This will allow us to run code in our CLI.const { exec } =require('child_process');// The function we will call on the front end, to run a lilypad job.functionrunCliCommand(userInput, callback) {console.log("Lilypad Starting...");// Ensure the WEB3_PRIVATE_KEY environment variable is setconstweb3PrivateKey=process.env.WEB3_PRIVATE_KEY;// If the private key was not set up properly, we should expect to see this error.if (!web3PrivateKey) {console.error('WEB3_PRIVATE_KEY is not set in the environment variables.');return; } // This command will first export our private key, and then run the Lilypad SDXL module with the prompt provided by the user.
const command = `export WEB3_PRIVATE_KEY=${web3PrivateKey} && lilypad run sdxl-pipeline:v0.9-base-lilypad3 -i PromptEnv=Prompt="${userInput}”;
// This is a callback function to handle any errors when calling runCliCommand function.exec(command,async (error, stdout, stderr) => {if (error) {console.error(`Error: ${error.message}`);returncallback(error); }if (stderr) {console.error(`Stderr: ${stderr}`);returncallback(stderr); } // When Lilypad runs successfully, it returns the relative path to the files it generated, and the IPFS url. Here we are grabbing the relative path in local storage to serve the image to our front end.
constlines=stdout.trim().split('\n');constpath= lines[lines.length-4].trim(); // Trim any extra whitespaceconstfilePath=path.replace('open ','') +'/outputs/image-42.png';// This console log will confirm that Lilypad ran successfully.console.log(stdout)// This will return our output to the front end.if (callback) {callback(null, filePath); } });}module.exports= { runCliCommand };
Prepare your server
Install Express and CORS:
npm install express cors
In your app folder, create a file named server.js and paste the following code:
// Express is a framework for APIs.constexpress=require('express');// Cors will allow our browser to interact with our command line.constcors=require('cors');// Here we are importing our cli wrapper we just createdconst { runCliCommand } =require('./pages/api/cliWrapper');// Create an instance of Express to set up the serverconstapp=express();// Define the port number on which the server will listenconstport=3001;// Enable CORS middleware to handle cross-origin requestsapp.use(cors());// Use express.json middleware to parse JSON requestsapp.use(express.json());// Use express.urlencoded middleware to handle URL-encoded data (useful for form submissions)app.use(express.urlencoded({ extended:true }));// Here we will serve images from the directory Lilypad saves files to/app.use('/images',express.static('/tmp/lilypad/data/downloaded-files'));// When a user submits their prompt, this will run our CLI Wrapper with their prompt.app.post('/api/cliWrapper', (req, res) => {// Grab the user's inputconstuserInput=req.body.userInput;// Run our CLI Wrapper with their inputrunCliCommand(userInput, (error, filePath) => {if (error) {returnres.status(500).send('Error processing command'); }// Convert file path to URLconsturlPath=filePath.replace('/tmp/lilypad/data/downloaded-files','');// Send the URL where the result can be accessedres.send(`http://localhost:${port}/images${urlPath}`); });});// Start the server and listen on the specified portapp.listen(port, () => {console.log(`Server listening at http://localhost:${port}`);});
Prepare your front end
In the App folder, delete the default content on the Page.tsx file and paste in the following code:
"use client";import React, { useState, FormEvent } from'react';import Image from'next/image';exportdefaultfunctionHome():JSX.Element {// State for storing the user's inputconst [userInput,setUserInput] =useState<string>('');// State for storing the URL of the generated imageconst [output,setOutput] =useState<string>('');// State to track loading statusconst [isLoading,setIsLoading] =useState<boolean>(false);// Function to handle the form submissionconsthandleSubmit=async (event:FormEvent<HTMLFormElement>):Promise<void> => {event.preventDefault();setIsLoading(true); // Start loadingtry {constresponse=awaitfetch('http://localhost:3001/api/cliWrapper', { method:'POST', headers: {'Content-Type':'application/json', }, body:JSON.stringify({ userInput }), });constimageURL=awaitresponse.text();setOutput(imageURL); } catch (error) {console.error('Error:', error); }setIsLoading(false); // End loading };return ( <main><div className="flex min-h-screen flex-col items-center p-24"> <div className="z-10 max-w-5xl w-full flex flex-col items-center justify-center font-mono text-sm space-y-4">
<h1>Lilypad Image Generator</h1><div className="w-[500px] flex flex-col items-center"> {/* Container for image and form */} {/* Placeholder or Loading Animation */} {!output && ( <div className="border-dashed border-2 border-gray-300 w-full h-[500px] flex items-center justify-center">
{isLoading ? ( <span className="text-gray-500">Loading...</span> // Replace with your loading animation
) : ( <span className="text-gray-500">Image will appear here</span> )} </div> )} {/* Display the generated image if the output URL is available */} {output && <Image src={output} alt="Generated" width={500} height={500} />}<form onSubmit={handleSubmit} className="w-full mt-2"> {/* Textarea for user input */}<textarea className="w-full p-2" value={userInput} onChange={(e) => setUserInput(e.target.value)} placeholder="Enter your prompt" rows={4}/><div className="w-full flex justify-end"> {/* Submit button for the form */}<button type="submit" className="mt-4">Submit</button></div></form></div></div></div></main> );}
Running your front end
To run your front end on your local host, you will need to update the next.config.js file. Open the file and paste the following:
Open two terminals, one to run your server and another to run your front end. In both terminals, navigate to your App folder. For your server terminal, run the following:
In your second terminal, run your front end with the following command:
npmrundev
You should see something like this.
In your browser, navigate to http://localhost:3000/ and you should now be able to see your front end. It should look like this.
Type in a prompt and click on the enter button. If you look at the terminal running your server, you’ll notice that it says “Lilypad Starting…”. Lilypad will print to the console once the job is complete, and it should look something like this.
Now check your browser, and you should see your image.
Congrats! You have successfully integrated Lilypad into your front end.
Share your custom front ends with us!
If you were able to complete the tutorial, share your front end with us on Twitter!
If you need any help or have any feedback, hop into our Discord!