Prompt Version Diff

Compare two versions of any prompt side by side with word-level LCS diff. Track changes, save version history to localStorage, export/import archives, and iterate with confidence. 100% client-side, nothing leaves your browser.

2 steps codestral-2508 Utility 🏆 ARENA WINNER
🏆 Arena Winner

codestral-2508 via Mistral scored 6.5/10 — the arena winner. Interactive tool works but tutorial and formatting required significant manual fixes. See full arena results →

🛠️ Try the Prompt Version Diff

Paste two versions of a prompt below and click Compare to see a word-level diff. Save important versions to your history for later comparison.

Samples:

💾 Version History

No saved versions yet. Type a prompt and click "Save Current" to save it.
01

Diff Viewer — LCS Word-Level Comparison Arena: codestral-2508 won

Complete

Why learn this

Prompt engineering is an iterative process. You refine a prompt, test it, then refine again. Without a diff tool, you are manually scanning two text blocks looking for differences — error-prone and slow for any prompt longer than a sentence. A word-level diff implementation automates this: it highlights exactly what changed between versions so you can audit modifications at a glance.

Beyond prompt engineering, LCS (Longest Common Subsequence) diff has applications everywhere: code review, document comparison, plagiarism detection, and version control. Understanding how to implement it from scratch builds algorithmic thinking — dynamic programming, matrix memoization, and backtracking — skills that transfer to interview problems, text processing pipelines, and data reconciliation tools.

What you built and why this way

Word-level vs character-level diff. Prompt changes typically happen at the word or phrase level — replacing "comprehensive" with "detailed", adding a constraint like "in markdown format", removing a clause. Character-level diff would highlight individual letter changes (like "runing" → "running" for a typo fix), which is too granular for meaningful prompt review. Word-level diff groups changes by whole words, making the output readable and actionable.

LCS algorithm with O(m*n) space. The classic dynamic programming approach fills a 2D matrix where cell (i,j) stores the length of the LCS of prefixes a[0..i] and b[0..j]. This uses O(m*n) memory for m and n word counts. For typical prompts (50-500 words), that is 2,500-250,000 cells — trivial for modern browsers. The backtracking pass reconstructs the actual common subsequence, which drives the diff coloring.

Three-color diff scheme. Green (added) highlights words present in B but not A. Red (removed) highlights words present in A but not B. Yellow (changed) highlights words present in both but at positions that do not align with the LCS — indicating substitution. This follows the convention set by tools like git diff and GitHub diff view, so it is immediately familiar to developers.

Swap button for bidirectional comparison. The swap button flips versions A and B. This is useful when you paste them in the wrong order, or when you want to see the diff from the other direction (what was removed in the new version vs what was added).

Key concepts

  • LCS dynamic programming matrix — A 2D array of size (m+1) x (n+1) where cell (i,j) = LCS length of a[0..i-1] and b[0..j-1]. Fill rule: if a[i-1] === b[j-1], cell = 1 + diagonal; else cell = max(up, left)
  • Backtracking to extract common subsequence — Starting from (m,n), walk back: if characters match and equal current common element, emit it and move diagonal; otherwise move toward the larger neighbor (up or left)
  • Tokenization by whitespace — text.split(/\s+/) splits on any whitespace, producing an array of words. Punctuation is retained as part of each word, which means "hello," and "hello" are treated as different tokens. A more advanced version strips punctuation before comparison
  • Diff classification — Walk both word arrays using the LCS as a guide: matching words = "same", words skipped in A = "removed", words skipped in B = "added", non-matching words at the same position = "changed"
  • DOM rendering with span nodes — Each classified word becomes a span with a CSS class for its type

Alternative approaches

  • Character-level diff with Myers algorithm — The standard git diff algorithm (Myers O(ND)) operates at character/line level. It is faster than LCS for large inputs (linear vs quadratic) but produces noisier output for prompts. Character diffs highlight typos and case changes that clutter the review
  • Line-level diff — Split by newlines instead of words. Useful for comparing structured prompts with multiple paragraphs or instructions. Less granular than word-level but faster to compute for very long documents
  • Semantic diff with embedding similarity — Use sentence embeddings to detect semantically equivalent phrases even when wording differs. Requires an embedding API call, so not suitable for client-only use
  • Tree-diff for structured prompts — If prompts follow a template (e.g., system + user + instructions sections), parse into an AST and diff at the node level
  • Third-party diff libraries — Libraries like diff-match-patch (Google) or jsdiff provide battle-tested diff implementations. Skipping them keeps the bundle under 30KB and teaches the algorithm

