DEV Community

Cover image for Building EditPrompt: A Visual Prompt Library with React and SQLite
Rohan Arora
Rohan Arora

Posted on

Building EditPrompt: A Visual Prompt Library with React and SQLite

“Make this photo cinematic” leaves a lot of decisions to an image model. Should the background change? The lighting? The person's expression? Their clothes?

A useful editing prompt makes those decisions visible. It describes what to change, what to preserve, and what to leave out.

I built EditPrompt around that workflow: browse a visual prompt library, choose a starting point, adapt the instructions, and use them with a reference photo in an external image editor. The site also includes a generator that assembles prompt text directly in the browser.

The interesting engineering problem is keeping that experience predictable. The application uses React and Vite for the interface, a Node.js API backed by SQLite for the catalog, and a shared JavaScript module for prompt generation.

Here are the decisions behind that design, including a small generator you can run yourself.

1. Give the prompt a structure users can edit

An editing request has at least three parts:

  • Change: the background, lighting, framing, or another specific feature.
  • Preserve: facial details, hairstyle, clothing, or the existing composition.
  • Exclude: extra people, lettering, logos, or unwanted accessories.

For example:

Replace the background with a quiet garden. Keep the person's facial features, hairstyle, glasses, and clothing close to the reference. Match the background lighting to the light already on the face. Use a waist-up frame with realistic skin texture. Add no extra people or lettering.

Each sentence gives the user something concrete to adjust. If the background is wrong, they can change that instruction without rewriting the rest.

This also separates two kinds of correctness. Software can verify that the instructions contain the selected scene. Whether an image model follows those instructions requires a different evaluation. Requests to preserve identity remain requests; the resulting image still needs review.

2. Make the generator a pure function

EditPrompt's generator has four inputs: subject, scene, lighting, and framing. Each currently has three choices, giving 3 × 3 × 3 × 3 = 81 combinations.

That bounded space makes deterministic composition useful. The same choices produce the same text, and changing a dropdown requires no model request.

The generator lives in a shared module, separate from React. Here is a standalone, shortened implementation of the same approach. Save it as prompt-demo.mjs:

const options = {
  subject: {
    portrait: "Edit the uploaded reference of one person.",
    couple: "Edit the uploaded reference of two adults, with Person A on the left and Person B on the right.",
    child: "Edit the uploaded reference of a child.",
  },
  scene: {
    studio: "Use a plain neutral studio background.",
    garden: "Use a quiet garden with foliage clear of the face.",
    birthday: "Place a plain birthday cake on a separate table, with balloons behind the subject and no lettering or lit candles.",
  },
  light: {
    window: "soft window light",
    overcast: "overcast daylight",
    warm: "warm reflected light",
  },
  framing: {
    close: "head-and-shoulders",
    waist: "waist-up",
    full: "full-length",
  },
};

function buildPrompt(choices) {
  for (const [field, values] of Object.entries(options)) {
    if (!Object.hasOwn(values, choices[field])) {
      throw new TypeError(`Invalid ${field}`);
    }
  }

  return [
    options.subject[choices.subject],
    options.scene[choices.scene],
    `Use ${options.light[choices.light]} and ${options.framing[choices.framing]} framing.`,
    "Preserve each person's distinct facial details, hairstyle, actual age, skin tone, natural proportions, and age-appropriate clothing coverage.",
    "Retain realistic skin texture. Add no lettering, logos, new accessories, or extra people.",
  ].join(" ");
}
Enter fullscreen mode Exit fullscreen mode

The validation belongs inside the function even though the interface uses dropdowns. Tests, future callers, and restored state can supply values the interface never offered.

Object.hasOwn() checks the allowed values themselves. A value such as "__proto__" cannot pass by matching an inherited property.

React only needs to hold the choices and derive the text:

const [choices, setChoices] = useState({
  subject: "portrait",
  scene: "studio",
  light: "window",
  framing: "waist",
});

const prompt = buildPrompt(choices);
Enter fullscreen mode Exit fullscreen mode

There is no second state variable containing the generated prompt, so there is no extra synchronization step between the selections and the text. EditPrompt displays that text in a read-only textarea and offers a copy button, with manual selection available if clipboard access fails.

3. Test every combination, then state what the tests prove

With 81 combinations, exhaustive composition checks are small enough to run directly. Append this to prompt-demo.mjs and run node prompt-demo.mjs:

import assert from "node:assert/strict";

const outputs = new Set();

