Leo
  • Posts
  • Projects
  • About
  • Posts
  • Projects
  • About
Bilibili抖音小红书

© 2026 XT. All rights reserved.

  1. Home
  2. Posts
  3. Inside AI Prompt Studio: How a Chrome Extension Bridges 12+ Vision LLMs

Inside AI Prompt Studio: How a Chrome Extension Bridges 12+ Vision LLMs

AI Prompt Studio is a Chrome extension that reverse-engineers AI image prompts from any web image using your own vision LLM API. This post dives into its message-driven MV3 architecture, Shadow DOM isolation, unified streaming layer, and component-based strategy system.

Leo WangJune 15, 2026

By Leo Wang
Published: 2026-06-15

TL;DR
AI Prompt Studio is a Chrome MV3 extension that reverse-engineers prompts from images with a right-click and your own vision API. This post covers: message-driven MV3 architecture → Shadow DOM floating panel → unified streaming layer for 22 providers → component-based strategy system → video storyboarding.

Quick Try

  1. Install from Chrome Web Store (free)
  2. Open the extension Options page and configure a Vision API key, or top up the built-in service
  3. On any webpage, right-click an image → AI Prompt Studio → Extract prompt. Results appear in the bottom-right panel within seconds

No API key yet? See the project website for a full walkthrough and feature overview.

Table of Contents

  • 1. Introduction
  • 2. Architecture: Chrome MV3 + Monorepo
  • 3. Content Script & Shadow DOM Isolation
  • 4. Unified Streaming Layer for 12+ Providers
  • 5. Prompt Extraction Strategy System
  • 6. Video Sampling & Multi-Image Composition
  • 7. Engineering Practices & Lessons
  • 8. Conclusion

1. Introduction

If you use AI image generation tools like Stable Diffusion or Midjourney, you know the pain: you find a stunning reference image online, but translating its lighting, texture, color palette, and composition into a usable prompt takes time and skill.

AI Prompt Studio is a Chrome Manifest V3 extension that solves this. Right-click any image on the web, and it sends the visual to your own vision LLM API key, which reverse-engineers a prompt. Seconds later, the result appears in a floating panel in the bottom-right corner, ready to copy.

This post breaks down the architecture, design decisions, and engineering challenges behind the project. The tech stack: React 19 + TypeScript 5.7 + Vite 6 + Tailwind CSS v4 + Chrome MV3.


2. Architecture: Chrome MV3 + Monorepo

One-liner: Four independent execution contexts, all cross-context communication goes through the Service Worker for unidirectional data flow.

2.1 The Four-Layer MV3 Structure

Manifest V3 splits the extension into four independent execution contexts:

加载图表...
  • Background Service Worker: Stateless message routing hub. All API calls and storage/IndexedDB operations go through here exclusively.
  • Content Script: Injected into every page (all_frames: true). Renders the floating panel inside Shadow DOM.
  • Popup: Toolbar popup for quick actions and recent history.
  • Options Page: Full settings UI (providers, models, strategies) and prompt library.

2.2 Message Protocol Architecture

The entire extension is message-driven via a RuntimeMessage discriminated union:

// src/lib/types/messages.ts
type RuntimeMessage =
  | { type: 'EXTRACT_PROMPT'; payload: { imageUrl: string; requestId: string } }
  | { type: 'EXTRACT_RESULT'; payload: { requestId: string; prompt: string } }
  | { type: 'EXTRACT_ERROR'; payload: { requestId: string; error: string } }
  | { type: 'REFINE_PROMPT'; payload: { current: string; instruction: string } }
  | { type: 'PANEL_APPEND_REFERENCE'; payload: { imageUrl: string } }
// ... 30+ variants

Add a message type by adding one variant. TypeScript ensures exhaustive switch/match branches.

2.3 Monorepo Structure

AI-Prompt-Studio/
├── src/                  # Extension (React + Vite + MV3)
├── website/              # Marketing site (Next.js App Router)
└── services/api/         # Platform API (Bun/Elysia + SQLite + Docker)

Key benefit: Shared type definitions in src/lib/types/ (ProviderId, RuntimeMessage, etc.) are consumed by all three workspaces, eliminating protocol drift.