Browser compatibility

  • CSS Grid: Chrome 57+, Firefox 52+, Safari 10.1+, Edge 16+. No IE11 support for the two-column layout
  • Array.from() / spread operator: Chrome 45+, Firefox 25+, Safari 8+, Edge 12+. IE11 needs polyfills
  • innerHTML: supported in all browsers since IE4
  • localStorage: Chrome 4+, Firefox 3.5+, Safari 4+, IE8+. Universal support
  • ES6 arrow functions and template literals: Chrome 45+, Firefox 22+, Safari 10+, Edge 13+

Performance notes

  • LCS on two 500-word prompts: ~250K cells, computed in ~2-3ms
  • Backtracking pass: O(m+n) — runs in microseconds even for 500-word inputs
  • DOM rendering: ~1-2ms for 1,000 span nodes
  • Total page size: ~32KB of HTML/CSS/JS
  • No external dependencies, no CDN fonts, no analytics

Common pitfalls

  • LCS is not unique. When multiple common subsequences have equal length, the backtracking pass picks one arbitrarily
  • Whitespace normalization. Trailing spaces, multiple consecutive spaces, and different line endings (LF vs CRLF) all produce different word arrays. Normalize whitespace before diffing
  • Empty textareas crash the LCS. Check for empty text before running the diff
  • Large prompts can freeze the UI. LCS is O(m*n) — for two 10,000-word prompts that is 100M cells
  • Punctuation sensitivity. "hello!" and "hello" are different words. For prompts, this often produces spurious diffs on punctuation

Next up

Step 2 explores the version history system — how to persist prompt versions in localStorage, browse saved history, select any two versions for comparison, and export/import your entire history as JSON.

JS — LCS algorithm implementation
function lcs(a, b) {
  const m = a.length, n = b.length;
  const dp = new Array(m + 1);
  for (let i = 0; i <= m; i++)
    dp[i] = new Int32Array(n + 1);
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (a[i-1] === b[j-1]) {
        dp[i][j] = dp[i-1][j-1] + 1;
      } else {
        dp[i][j] = Math.max(
          dp[i-1][j], dp[i][j-1]);
      }
    }
  }
  const result = [];
  let i = m, j = n;
  while (i > 0 && j > 0) {
    if (a[i-1] === b[j-1]) {
      result.unshift(a[i-1]);
      i--; j--;
    } else if (dp[i-1][j] >
      dp[i][j-1]) {
      i--;
    } else { j--; }
  }
  return result;
}
CSS — Diff color classes
.diff-word-added {
  background: rgba(34,197,94,0.2);
  color: #22c55e;
}
.diff-word-removed {
  background: rgba(239,68,68,0.2);
  color: #ef4444;
  text-decoration: line-through;
}
.diff-word-changed {
  background: rgba(245,158,11,0.2);
  color: #f59e0b;
}
02

Version History — localStorage Persistence Save & Manage

Complete

Why learn this

Prompt iteration produces many versions. After a few rounds of refinement, you can't remember which version had the best output, which constraint was added in v3, or whether the current version is an improvement over last week's. A version history system solves this: every prompt version is saved with a timestamp, labeled, and instantly retrievable. You can compare any two saved versions side by side, export your history as JSON for backup or sharing, and import a history from another session.

This pattern applies everywhere: code snippet managers, template libraries, A/B test records, and configuration versioning. The localStorage API is available in every browser, requires no server, and persists across page reloads. Combined with JSON import/export, it gives you a portable, zero-infrastructure version control system for any text-based asset.

What you built and why this way