for (const subject of Object.keys(options.subject)) {
  for (const scene of Object.keys(options.scene)) {
    for (const light of Object.keys(options.light)) {
      for (const framing of Object.keys(options.framing)) {
        const prompt = buildPrompt({ subject, scene, light, framing });
        assert.ok(prompt.includes(options.subject[subject]));
        assert.ok(prompt.includes(options.scene[scene]));
        assert.ok(prompt.includes(options.light[light]));
        assert.ok(prompt.includes(options.framing[framing]));
        assert.doesNotMatch(prompt, /undefined/);
        outputs.add(prompt);
      }
    }
  }
}

assert.equal(outputs.size, 81);
assert.throws(() => buildPrompt({}), /Invalid subject/);
assert.throws(() => buildPrompt({ subject: "__proto__" }), /Invalid subject/);

console.log("81 distinct combinations checked; invalid inputs rejected.");
Enter fullscreen mode Exit fullscreen mode

The project's generator tests also check subject-specific wording and birthday constraints. These checks establish that the text builder follows its contract.

They do not establish image quality or compatibility with every image model. An evaluation of generated images would need to record the reference image, model, settings, prompt, and review criteria separately. Keeping those claims separate makes the feature easier to explain honestly.

4. Let the catalog API return only what the screen needs

The browsing side has different requirements from the generator. It needs searchable records, categories, tags, previews, and full detail pages.

The SQLite schema separates posts, taxonomy terms, and assets. Join tables connect posts to terms and additional media. That keeps a prompt's text distinct from the files used to illustrate it.

The public API makes a corresponding distinction:

GET /api/prompts?category=portrait&page=1&limit=24
GET /api/prompts/:slug
Enter fullscreen mode Exit fullscreen mode

The list endpoint returns summary fields and preview URLs. The detail endpoint returns the complete prompt and article content. A gallery visit therefore avoids downloading every full prompt body.

List requests default to 24 records and cap the limit at 48. Queries bind user-supplied values, and sort choices map to predefined SQL ordering clauses. A stable identifier breaks ties when publication dates match.

The web process opens the catalog read-only. Catalog preparation runs separately from HTTP requests, keeping image processing and imports outside the visitor's request path.

There are practical limits here. The current search uses LIKE against titles and prompt text, and database calls are synchronous. Query duration and event-loop delay would matter as the workload grows. If search became a bottleneck, SQLite FTS5 would be one option to evaluate for full-text indexing.

5. Treat image delivery as part of the interface

A visual library asks visitors to compare previews before opening a detail page. Image delivery directly affects how soon they can make that choice.

EditPrompt prepares WebP gallery thumbnails and supplies responsive candidates through srcset and sizes. Cards include image dimensions to reserve space while files load. Initial cards load eagerly; later cards use lazy loading.

Those choices solve different problems:

  • Smaller image candidates reduce the bytes needed for a small card.
  • Reserved dimensions help prevent the layout from moving during loading.
  • Lazy loading postpones requests for cards farther down the page.
  • Eager loading lets the first visible previews start loading immediately.

The grid and its sizes rules need to agree. A browser can only choose a suitable candidate when the declared display size is close to the space the card actually occupies.

The important measurement is whether visitors receive useful previews sooner under realistic conditions. A different file extension alone cannot establish that improvement.

6. Share the initial page between server and browser

The React application also renders HTML on the server. Its initial response includes page content and route-specific metadata, followed by the data needed for hydration.

The flow is:

Route + catalog data
        ↓
React server render → HTML + initial data
        ↓
Browser hydration → interactive page
Enter fullscreen mode Exit fullscreen mode

The browser uses hydrateRoot to attach React to the existing HTML. The initial browser render must agree with the server render, including the generator's default selections.

This makes shared data and pure functions useful beyond testing: the server and browser can derive the same prompt from the same inputs. Browser-only features, such as saved prompt IDs in localStorage, still need separate handling. Those saves belong to that browser and can disappear when site data is cleared.

A useful boundary for AI product design

EditPrompt prepares an editable brief. The chosen external editor handles the reference photo and image generation under its own capabilities and access rules.

That division gives each layer a clear responsibility: the catalog helps people find a starting point, the generator turns choices into explicit instructions, and the interface makes those instructions easy to inspect and copy.

You can try the photo prompt generator and change one input at a time to see that composition in practice.

If you were extending this tool, which would you add first: more scene controls, versioned prompt templates, or an evaluation set for reviewing model outputs?

Top comments (0)