3. Content Script & Shadow DOM Isolation

One-liner: The entire React panel lives inside Shadow DOM for style isolation; a 6-state FSM manages its lifecycle.

3.1 Why Shadow DOM?

Content Script styles and host page CSS leak into each other. The fix: render inside Shadow DOM.

const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
const root = createRoot(shadow);
root.render(<PanelApp />);
document.body.appendChild(host);

All styles are compiled by Tailwind and inlined into the Shadow DOM.

3.2 Panel FSM

The floating panel needs an explicit finite state machine:

idle → compose → streaming → result ──→ refining
                              ↘ error

Under 80 lines, pure function:

// src/content/panel/panelFsm.ts
const PANEL_FSM_TRANSITIONS = {
  idle: { PANEL_OPEN: 'compose', APPEND_REFERENCE: 'compose' },
  compose: { EXTRACT_START: 'streaming', PANEL_CLOSE: 'idle' },
  streaming: {
    EXTRACT_DONE: 'result',
    EXTRACT_FAIL: 'error',
    PANEL_CLOSE: 'idle'
  },
  result: {
    REFINE_START: 'refining',
    EXTRACT_START: 'streaming',
    PANEL_CLOSE: 'idle'
  },
  error: { EXTRACT_START: 'streaming', PANEL_CLOSE: 'idle' },
  refining: { REFINE_DONE: 'result', PANEL_CLOSE: 'idle' }
}

The reducer uses a handler-per-event pattern: each event type gets its own file under reducerHandlers/, preventing the main reducer from ballooning.


4. Unified Streaming Layer for 12+ Providers

One-liner: Three native API protocols → unified SSE parser → graceful fallback to client-side typewriter. No static model table at runtime.

4.1 Three Protocols, One Pipeline

The extension supports 22 providers behind three distinct API protocols:

ProtocolStreaming Implementation
OpenAI Chat Completionsstream: true + SSE data: lines
Anthropic Messagesstream: true + content_block_delta / text_delta
Google Gemini:streamGenerateContent?alt=sse endpoint

Adding a new provider is just one entry in the registry table.

4.2 SSE Parsing + Graceful Fallback

The core parser is an async generator that splits bytes into data: lines (abbreviated):

// src/lib/api/http.ts
export async function* readSseDataChunks(resp: Response) {
  const reader = resp.body.getReader()
  const decoder = new TextDecoder()
  let buf = ''
  for await (const chunk of iterReader(reader)) {
    buf += decoder.decode(chunk, { stream: true })
    // split by \n and yield payload after "data:"
  }
}

Graceful fallback: Instead of maintaining a static capability table, the runtime checks Content-Type at call time. Real SSE for text/event-stream, otherwise parse full JSON and simulate typewriter reveal.

if (!isSseResponse(resp)) {
  // Fallback: parse full JSON + client-side typewriter
  const json = await resp.json()
  await revealBufferedExtractProgress(onProgress, final, signal)
} else {
  // Normal SSE: accumulate chunks, throttle to 80ms
  for await (const data of readSseDataChunks(resp)) {
    applyOpenAIStreamDelta(cfg, acc, deltaObj)
  }
}

Typewriter simulation key logic:

// Skip animation for text ≤12 chars; max 30 frames, 1500ms
const frameCount = Math.min(30, Math.max(2, Math.ceil(final.length / 8)))
for (let i = 1; i <= frameCount; i++) {
  emitPartial(final.slice(0, Math.floor((i / frameCount) * final.length)))
  await sleep(frameMs, signal)
}

4.3 Engineering Lesson: Streaming Debug SOP

When streaming output is truncated, follow this order. Do not touch parameters first.

Debug case: MiniMax M3 generated only 1-2 sentences. Two rounds of wrong fixes (max_tokens doubled, then max_completion_tokens) did nothing. Root cause: = instead of += when accumulating delta.content:

// BUG
acc.answerRaw = contentPiece // only keeps the last fragment

// FIX
acc.answerRaw += contentPiece // accumulates full output