localStorage with JSON serialization. All version data is stored as a single JSON array under a fixed key (promptVersions). Each version object stores a label (auto-timestamped), the A and B prompt texts, and a unique ID. JSON.parse/JSON.stringify handles serialization — simple, universal, and transparent. Users can inspect their data directly in DevTools under Application → Local Storage.

Save, select, compare workflow. The "Save Current" button captures whatever is in the two textareas at that moment. Clicking a saved version loads it back into the diff inputs so you can compare it with another version or re-run the diff. Selecting two versions (one highlighted, one clicked) triggers a fresh diff between them. This three-step flow (save → select → compare) mirrors how version control tools like Git work: commit, checkout, diff.

Export/import for portability. The Export button serializes the entire version array to a downloadable JSON file. Import reads a JSON file and merges it into the existing history (skipping duplicates by ID). This enables sharing prompt histories between machines, backing up work, or collaborating on prompt libraries. No cloud service, no account — just a file.

Delete for cleanup. Each version item has a delete button (X) for removing stale or incorrect entries. The list updates immediately after deletion. This prevents the version list from accumulating noise and keeps the UI focused on active versions.

Key concepts

  • localStorage key-value API — localStorage.getItem('key') and localStorage.setItem('key', JSON.stringify(data)). Synchronous, blocking, limited to ~5MB per origin. Perfect for small version histories (100 versions at ~1KB each = 100KB)
  • JSON serialization with date formatting — Each version stores id, label (formatted like "Jul 18, 2026 3:42 PM"), versionA, and versionB. The label uses toLocaleDateString and toLocaleTimeString for readable timestamps
  • File download via Blob and ObjectURL — new Blob([json], {type: 'application/json'}) creates a file object. URL.createObjectURL(blob) generates a download link. A hidden <a> element's click event triggers the browser download dialog
  • File upload via hidden input — A hidden <input type="file" accept=".json"> is clicked programmatically. The FileReader.readAsText() method reads the selected file and parses the JSON. The merge strategy appends only versions with new IDs
  • Optimistic UI updates — Version list items are regenerated from the data array after every mutation (save, delete, import). No manual DOM manipulation for individual items — just re-render the whole list

Alternative approaches

  • IndexedDB for larger histories — IndexedDB supports much larger storage (hundreds of MB) and structured data without serialization. Overkill for prompt histories (typically <1MB) but the right choice for image-heavy or binary data
  • Git-based version control — Using a real Git repo for prompt versions gives you branches, diffs, commit messages, and collaboration. Too heavy for individual prompt iteration, but appropriate for team-shared prompt libraries
  • Server-side database (Supabase, Firebase) — Persisting to a cloud database enables cross-device sync, sharing, and collaboration. Introduces latency, cost, and privacy concerns. localStorage keeps everything local and instant
  • Session-only storage (no persistence) — Using sessionStorage instead of localStorage means versions disappear when the tab closes. Useful for ephemeral comparisons where you don't want to leave traces, but defeats the purpose of version history

Browser compatibility

  • localStorage: Chrome 4+, Firefox 3.5+, Safari 4+, IE8+. Universal support across all modern browsers
  • File API (FileReader + Blob): Chrome 7+, Firefox 3.6+, Safari 6+, IE10+. No IE9 or below support
  • URL.createObjectURL: Chrome 8+, Firefox 4+, Safari 6+, IE10+. Same support range as File API
  • JSON.parse/JSON.stringify: Chrome 3+, Firefox 3.5+, Safari 4+, IE8+. Effectively universal
  • The 5MB localStorage limit applies. A history of 5,000 versions at ~800 bytes each would hit the limit

Performance notes

  • localStorage read/write: ~3-5ms for a 50-version history (single read + parse)
  • Full list re-render: ~2-4ms for 50 version items (innerHTML replace)
  • JSON export file size: ~8KB for 50 versions (well under the 5MB localStorage limit)
  • Import merge (50 versions): ~1-2ms for deduplication by ID
  • localStorage operations are synchronous and block the main thread. For prompt histories, this is imperceptible

