---
title: "AI Lab Project Building Part 2: Promptpad"
date: 2026-03-11
summary: Promptpad started as a standalone Next.js app running against Ollama and became an AI Lab workbench with revisions, diffs, and a model selector that had to match the chat's contract exactly.
status: published
tags: ai lab, promptpad, prompt engineering, ollama, revisions, testing, ui
mediaSourcePath: /ai-lab/promptpad/
mediaSourceSelector: main, .promptpad-layout
mediaSourceTheme: dark
imageAlt: Promptpad's editor with a revision history and enhancement controls.
imageCaption: A prompt is a document. It deserves revisions, diffs, and an undo button.
---

# AI Lab Project Building Part 2: Promptpad

Promptpad exists because I kept writing the same terse instruction into a chat box and then spending three turns expanding it. The idea was to treat a prompt like a document: draft it, have a model tighten it, see the diff, keep the version you like.

There are two Promptpads, and it's worth being clear about which is which.

## The standalone app

The first one is a Next.js application at [github.com/benmcnulty/promptpad](https://github.com/benmcnulty/promptpad). It runs entirely against a local Ollama server, so nothing you type leaves your machine. It implements a two-layer refinement pass: a structural rewrite, then an optional normalization pass that only kicks in when a model's output drifts from the expected shape. It has a CLI for the same workflow, and it carries its own test suite, 156 tests at last count. That app was my first serious attempt at making a local model do one narrow job well, and it taught me that the second pass is where most of the value is. First drafts from small models are fine. Consistency is what they lack, and a normalization step buys a lot of it.

## The AI Lab version

The second one is the workbench at [/ai-lab/promptpad/](/ai-lab/promptpad/). Same idea, different constraints: it runs in a static page against the site's Firebase Function and OpenRouter, it has to share the chat's model tiers, and it has to look like it belongs next to the other apps.

The data model is the part I'd defend most. Every document has an ordered list of revisions. Each revision records its parent, its content, whether it came from a manual edit or an enhancement, which enhancement options were selected, and which model produced it, down to the display name. There's a committed revision, a history cursor for undo and redo, and a stored "last diff" so the comparison view survives a reload. All of it lives in `localStorage` under a versioned key.

That structure is what makes the workbench feel like a tool instead of a text box. You can enhance, look at what changed, step back, try different options, and commit the one you want.

## The bug that shaped the sprint

The first working version had its own copy of the tier-to-model mapping, forked from the chat's. The two drifted. Promptpad carried a field the chat didn't, the chat carried one Promptpad didn't, and a visitor could pick a tier in Promptpad that sent a request the function would reject. You cannot see a malformed request body in a screenshot, which is exactly how it survived manual review.

The fix was to delete the fork. The mapping now lives in one shared module, `js/api/chat-config.js`, and both apps read it. Promptpad keeps one local table for its own retired-model migrations and one override so its pre-existing storage keys didn't reset everyone's saved tier when it moved onto the shared module. The comment above that override explains why it exists, because a year from now it will look like something to clean up.

Promptpad defaults to the Thoughtful tier where the chat defaults to Balanced. Prompt refinement benefits from the stronger reasoning model more than a quick answer does, and that's the kind of per-app decision the shared config was built to allow.

## Wiring every control

A second pass went through every visible control and asked whether it did something real. Copy buttons got success feedback. Unlabeled controls were either removed or given an accessible name and a defined behavior. Revision actions had to be deterministic under repeated clicks. The standard was clarity under actual use, not alignment in a mockup.

Light mode is where visual regressions showed up first, so that's where I spent the polish: text contrast, panel edges, spacing rhythm matched to the rest of the site.

## What's tested

Four spec files, layered the same way the other apps are: unit tests for the state serializer and the diff logic, integration tests that assert the prompt key and request order, UI control tests for every button, and an end-to-end run through draft, enhance, diff, commit. If a control is visible, there's a test that clicks it.

## What it became

Promptpad turned into the style and interaction reference for the two apps that followed. It proved that a sequential, user-triggered inference loop, a real revision model, and a polished editor could ship together and stay testable. That's harder than it sounds when you're carrying two themes, several breakpoints, and interactive state across revisions at the same time.

- Previous: [AI Lab Project Building Part 1: AI Chat](/blog/ai-lab-project-building-part-1-ai-chat/)
- Next: [AI Lab Project Building Part 3: Problem Solver](/blog/ai-lab-project-building-part-3-problem-solver/)
