# Study: AI Experiments Plugin — Complete Analysis

> **Reference Plugin:** `D:\xampp\htdocs\azizulhasan\tts\wp-content\plugins\ai`
> **Study Date:** 2026-02-25
> **Purpose:** Understand architecture, patterns, and implementation details to port features into AI Agent Hub plugin with advanced enhancements.

---

## Table of Contents

1. [Plugin Architecture & Bootstrap](#1-plugin-architecture--bootstrap)
2. [Abstract Base Classes](#2-abstract-base-classes)
3. [Shared Utilities & Helpers](#3-shared-utilities--helpers)
4. [Experiment 1: Image Generation](#4-experiment-1-image-generation)
5. [Experiment 2: Excerpt Generation](#5-experiment-2-excerpt-generation)
6. [Experiment 3: Summarization](#6-experiment-3-summarization)
7. [Experiment 4: Title Generation](#7-experiment-4-title-generation)
8. [Experiment 5: Alt-Text Generation](#8-experiment-5-alt-text-generation)
9. [Build System](#9-build-system)
10. [AI_Client Integration](#10-ai_client-integration)
11. [Integration Patterns Summary](#11-integration-patterns-summary)
12. [Porting Decisions for AI Agent Hub](#12-porting-decisions-for-ai-agent-hub)

---

## 1. Plugin Architecture & Bootstrap

### File: `ai.php` (Main plugin file)
- Defines `AI_EXPERIMENTS_DIR` constant
- Requires `includes/bootstrap.php`

### File: `includes/bootstrap.php`
- Checks WP version compatibility
- Loads Jetpack Autoloader from `vendor/autoload_packages.php`
- Calls `initialize_experiments()` on `init` hook
- Registers settings via `Settings_Registration::register()` on `init`

### File: `includes/Experiment_Loader.php`
- Loads experiment classes from the registry
- Calls `register()` on each enabled experiment
- Fires `ai_experiments_initialized` action when complete

### File: `includes/Experiment_Registry.php`
- Stores experiment instances by ID
- Default experiments registered via `ai_experiments_default_experiment_classes` filter:
  - `Image_Generation`
  - `Excerpt_Generation`
  - `Summarization`
  - `Title_Generation`
  - `Alt_Text_Generation`
- Additional experiments can be added via `ai_experiments_register_experiments` action

### Hook Execution Order
```
plugins_loaded → ai.php bootstrap
init           → Settings_Registration::register()
wp_abilities_api_init → All abilities registered
ai_experiments_default_experiment_classes (filter) → Modify experiment list
ai_experiments_register_experiments (action) → Register additional experiments
ai_experiments_enabled (filter) → Global toggle
ai_experiments_experiment_{id}_enabled (filter) → Per-experiment toggle
ai_experiments_initialized (action) → All experiments loaded
enqueue_block_editor_assets → Block editor JS/CSS enqueued
wp_enqueue_media → Media modal JS enqueued (Alt-Text only)
admin_enqueue_scripts → Admin page JS enqueued
```

---

## 2. Abstract Base Classes

### `Abstract_Experiment` (`includes/Abstracts/Abstract_Experiment.php`)

Base class for all experiments. Implements `Experiment` contract.

**Properties:**
- `$id` (string) — Experiment identifier
- `$label` (string) — Human-readable label
- `$description` (string) — Description
- `$enabled_cache` (?bool) — Cached enabled status

**Constructor (final):**
- Calls `load_experiment_metadata()` — must return `['id', 'label', 'description']`
- Throws `Invalid_Experiment_Metadata_Exception` if any field is empty

**Methods:**
| Method | Type | Description |
|--------|------|-------------|
| `load_experiment_metadata()` | abstract | Returns `[id, label, description]` array |
| `get_id()` | public | Returns experiment ID |
| `get_label()` | public | Returns translated label |
| `get_description()` | public | Returns translated description |
| `is_enabled()` | final public | Checks global toggle + per-experiment toggle + filter. Results cached. |
| `register_settings()` | public virtual | Override to register custom settings |
| `render_settings_fields()` | public virtual | Override to render custom settings UI |
| `get_field_option_name($name)` | final protected | Returns `ai_experiment_{id}_field_{name}` |
| `register()` | abstract | Set up hooks and functionality |

**Enable/Disable Logic:**
```php
// Both must be true:
$global  = get_option('ai_experiments_enabled', false);
$per_exp = get_option("ai_experiment_{$id}_enabled", false);
// Plus filterable:
$enabled = apply_filters("ai_experiments_experiment_{$id}_enabled", $per_exp);
```

### `Abstract_Ability` (`includes/Abstracts/Abstract_Ability.php`)

Base class for all abilities. Extends core `WP_Ability`.

**Constructor:**
```php
parent::__construct($name, [
    'label'               => $properties['label'],
    'description'         => $properties['description'],
    'category'            => $this->category(),
    'input_schema'        => $this->input_schema(),
    'output_schema'       => $this->output_schema(),
    'execute_callback'    => [$this, 'execute_callback'],
    'permission_callback' => [$this, 'permission_callback'],
    'meta'                => $this->meta(),
]);
```

**Abstract Methods:**
| Method | Returns | Description |
|--------|---------|-------------|
| `input_schema()` | array | JSON Schema for input |
| `output_schema()` | array | JSON Schema for output |
| `execute_callback($input)` | mixed\|WP_Error | Main execution logic |
| `permission_callback($input)` | bool\|WP_Error | Permission check |
| `meta()` | array | Ability metadata (must include `show_in_rest => true`) |

**Concrete Methods:**
| Method | Description |
|--------|-------------|
| `category()` | Returns `AI_EXPERIMENTS_DEFAULT_ABILITY_CATEGORY` |
| `path($experiment_id)` | Static. Returns `wp-abilities/v1/abilities/ai/{id}/run` |
| `get_system_instruction($filename, $data)` | Loads prompt from PHP file in ability's directory |
| `load_system_instruction_from_file($filename, $data)` | Reflection-based file loader. Tries `system-instruction.php` by default. Extracts `$data` as variables. |

---

## 3. Shared Utilities & Helpers

### `includes/helpers.php` (Namespace: `WordPress\AI`)

| Function | Signature | Description |
|----------|-----------|-------------|
| `normalize_content($content)` | `string → string` | Strips HTML entities, tags, shortcodes. Filterable via `ai_experiments_pre_normalize_content` and `ai_experiments_normalize_content` |
| `get_post_context($post_id)` | `int → array` | Uses `ai/get-post-details` and `ai/get-post-terms` abilities to build context array |
| `get_preferred_models_for_text_generation()` | `→ array` | Returns `[['anthropic','claude-haiku-4-5'], ['google','gemini-2.5-flash'], ['openai','gpt-4o-mini'], ['openai','gpt-4.1']]`. Filterable. |
| `get_preferred_image_models()` | `→ array` | Returns image model preferences. Filterable. |
| `get_preferred_vision_models()` | `→ array` | Returns vision model preferences. Filterable. |
| `get_ai_service()` | `→ AI_Service` | Returns singleton AI_Service instance |
| `has_ai_credentials()` | `→ bool` | Checks `wp_ai_client_provider_credentials` option |
| `has_valid_ai_credentials()` | `→ bool` | Checks credentials + tests text generation support |

### `includes/Services/AI_Service.php`

Singleton wrapper around `AI_Client`. Provides:
- `create_textgen_prompt($prompt, $options)` — Returns a fluent builder
- `create_imagegen_prompt($prompt, $options)` — Returns a fluent builder
- Options: `system_instruction`, `temperature`, `model_preference`, `candidate_count`

### `src/utils/run-ability.ts`

Universal ability executor with dual-path strategy:
```typescript
export async function runAbility<T>(ability: string, input?: AbilityInput, options?: RunAbilityOptions): Promise<T>
```

**Strategy:**
1. Try `window.wp.abilities.executeAbility(ability, input)` first
2. If throws `ability_not_found` error → fall back to REST: `apiFetch({path: '/wp-abilities/v1/abilities/{ability}/run', method: 'POST', data: {input}})`
3. All other errors are re-thrown

**Used by:** Image Generation, Summarization, Title Generation, Alt-Text Generation

### `src/utils/text.ts`
- `trimText(text, maxLength)` — Trims to max length respecting word boundaries

### `src/utils/generate-alt-text.ts`
- Shared alt-text generation function used by both Alt-Text experiment and Image Generation experiment

---

## 4. Experiment 1: Image Generation

### PHP Backend

**Experiment Class:** `includes/Experiments/Image_Generation/Image_Generation.php`
- ID: `image-generation`
- Registers 3 abilities + conditionally registers alt-text ability
- Enqueues on: `post.php`, `post-new.php` (post types supporting `thumbnail`)
- Localized data: `window.aiImageGenerationData = {enabled, altTextEnabled}`
- Custom setting: `Alt Text on Upload` toggle (auto-generate alt-text when uploading images)

**Abilities:**

| Ability | Input | Output | AI Model | Temp |
|---------|-------|--------|----------|------|
| `ai/image-generation` | `{prompt}` | Image file (inline binary) | Image models | default |
| `ai/image-prompt-generation` | `{content, context?, style?}` | Text (prompt string) | Text models | 0.9 |
| `ai/image-import` | `{data, filename?, title?, description?, alt_text?, mime_type?, meta?}` | `{attachment_id, url}` | N/A (no AI) | N/A |
| `ai/alt-text-generation` | `{attachment_id?, image_url?, context?}` | `{alt_text: string}` | Vision models | 0.3 |

**System Instructions:**
- Image prompt generation: Loaded from `system-instruction.php` in ability directory. Uses XML-wrapped content.
- Alt-text: Loaded from `system-instruction.php`. Context about image provided.

**Content Wrapping Pattern:**
```php
$content = '<content>' . $content . '</content>';
$content .= '<additional-context>' . $context . '</additional-context>';
$content .= '<style>' . $style . '</style>';
```

### JS Frontend

**Entry:** `src/experiments/image-generation/index.ts`
- Imports `featured-image` module

**Integration Point:** `addFilter('editor.PostFeaturedImage', ...)`
- Wraps the native Featured Image component with AI generation controls

**Key Component:** `GenerateFeaturedImage.tsx`
- Multi-step progress UI (5 steps):
  1. Get post context
  2. Generate prompt from context
  3. Generate image from prompt
  4. Upload/import image
  5. (Optional) Generate alt-text
- Uses `runAbility()` for all API calls
- Shows "AI Generated" label on generated images via `AILabel` component

**Flow Functions:**
| File | Function | Description |
|------|----------|-------------|
| `get-context.ts` | `getContext()` | Gets post context via `ai/get-post-details` |
| `format-context.ts` | `formatContext()` | Formats context as string |
| `generate-prompt.ts` | `generatePrompt()` | Calls `ai/image-prompt-generation` |
| `generate-image.ts` | `generateImage()` | Calls `ai/image-generation` |
| `upload-image.ts` | `uploadImage()` | Calls `ai/image-import` |

---

## 5. Experiment 2: Excerpt Generation

### PHP Backend

**Experiment Class:** `includes/Experiments/Excerpt_Generation/Excerpt_Generation.php`
- ID: `excerpt-generation`
- Registers 1 ability
- Enqueues on: `post.php`, `post-new.php` (post types supporting `excerpt`, excluding `attachment`)
- Localized data: `window.aiExcerptGenerationData = {enabled, path}`
- The `path` is `wp-abilities/v1/abilities/ai/excerpt-generation/run`

**Ability:** `includes/Abilities/Excerpt_Generation/Excerpt_Generation.php`

| Field | Value |
|-------|-------|
| Slug | `ai/excerpt-generation` |
| Input | `{content?: string, context?: string}` |
| Output | `string` (generated excerpt) |
| Model | Text models |
| Temperature | 0.7 |
| Permission | `edit_posts` (or `edit_post` for specific post) |

**Execute Logic:**
1. If `context` is numeric (post ID) → get post context via `get_post_context()`
2. If `content` provided → use it; otherwise use post content from context
3. Wrap: `<content>...</content>` + `<additional-context>...</additional-context>`
4. Call `AI_Client::prompt_with_wp_error($content)->using_system_instruction(...)->using_temperature(0.7)->using_model_preference(...)->generate_text()`
5. Return `sanitize_textarea_field(trim($result, ' "\''))`

### JS Frontend

**Entry:** `src/experiments/excerpt-generation/index.tsx`
- Two `registerPlugin()` calls:
  1. `excerpt-generation` — Uses `__experimentalPluginPostExcerpt` slot to add generate button in excerpt panel
  2. `excerpt-generation-inline` — Adds inline button next to excerpt toggle

**Integration Points:**
- `__experimentalPluginPostExcerpt` (WP experimental slot in excerpt panel)
- DOM injection via `MutationObserver` for inline button placement

**Key Hook:** `useExcerptGeneration.ts`
```typescript
function useExcerptGeneration(): {
    isGenerating: boolean;
    hasExcerpt: boolean;
    handleGenerate: () => Promise<void>;
}
```

**API Pattern:** Direct `apiFetch` (NOT `runAbility`):
```typescript
apiFetch({
    path: aiExcerptGenerationData.path,  // localized from PHP
    method: 'POST',
    data: { input: { content, post_id: postId } },
})
```

**Result Application (DOM manipulation):**
1. Update editor store: `editPost({ excerpt: generatedExcerpt })`
2. Find textarea: `document.querySelector('.editor-post-excerpt .editor-post-excerpt__textarea textarea')`
3. Use native setter: `Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set.call(el, value)`
4. Dispatch change event to trigger React sync

**Error Handling:**
- Uses `noticesStore` to show/dismiss error notices with ID `ai_excerpt_generation_error`

---

## 6. Experiment 3: Summarization

### PHP Backend

**Experiment Class:** `includes/Experiments/Summarization/Summarization.php`
- ID: `summarization`
- Registers 1 ability + registers post meta `ai_generated_summary`
- Post meta registered for ALL post types that `show_in_rest`
- Enqueues on: block editor (via `enqueue_block_editor_assets`)
- Also enqueues CSS
- Localized data: `window.aiSummarizationData = {enabled}`

**Ability:** `includes/Abilities/Summarization/Summarization.php`

| Field | Value |
|-------|-------|
| Slug | `ai/summarization` |
| Input | `{content?: string, context?: string, length?: enum('brief','standard','detailed')}` |
| Output | `string` (generated summary) |
| Model | Text models |
| Temperature | 0.9 |
| Permission | `edit_posts` |

**Dynamic System Instruction:**
```php
// The system-instruction.php file receives $length variable
$this->get_system_instruction(null, ['length' => $args['length'] ?? 'standard']);
```

**Content Wrapping:** Plain concatenation with context prefix.

### JS Frontend

**Entry:** `src/experiments/summarization/index.tsx`
- Three integration points:
  1. `registerPlugin('summarization-plugin')` → Adds "Summarize" button in `PluginPostStatusInfo`
  2. `addFilter('blocks.registerBlockType')` → Adds `aiGeneratedSummary` attribute to `core/paragraph` blocks
  3. `registerBlockVariation('core/paragraph', 'ai-summary')` → Registers block variation
  4. `addFilter('editor.BlockEdit')` → Adds toolbar controls on summary blocks

**Components:**
- `SummarizationPlugin.tsx` — Button in post status panel
- `SummarizationBlockControls.tsx` — Toolbar controls on AI summary blocks (regenerate, delete)

**Hook:** `useSummaryGeneration.ts`
- Uses `runAbility('ai/summarization', {content, context})`
- Inserts a `core/paragraph` block with `aiGeneratedSummary: true` attribute
- Updates post meta `ai_generated_summary` to track generated summaries

**Result Application:**
- Creates/updates a `core/paragraph` block in the editor
- Block identified by `aiGeneratedSummary` attribute

---

## 7. Experiment 4: Title Generation

### PHP Backend

**Experiment Class:** `includes/Experiments/Title_Generation/Title_Generation.php`
- ID: `title-generation`
- Registers 1 ability
- Enqueues on: block editor (post types supporting `title`, excluding `attachment`)
- Localized data: `window.aiTitleGenerationData = {enabled}`

**Ability:** `includes/Abilities/Title_Generation/Title_Generation.php`

| Field | Value |
|-------|-------|
| Slug | `ai/title-generation` |
| Input | `{content?: string, post_id?: number, candidates?: int(1-10)}` |
| Output | `{titles: string[]}` |
| Model | Text models |
| Temperature | 0.7 |
| Permission | `edit_posts` |

**Key Difference:** Uses `generate_texts()` (plural) for multiple candidates:
```php
AI_Client::prompt_with_wp_error($content)
    ->using_system_instruction(...)
    ->using_temperature(0.7)
    ->using_candidate_count($candidates)
    ->using_model_preference(...)
    ->generate_texts();  // Returns array of strings
```

**Content Wrapping:** Triple-quoted format:
```
"""
{context as key: value pairs}
"""
```

### JS Frontend

**Integration Point:** `addFilter('editor.BlockEdit')` on `core/post-title`
- Plus DOM injection into editor iframe for toolbar placement

**Key Component:** `TitleToolbar.tsx`
- Shows modal with editable title suggestions
- Each suggestion has a "Select" button
- User can edit suggestions before selecting

**Flow:**
1. Click "Generate Titles" in toolbar
2. Modal opens with loading state
3. API returns array of title candidates
4. User reviews, edits, and selects one
5. Selected title applied to post via `editPost({ title: selectedTitle })`

---

## 8. Experiment 5: Alt-Text Generation

### PHP Backend

**Experiment Class:** `includes/Experiments/Alt_Text_Generation/Alt_Text_Generation.php`
- ID: `alt-text-generation`
- Registers 1 ability (shared `ai/alt-text-generation` from Image experiment)
- **Three integration contexts:**
  1. Block editor — InspectorControls on `core/image` blocks
  2. Media modal — JS injected via `wp_enqueue_media`
  3. Attachment edit page — JS injected via `admin_enqueue_scripts`
- Two JS bundles: `alt-text-generation` + `alt-text-generation-media`

**Ability:** `includes/Abilities/Image/Alt_Text_Generation.php`

| Field | Value |
|-------|-------|
| Slug | `ai/alt-text-generation` |
| Input | `{attachment_id?: number, image_url?: string, context?: string}` |
| Output | `{alt_text: string}` |
| Model | Vision models |
| Temperature | 0.3 |
| Permission | `edit_posts` |

**Execute Logic:**
1. Resolve image URL from `attachment_id` or direct `image_url`
2. Load image as data URI via `AI_Client::with_file()`
3. Add context as text prompt
4. Call `generate_text()` with vision model preferences

### JS Frontend

**Block Editor Integration:**
- `addFilter('editor.BlockEdit')` targeting `core/image`
- Adds `InspectorControls` panel with "Generate Alt Text" button
- Two-step UX:
  1. **Generate** → Shows preview in `TextareaControl`
  2. **Apply/Dismiss** → Applies to block attributes or cancels

**Media Modal Integration:**
- Separate bundle `alt-text-generation-media`
- Injects button into WordPress media modal
- Uses `MutationObserver` to detect when media items are selected

**Shared Function:** `src/utils/generate-alt-text.ts`
```typescript
generateAltText({attachmentId, imageUrl, context}): Promise<{alt_text: string}>
```
Used by both Alt-Text experiment and Image Generation experiment.

---

## 9. Build System

### `webpack.config.js`
Uses `@wordpress/scripts` with custom entry points:

```javascript
module.exports = {
    ...defaultConfig,
    entry: {
        'experiments/image-generation': './src/experiments/image-generation/index.ts',
        'experiments/excerpt-generation': './src/experiments/excerpt-generation/index.tsx',
        'experiments/summarization': './src/experiments/summarization/index.tsx',
        'experiments/title-generation': './src/experiments/title-generation/index.tsx',
        'experiments/alt-text-generation': './src/experiments/alt-text-generation/index.tsx',
        'experiments/alt-text-generation-media': './src/experiments/alt-text-generation/media-index.tsx',
    }
};
```

### Build Output
- Compiled to `build/experiments/{name}/index.js` + `index.asset.php`
- Asset files auto-generated with dependencies and version hash

### Asset Loading (`Asset_Loader`)
- `enqueue_script($handle, $entry)` — Reads `build/{entry}/index.asset.php` for deps
- `localize_script($handle, $objectName, $data)` — Creates `window.ai{ObjectName}Data`
- Handle naming: `ai_{handle}` prefix
- JS global naming: `ai{ObjectName}Data` prefix

---

## 10. AI_Client Integration

### Vendor Package: `wordpress/wp-ai-client`

**Fluent Builder Pattern:**
```php
AI_Client::prompt('Your prompt text')
    ->using_system_instruction('System instruction')
    ->using_temperature(0.7)
    ->using_model_preference(['anthropic', 'claude-haiku-4-5'], ['google', 'gemini-2.5-flash'])
    ->using_candidate_count(5)
    ->generate_text();      // Single text result
    // OR
    ->generate_texts();     // Array of text results
    // OR
    ->generate_image();     // Image result
```

**Error Handling Variant:**
```php
AI_Client::prompt_with_wp_error('prompt')  // Returns WP_Error instead of throwing
    ->generate_text();
```

**Prompt Builder With WP Error:**
- Wraps every chained method in try/catch
- Exceptions converted to `WP_Error` and accumulated silently
- Errors surface only when terminal method called (`generate_text()`, `generate_image()`, etc.)
- 13 terminal methods defined

**Vision Support:**
```php
AI_Client::prompt_with_wp_error('')
    ->with_file($image_data_uri)  // Attach image for vision models
    ->using_model_preference(...get_preferred_vision_models())
    ->generate_text();
```

**Model Preference Lists (from `helpers.php`):**

| Type | Models |
|------|--------|
| Text | `anthropic/claude-haiku-4-5`, `google/gemini-2.5-flash`, `openai/gpt-4o-mini`, `openai/gpt-4.1` |
| Image | `google/gemini-3-pro-image-preview`, `google/gemini-2.5-flash-image`, `google/imagen-4.0`, `openai/gpt-image-1.5`, `openai/gpt-image-1`, `openai/dall-e-3` |
| Vision | `anthropic/claude-haiku-4-5-20251001`, `google/gemini-2.5-flash`, `openai/gpt-5-nano` |

**Credentials Storage:**
- Option: `wp_ai_client_provider_credentials`
- Array keyed by provider ID, values are API key strings
- Checked via `has_ai_credentials()` and `has_valid_ai_credentials()`

---

## 11. Integration Patterns Summary

### 11.1 Registered Abilities

| Ability Slug | Input Schema | Output | Model Type | Temp | Permission |
|---|---|---|---|---|---|
| `ai/image-generation` | `{prompt: string}` | Image file | Image | default | `upload_files` |
| `ai/image-import` | `{data, filename?, title?, description?, alt_text?, mime_type?, meta?}` | `{attachment_id, url}` | N/A | N/A | `upload_files` |
| `ai/image-prompt-generation` | `{content, context?, style?}` | Text (prompt) | Text | 0.9 | `edit_posts` |
| `ai/alt-text-generation` | `{attachment_id?, image_url?, context?}` | `{alt_text}` | Vision | 0.3 | `edit_posts` |
| `ai/excerpt-generation` | `{content?, context?}` | Text (excerpt) | Text | 0.7 | `edit_posts` |
| `ai/summarization` | `{content?, context?, length?}` | Text (summary) | Text | 0.9 | `edit_posts` |
| `ai/title-generation` | `{content?, post_id?, candidates?}` | `{titles[]}` | Text | 0.7 | `edit_posts` |
| `ai/get-post-details` | `{post_id, fields?}` | Post data | N/A | N/A | `edit_posts` |
| `ai/get-post-terms` | `{post_id, taxonomy?}` | Terms[] | N/A | N/A | `edit_posts` |

### 11.2 JS Globals / Localized Data

| Global Variable | Experiment | Properties |
|---|---|---|
| `window.aiImageGenerationData` | Image Generation | `{enabled, altTextEnabled}` |
| `window.aiExcerptGenerationData` | Excerpt Generation | `{enabled, path}` |
| `window.aiSummarizationData` | Summarization | `{enabled}` |
| `window.aiTitleGenerationData` | Title Generation | `{enabled}` |
| `window.aiAltTextGenerationData` | Alt-Text Generation | `{enabled}` |

### 11.3 Gutenberg Integration Points

| Experiment | Integration Method | Target |
|---|---|---|
| Image Generation | `addFilter('editor.PostFeaturedImage')` | Featured Image panel |
| Excerpt Generation | `registerPlugin` + `__experimentalPluginPostExcerpt` | Excerpt panel |
| Excerpt (inline) | `registerPlugin` + DOM injection + `MutationObserver` | Excerpt toggle link |
| Summarization | `registerPlugin` + `PluginPostStatusInfo` | Post Status panel |
| Summarization (blocks) | `addFilter('editor.BlockEdit')` + `registerBlockVariation` | `core/paragraph` blocks |
| Title Generation | `addFilter('editor.BlockEdit')` on `core/post-title` + DOM injection | Title block |
| Alt-Text (editor) | `addFilter('editor.BlockEdit')` on `core/image` + `InspectorControls` | Image block sidebar |
| Alt-Text (media) | `MutationObserver` on media modal | Media library modal |

### 11.4 Post Type Guards

| Experiment | Screens | Post Type Check | Exclusions |
|---|---|---|---|
| Image Generation | `post.php`, `post-new.php` | `supports('thumbnail')` | None |
| Excerpt Generation | Block editor | `supports('excerpt')` | `attachment` |
| Summarization | Block editor | None (all post types) | None |
| Title Generation | Block editor | `supports('title')` | `attachment` |
| Alt-Text | `post.php`, `post-new.php`, media modal, attachment edit | None (targets images) | None |

### 11.5 JS API Call Patterns

**Pattern A — `runAbility()` utility (most experiments):**
```javascript
import { runAbility } from '../utils/run-ability';
const result = await runAbility('ai/ability-name', { param: value });
```

**Pattern B — Direct `apiFetch` to localized path (Excerpt Generation only):**
```javascript
import apiFetch from '@wordpress/api-fetch';
const result = await apiFetch({
    path: excerptGenerationData.path,
    method: 'POST',
    data: { input: { content, context } },
});
```

### 11.6 Content Wrapping Patterns

| Ability | Format | Example |
|---------|--------|---------|
| Image Prompt Gen | XML tags | `<content>...</content>`, `<additional-context>...</additional-context>`, `<style>...</style>` |
| Excerpt Gen | XML tags | `<content>...</content>`, `<additional-context>...</additional-context>` |
| Title Gen | Triple-quoted | `"""..."""` for context |
| Summarization | Plain concat | Content with optional context prefix |
| Alt-Text Gen | Data URI + text | Image via `with_file()`, context as prompt text |

### 11.7 Error Handling Patterns

**PHP:**
- `AI_Client::prompt_with_wp_error()` — Accumulates errors silently, surfaces on terminal method call
- Early validation: content emptiness, post existence, permission checks
- All return `WP_Error` on failure

**JS:**
- `runAbility()` — Dual-path with automatic fallback
- `noticesStore` for user-facing error display
- Try/catch with `finally` for loading state cleanup

### 11.8 Settings System

| Option | Description |
|--------|-------------|
| `ai_experiments_enabled` | Global toggle (boolean) |
| `ai_experiment_{id}_enabled` | Per-experiment toggle |
| `ai_experiment_{id}_field_{name}` | Custom experiment settings |

---

## 12. Porting Decisions for AI Agent Hub

### What We're Porting
All 5 experiments: Image Gen, Excerpt Gen, Summarization, Title Gen, Alt-Text Gen

### Key Differences from Reference Plugin

| Aspect | AI Experiments (Reference) | AI Agent Hub (Our Plugin) |
|--------|---------------------------|--------------------------|
| Language | TypeScript (.tsx/.ts) | JavaScript (.js) |
| Build System | `@wordpress/scripts` | Laravel Mix 6 |
| UI Framework | `@wordpress/components` | `@wordpress/components` (for editor) |
| Ability Registration | Own `Abstract_Ability` → `WP_Ability` | Direct `wp_register_ability()` with config arrays |
| API Pattern | `runAbility()` dual-path | Our own pattern (TBD) |
| Provider Selection | Auto (AI_Client handles it) | User-selectable via dropdown |
| Model Selection | Hardcoded preference lists | User-selectable via dropdown |
| Prompt Visibility | Hidden (system instructions) | Visible, editable, saveable |
| Post-Generation | Direct apply | 3 buttons: Apply, Regenerate, Modify Prompt |
| Settings | Global WP Settings page | In-editor popup per tool |

### Our Provider Endpoint

**`GET /awfah/v1/providers`** (requires `manage_options`)
```json
{
    "providers": [
        {"id": "openai", "label": "OpenAI", "type": "provider", "supported": true},
        {"id": "google", "label": "Google Gemini", "type": "provider", "supported": false},
        {"id": "anthropic", "label": "Anthropic Claude", "type": "provider", "supported": true}
    ]
}
```

### Prompt Storage Decision

**Hybrid approach (recommended):**
- **Global prompts:** `wp_options` table — `awfah_prompt_{tool_slug}` (e.g., `awfah_prompt_excerpt-generation`)
- **Per-post prompts:** `wp_postmeta` table — `_awfah_prompt_{tool_slug}` (e.g., `_awfah_prompt_excerpt-generation`)
- **Prompt templates (Pro):** Custom table `{prefix}awfah_prompt_templates` for template library feature

### Build System Integration

Existing `webpack.mix.js` entry points:
```javascript
mix.js('assets/js/admin-app.js', 'js/admin-app.min.js').react();
mix.js('assets/js/post-meta-box.js', 'js/post-meta-box.min.js').react();
```

New entry points to add:
```javascript
mix.js('assets/js/experiments/excerpt-generation.js', 'js/experiments/excerpt-generation.min.js').react();
mix.js('assets/js/experiments/title-generation.js', 'js/experiments/title-generation.min.js').react();
mix.js('assets/js/experiments/summarization.js', 'js/experiments/summarization.min.js').react();
mix.js('assets/js/experiments/image-generation.js', 'js/experiments/image-generation.min.js').react();
mix.js('assets/js/experiments/alt-text-generation.js', 'js/experiments/alt-text-generation.min.js').react();
```

### Creative Features to Implement

1. **Prompt Templates Library** — Dropdown of pre-made prompts per tool (e.g., "SEO-Optimized", "Creative", "Professional" for titles)
2. **Generation History** — Last 3-5 generations stored in post meta, browseable in collapsible panel
3. **Tone/Style Quick Selector** — Chips that auto-modify the prompt (Professional, Casual, Friendly, Academic)
4. **"Explain This Generation" Button** — Shows AI reasoning about the generation
5. **Batch Generation Mode** — For Title Gen: 5 titles with different tones in comparison cards
6. **Content Quality Score** — Readability, SEO keyword match, character count indicators
7. **Keyboard Shortcut** — `Ctrl+Shift+G` for quick generation
8. **Smart Context Detection** — Disable button if post content too short, show helpful message

---

*End of Study Document*