Common pitfalls

  • localStorage quota exceeded. At ~5MB, users with very large histories or many other apps using localStorage can hit the quota. Wrap setItem in try/catch and surface a friendly error
  • Duplicate versions on import. If the imported JSON contains version IDs that already exist, they'll appear twice. Use a Set or Map for O(1) deduplication by id
  • Timestamps on save. If users rapidly click "Save Current", they get multiple versions with the same timestamp. Append a millisecond or counter suffix to guarantee uniqueness
  • Empty save. If both textareas are empty when the user clicks Save, you save a blank version. Gate the save button on at least one textarea having text
  • JSON file import validation. Malformed or malicious JSON files passed to Import could break the app. Validate the parsed object has the expected shape (array of objects with id, label, versionA, versionB) before writing to localStorage
JS — Save version to localStorage
function saveVersion(a, b) {
  if (!a.trim() && !b.trim()) return;
  const history = JSON.parse(
    localStorage.getItem('promptVersions') ||
    '[]');
  history.push({
    id: Date.now().toString(36),
    label: new Date().toLocaleDateString(
      'en-US', {
        month: 'short', day: 'numeric',
        year: 'numeric', hour: '2-digit',
        minute: '2-digit'
      }),
    versionA: a,
    versionB: b
  });
  localStorage.setItem('promptVersions',
    JSON.stringify(history));
  renderVersionList();
}
JS — Export/import as JSON
function exportHistory() {
  const json = localStorage.getItem(
    'promptVersions');
  if (!json) return;
  const blob = new Blob([json],
    {type: 'application/json'});
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'prompt-history.json';
  a.click();
  URL.revokeObjectURL(url);
}

function importHistory(file) {
  const reader = new FileReader();
  reader.onload = (e) => {
    const imported = JSON.parse(e.target.result);
    const existing = new Set(
      getHistory().map(v => v.id));
    const merged = [
      ...getHistory(),
      ...imported.filter(v =>
        !existing.has(v.id))
    ];
    localStorage.setItem('promptVersions',
      JSON.stringify(merged));
    renderVersionList();
  };
  reader.readAsText(file);
}
JS — Version list render
function renderVersionList() {
  const history = getHistory();
  const container = document
    .getElementById('versionList');
  if (!history.length) {
    container.innerHTML =
      '<div class="version-empty">' +
      'No saved versions yet.</div>';
    return;
  }
  container.innerHTML = history
    .map(v => `<div class="version-item"
      data-id="${v.id}">
      <span class="v-label">
        ${v.label}</span>
      <span class="v-preview">
        ${escapeHtml(
          v.versionA.slice(0, 60))}
      </span>
      <button class="btn btn-sm"
        onclick="loadVersion(
          '${v.id}')">
        Load</button>
      <button class="btn btn-sm"
        onclick="deleteVersion(
          '${v.id}')">
        &times;</button>
    </div>`).join('');
}
Model
codestral-2508
Arena Score
6.5/10
Steps
2
Size
31KB
Built via arena match — 6 models competed. codestral-2508 won but required significant manual fixes. See full arena results.

📝 Lessons Learned

Key insights from building the Prompt Version Diff tool.

🔄
Refined scoring after the match

The winning model scored well on code output but produced AI-slush tutorial content and missed the site template entirely. Initial score (8.5/10) was too generous — corrected to 6.5/10 to reflect the manual rewrite needed. Arena scoring should measure final production quality, not just code correctness.

📋
Template structure must be in the prompt

The arena runner prompt listed features but didn't specify the blog's component architecture (Header/Footer imports) or the 7-subsection tutorial format. Models guessed wrong, producing inline nav and generic tutorials. Future arena prompts must include structural requirements as requirements, not suggestions.

⚙️
LCS algorithm is fast enough for real-time use

Even with 500-word prompts, the LCS DP matrix (250K cells) computes in 2-3ms. No need for optimization: the O(m*n) algorithm runs comfortably in the browser event loop. The bottleneck is DOM rendering, which adds ~1-2ms for 1,000 span nodes.

The interactive tool works well, but the page needed a complete tutorial rewrite. A better arena prompt would have saved at least one full iteration cycle.

Next tool on the roadmap
Variable Extractor →

Extract {{variables}} from prompt templates, detect duplicates, and fill values inline.

View the full roadmap →