MiniMax docs claim delta.content is cumulative snapshots; the actual API sends incremental deltas. The debugging sequence:

  1. Check data accumulation first — = vs +=
  2. Verify delta field names — API reality vs code assumptions
  3. Check token parameters — last thing to touch
  4. Inspect SSE parsing — confirm no silent drops

5. Prompt Extraction Strategy System

One-liner: Each strategy tier combines 3 independent component versions. Metadata and instruction text are physically split, saving ~26KB in Content Script bundles.

5.1 Component-Based Architecture

Each "strategy tier" is decomposed into three component dimensions:

DimensionDescriptionVersions
stylePromptSetInstruction text per OutputStylev0.1.0, shared, v0.3.6, v0.4.0
samplingTemperature + maxTokens pairv0.1.0, shared
customJoinUser template positionv0.1.0, shared

A strategy is just metadata + three version references:

const STRATEGIES_INTERNAL = {
  classic: {
    label: 'Classic',
    components: {
      stylePromptSet: 'v0.1.0',
      sampling: 'v0.1.0',
      customJoin: 'v0.1.0'
    }
  },
  v036: {
    label: 'v0.3.6',
    components: {
      stylePromptSet: 'v0.3.6',
      sampling: 'shared',
      customJoin: 'shared'
    }
  },
  v040: {
    label: 'v0.4.0',
    components: {
      stylePromptSet: 'v0.4.0',
      sampling: 'shared',
      customJoin: 'shared'
    }
  },
  native: {
    label: 'Native Recognition',
    components: {
      stylePromptSet: 'v_native',
      sampling: 'shared',
      customJoin: 'shared'
    }
  },
  custom: {
    /* free-form mixing */
  }
}

Add or remove a tier by changing only this one object. StrategyId is auto-derived from keyof typeof STRATEGIES_INTERNAL.

5.2 Bundle Size Optimization

Content Script only needs strategy labels, not the full instruction text. These are physically split into two files:

  • strategies-meta.ts: lightweight metadata → safe for Content Script
  • strategies.ts: full prompt text + resolvers → Service Worker / Options

Before splitting, the Content Script was ~26KB larger.


6. Video Sampling & Multi-Image Composition

One-liner: Up to 8 reference images → one prompt. Video frames with timestamps → storyboard. GIF/WebP flattened to first frame.

Multi-image composition: First image is primary anchor, rest supplement style/texture/lighting:

const MULTI_IMAGE_INSTRUCTION_NOTE =
  'This message contains multiple reference images. Use the first as the primary anchor for subject, composition, and visual weight; the rest supplement style, material, lighting, and detail.'

Video storyboarding: Auto-capture frames with timestamp anchors for motion understanding.

Animated image flattening: GIF/APNG/animated WebP reduced to first frame.


7. Engineering Practices & Lessons

One-liner: Service Workers die after 30s idle; IndexedDB is for large data but Content Scripts can't access it directly; discriminated union messages guarantee type safety.

7.1 Chrome Extension Pitfalls

Service Worker lifecycle: Terminated after 30s inactivity. Persistent state must explicitly write to chrome.storage or IndexedDB — global variables are unreliable.

chrome.storage vs IndexedDB: chrome.storage.local for small configs; IndexedDB for large structured data. Content Scripts cannot read IndexedDB directly — must go through Service Worker relay.

Cross-context type safety: RuntimeMessage discriminated union ensures exhaustive type checking. All handlers registered in background/messages.ts.

7.2 AGENTS.md: Context Anchor for AI Collaboration

The project maintains an AGENTS.md file as an "AI visitor's guide", covering module maps, message protocols, FSM events, and coding conventions — so AI agents can grasp the architecture in minutes.


8. Conclusion

AI Prompt Studio is a "small tool, big architecture" project. The core problem is simple — turn images into prompts — but the engineering challenges were anything but: multi-protocol API compatibility, graceful streaming fallback, message-driven cross-context communication, componentized strategy design, and Content Script bundle optimization.

If you're interested in the technical details:

  • Install from Chrome Web Store
  • GitHub Repository
  • Project Website

Originally published in English on the author's blog. For Chinese version, see the companion post.