Back to catalog

Platform Integration Workflow

Guide for adding new AI platform support (e.g., Gemini, Mistral, Anthropic) to the Chrome extension with strategy pattern and testing requirements

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • GitHub stars: 5
  • Updated: 2025-11-21

Download the latest version (v1.0.0) from this page.

SKILL.md

---
name: Platform Integration Workflow
description: Guide for adding new AI platform support (e.g., Gemini, Mistral, Anthropic) to the Chrome extension with strategy pattern and testing requirements
---

# Platform Integration Workflow

This skill guides you through adding support for new AI platforms (like Gemini, Mistral.ai, Meta AI, etc.) to the My Prompt Manager Chrome extension.

## When to Use This Skill

- User asks to add support for a new AI platform or website
- User mentions "add platform", "support [platform name]", "integrate with [AI]"
- User wants the prompt library icon to appear on a new AI chat interface
- User asks about platform compatibility or extending platform support

## Overview

Adding a new platform requires a **10-step workflow** using the centralized configuration system introduced in v1.3+. The process takes approximately 15-30 minutes for basic support.

**Architecture Pattern:** Strategy Pattern + Centralized Configuration
- Each platform has a strategy class extending `PlatformStrategy`
- All platforms are registered in `src/config/platforms.ts`
- Platform detection, injection, and insertion are handled automatically

## Prerequisites

Before starting, ensure you have:
1. The target platform URL (e.g., `gemini.google.com`)
2. Browser DevTools open on the target platform
3. Understanding of the input element type (textarea vs contenteditable)
4. Preferred Tailwind classes for the platform badge styling (optional but recommended)

## 10-Step Integration Workflow

### Step 1: Inspect the Target Platform

**Action:** Open browser DevTools on the target platform and identify:

```javascript
// In browser console, test selectors:
document.querySelectorAll('textarea[placeholder*="message"]')
document.querySelectorAll('div[contenteditable="true"]')
document.querySelectorAll('[role="textbox"]')
```

**What to find:**
- CSS selector for the text input element
- Container where the icon should be placed
- Input element type (textarea, contenteditable, or custom)

**Common patterns:**
- ChatGPT: `textarea[data-testid="chat-input"]`
- Claude: `div[contenteditable="true"][role="textbox"]`
- Perplexity: `textarea[placeholder*="Ask"]`

### Step 2: Add Platform Configuration

**File:** `src/config/platforms.ts`

**Action:** Add a new entry to the `SUPPORTED_PLATFORMS` object:

```typescript
export const SUPPORTED_PLATFORMS: Record<string, PlatformDefinition> = {
  // ... existing platforms ...

  yourplatform: {
    id: 'yourplatform',                          // Unique ID (lowercase, no spaces)
    hostname: 'your-ai-platform.com',            // Exact hostname
    displayName: 'Your AI Platform',             // Human-readable name
    priority: 70,                                 // 60-79 for new platforms
    defaultEnabled: true,                         // Include in fresh installs
    selectors: [                                  // CSS selectors (most specific first)
      'textarea[data-testid="chat-input"]',
      'div[contenteditable="true"][role="textbox"]',
      '[role="textbox"]'                         // Generic fallback
    ],
    buttonContainerSelector: '.toolbar-container', // Where to place icon (optional)
    strategyClass: 'YourPlatformStrategy',        // Strategy class name
    hostnamePatterns: ['yourplatform', 'your-ai'], // Additional hostname patterns
    brandColors: {                                // Tailwind classes for badge styling
      enabled: 'bg-[#123456] text-white shadow-sm',
      disabled: 'bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
    }
  }
};
```

> ℹ️ If you omit `brandColors`, the UI falls back to a green gradient badge so the integration still renders correctly.

**Priority Guidelines:**
- 100: Premium platforms (Claude, ChatGPT)
- 80-90: Major platforms (Perplexity, Mistral, Gemini)
- 60-79: Secondary platforms (new additions)
- < 60: Experimental/beta platforms

### Step 2.1: Update SiteCard UI (Optional)

Add or update the platform card shown in settings so users can enable/disable the integration.

**When to do this:**
- You're adding a core platform (not just a custom site).
- You want the new platform visible in the Settings UI.

**File:** `src/components/settings/SiteCard.tsx`

**Actions:**
1. Add an icon component in `src/components/icons/SiteIcons.tsx` (or reuse an existing one).
2. Update `src/components/settings/SiteCardList.tsx` (or wherever cards are listed) to include the new platform with the right icon.
3. Make sure the new card passes the correct hostname and uses `getBrandColors(hostname)`.
4. Verify the badge colors/labels look correct; adjust `brandColors` if needed.

**Tip:** The label for the platform name comes from `displayName` in `SUPPORTED_PLATFORMS`. Keep them in sync.

### Step 3: Create the Platform Strategy

**File:** `src/content/platforms/yourplatform-strategy.ts`

**Action:** Create a new strategy class:

```typescript
import { getPlatformById } from '../../config/platforms';
import type { InsertionResult } from '../types/index';
import type { UIElementFactory } from '../ui/element-factory';
import { PlatformStrategy } from './base-strategy';

export class YourPlatformStrategy extends PlatformStrategy {
  constructor(hostname?: string) {
    super('yourplatform', 70, {
      selectors: [
        'textarea[placeholder*="Enter your message"]',
        'div[contenteditable="true"][role="textbox"]'
      ],
      buttonContainerSelector: '.toolbar-container',
      priority: 70
    }, hostname);
  }

  /**
   * Determines if this strategy can handle the element
   * Handles any element on your-ai-platform.com
   */
  canHandle(_element: HTMLElement): boolean {
    return this.hostname === getPlatformById('yourplatform')?.hostname;
  }

  /**
   * Inserts content using platform-compatible methods
   */
  async insert(element: HTMLElement, content: string): Promise<InsertionResult> {
    try {
      // Focus the element first
      element.focus();
      element.click();

      // Wait for platform initialization
      await new Promise(resolve => setTimeout(resolve, 50));

      // Method 1: Try for textarea elements
      if (element.tagName === 'TEXTAREA') {
        (element as HTMLTextAreaElement).value = content;
        element.dispatchEvent(new Event('input', { bubbles: true }));
        element.dispatchEvent(new Event('change', { bubbles: true }));
        return { success: true, method: 'yourplatform-textarea' };
      }

      // Method 2: Try for contenteditable elements
      if (element.contentEditable === 'true') {
        // Clear existing content
        element.textContent = '';

        // Insert new content
        const inserted = document.execCommand('insertText', false, content);

        if (inserted) {
          // Trigger platform events
          element.dispatchEvent(new InputEvent('input', {
            bubbles: true,
            cancelable: true,
            inputType: 'insertText',
            data: content
          }));

          return { success: true, method: 'yourplatform-execCommand' };
        }
      }

      return { success: false, error: 'Unsupported element type' };

    } catch (error) {
      this._error('Insertion failed', error as Error);
      return { success: false, error: (error as Error).message };
    }
  }

  /**
   * Gets selectors for finding input elements
   */
  getSelectors(): string[] {
    return this.config?.selectors || [];
  }

  /**
   * Creates platform-specific icon using the UI factory
   */
  createIcon(uiFactory: UIElementFactory): HTMLElement | null {
    return uiFactory.createFloatingIcon(); // Use generic icon
    // OR: return uiFactory.createYourPlatformIcon(); // Custom icon
  }
}
```

**Reference Example:** See `src/content/platforms/perplexity-strategy.ts` for a complete implementation with multiple fallback methods.

### Step 4: Register the Strategy

**File:** `src/content/platforms/index.ts`

**Action:** Add export for your strategy:

```typescript
export { YourPlatformStrategy } from './yourplatform-strategy';
```

**File:** `src/content/platforms/platform-manager.ts`

**Action:** Import and register in the constructor:

```typescript
// Add import at top
import { YourPlatformStrategy } from './yourplatform-strategy';

// In the constructor, add to the strategies array:
constructor(options: PlatformManagerOptions = {}) {
  this.strategies = [
    new ClaudeStrategy(),
    new ChatGPTStrategy(),
    new PerplexityStrategy(),
    new MistralStrategy(),
    new GeminiStrategy(),
    new YourPlatformStrategy(), // Add your strategy here
    new DefaultStrategy()       // IMPORTANT: Keep DefaultStrategy last
  ];
  // ... rest of constructor
}
```

### Step 5: Update Manifest Permissions (if needed)

**File:** `manifest.json`

**Action:** If the platform hostname is not already covered by `<all_urls>`, add it to content_scripts matches:

```json
{
  "content_scripts": [
    {
      "matches": [
        "https://*.your-ai-platform.com/*"
      ],
      "js": ["src/content/index.ts"],
      "css": ["src/content/styles/injected-icon.css"]
    }
  ]
}
```

**Note:** The extension uses `<all_urls>` host_permissions, so this step is usually not needed.

### Step 6: Create Unit Tests

**File:** `src/content/platforms/__tests__/yourplatform-strategy.test.ts`

**Action:** Create comprehensive tests:

```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { YourPlatformStrategy } from '../yourplatform-strategy';

describe('YourPlatformStrategy', () => {
  let strategy: YourPlatformStrategy;

  beforeEach(() => {
    strategy = new YourPlatformStrategy('your-ai-platform.com');
  });

  describe('canHandle', () => {
    it('returns true for your-ai-platform.com', () => {
      const element = document.createElement('textarea');
      expect(strategy.canHandle(element)).toBe(true);
    });

    it('returns false for other hostnames', () => {
      const otherStrategy = new YourPlatformStrategy('other-site.com');
      const element = document.createElement('textarea');
      expect(otherStrategy.canHandle(element)).toBe(false);
    });
  });

  describe('insert', () => {
    it('inserts content into textarea element', async () => {
      const textarea = document.createElement('textarea');
      document.body.appendChild(textarea);

      const result = await strategy.insert(textarea, 'Test content');

      expect(result.success).toBe(true);
      expect(textarea.value).toBe('Test content');
      expect(result.method).toBe('yourplatform-textarea');

      document.body.removeChild(textarea);
    });

    it('inserts content into contenteditable element', async () => {
      const div = document.createElement('div');
      div.contentEditable = 'true';
      div.setAttribute('role', 'textbox');
      document.body.appendChild(div);

      const result = await strategy.insert(div, 'Test content');

      expect(result.success).toBe(true);
      expect(div.textContent).toContain('Test content');

      document.body.removeChild(div);
    });

    it('returns error for unsupported elements', async () => {
      const span = document.createElement('span');
      const result = await strategy.insert(span, 'Test content');

      expect(result.success).toBe(false);
      expect(result.error).toBeDefined();
    });
  });

  describe('getSelectors', () => {
    it('returns configured selectors', () => {
      const selectors = strategy.getSelectors();
      expect(selectors).toBeInstanceOf(Array);
      expect(selectors.length).toBeGreaterThan(0);
    });
  });
});
```

### Step 7: Test Locally

**Action:** Build and load the extension:

```bash
# Build the extension
npm run build

# Load in Chrome:
# 1. Open chrome://extensions/
# 2. Enable Developer mode
# 3. Click "Load unpacked"
# 4. Select the dist/ folder
```

**Manual Testing Checklist:**
- [ ] Visit your-ai-platform.com
- [ ] Verify the prompt library icon appears
- [ ] Check icon positioning is correct
- [ ] Click the icon to open prompt selector
- [ ] Select a prompt from the library
- [ ] Verify content is inserted into the input field
- [ ] Confirm the platform recognizes the inserted content
- [ ] Test in both light and dark mode
- [ ] Check browser console for errors

**Debug Mode:**
```javascript
// Enable debug logging in browser console:
localStorage.setItem('prompt-library-debug', 'true');
// Then reload the page

// Check debug interface:
console.log(window.__promptLibraryDebug?.strategies);
console.log(window.__promptLibraryDebug?.activeStrategy);
console.log(window.__promptLibraryDebug?.foundElements);
```

### Step 8: Run Automated Tests

**Action:** Ensure all tests pass:

```bash
# Run unit tests
npm test

# Run specific test file
npm test -- yourplatform-strategy.test.ts

# Run with coverage
npm run test:coverage
```

**CRITICAL:** Both tests and linting MUST pass before proceeding:

```bash
# Run linting
npm run lint

# Fix linting issues automatically
npm run lint:fix
```

### Step 9: Create Platform Icon (Optional)

**When to create a custom icon:**
- Platform has unique UI styling that requires custom integration
- You want to match the platform's native button design
- The generic floating icon doesn't fit the platform's visual language

**When to use the generic icon:**
- Platform has simple UI without specific styling requirements
- You want quick integration (just call `uiFactory.createFloatingIcon()`)
- Platform uses standard button patterns

#### Option A: Custom Platform Icon (Recommended for Major Platforms)

**File:** `src/content/ui/element-factory.ts`

**Action:** Add a custom icon creation method following the modern pattern:

```typescript
/**
 * Creates Your Platform-specific icon matching native button styling
 * Uses shared prompt-library-integrated-icon class for E2E test compatibility
 */
createYourPlatformIcon(): HTMLElement {
  const icon = document.createElement('button');

  // IMPORTANT: Always include 'prompt-library-integrated-icon' for E2E test compatibility
  // Add platform-specific Tailwind classes after the shared class
  icon.className = 'prompt-library-integrated-icon flex items-center gap-2 px-3 h-10 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 active:scale-[0.98] transition-all duration-200 text-gray-700 dark:text-gray-300';

  // Required attributes
  icon.setAttribute('type', 'button');
  icon.setAttribute('aria-label', 'Open my prompt manager - Access your saved prompts');
  icon.setAttribute('title', 'My Prompt Manager - Access your saved prompts');
  icon.setAttribute('data-instance-id', this.instanceId);
  icon.setAttribute('tabindex', '0');
  icon.setAttribute('data-dashlane-label', 'true');

  // Create SVG using secure utility (STANDARD SIZE: 18x18)
  const svg = createSVGElement('svg', {
    xmlns: 'http://www.w3.org/2000/svg',
    width: '18',  // Standard size - use 18x18 for consistency
    height: '18',
    viewBox: '0 0 24 24',
    fill: 'currentColor',
    'aria-hidden': 'true',
    style: 'flex-shrink: 0;'
  });

  // Standard chat bubble icon with three dots (consistent across platforms)
  const path = createSVGElement('path', {
    d: 'M5.65 9.42C5.93 9.42 6.17 9.32 6.36 9.13C6.55 8.94 6.65 8.69 6.65 8.4C6.65 8.1 6.55 7.85 6.36 7.66C6.17 7.47 5.93 7.37 5.65 7.37C5.37 7.37 5.13 7.47 4.94 7.66C4.75 7.85 4.65 8.1 4.65 8.4C4.65 8.69 4.75 8.94 4.94 9.13C5.13 9.32 5.37 9.42 5.65 9.42ZM10.08 9.42C10.36 9.42 10.6 9.32 10.79 9.13C10.98 8.94 11.08 8.69 11.08 8.4C11.08 8.1 10.98 7.85 10.79 7.66C10.6 7.47 10.36 7.37 10.08 7.37C9.8 7.37 9.56 7.47 9.37 7.66C9.18 7.85 9.08 8.1 9.08 8.4C9.08 8.69 9.18 8.94 9.37 9.13C9.56 9.32 9.8 9.42 10.08 9.42ZM14.32 9.42C14.6 9.42 14.84 9.32 15.03 9.13C15.22 8.94 15.32 8.69 15.32 8.4C15.32 8.1 15.22 7.85 15.03 7.66C14.84 7.47 14.6 7.37 14.32 7.37C14.04 7.37 13.8 7.47 13.61 7.66C13.42 7.85 13.32 8.1 13.32 8.4C13.32 8.69 13.42 8.94 13.61 9.13C13.8 9.32 14.04 9.42 14.32 9.42ZM0 19.21V1.58C0 1.18 0.15 0.81 0.45 0.49C0.75 0.16 1.1 0 1.5 0H18.5C18.88 0 19.23 0.16 19.54 0.49C19.85 0.81 20 1.18 20 1.58V15.3C20 15.7 19.85 16.07 19.54 16.39C19.23 16.72 18.88 16.88 18.5 16.88H4L1.28 19.76C1.04 20.01 0.77 20.06 0.46 19.93C0.15 19.8 0 19.56 0 19.21ZM1.5 17.28L3.37 15.3H18.5V1.58H1.5V17.28ZM1.5 1.58V15.3V17.28V1.58Z'
  });

  svg.appendChild(path);
  icon.appendChild(svg);

  // Add text label (optional - include for button-style integrations)
  const textElement = createElement('span', {
    class: 'text-sm font-medium whitespace-nowrap'
  });
  textElement.textContent = 'My Prompts';
  icon.appendChild(textElement);

  return icon;
}
```

**Key Patterns to Follow:**

1. **Always include `prompt-library-integrated-icon` class** - Required for E2E test selectors
2. **Use Tailwind classes** - No custom CSS needed, all styling via Tailwind utilities
3. **Standard 18x18 SVG sizing** - Consistent across all platforms
4. **Use `createElement` and `createSVGElement`** - Secure DOM construction (never `innerHTML`)
5. **Include dark mode classes** - Use `dark:` variants for all styles
6. **Text label optional** - Include for button-style integrations (Claude, Perplexity, Mistral, Copilot)

**Reference Examples:**
- Button with text: See `createClaudeIcon()`, `createPerplexityIcon()`, `createCopilotIcon()`
- Icon only: See `createChatGPTIcon()`, `createGeminiIcon()`
- Generic floating: See `createFloatingIcon()`

#### Option B: React Icon Component (for Settings UI)

**File:** `src/components/icons/SiteIcons.tsx`

**Action:** Add a React icon component for the settings UI:

```typescript
export const YourPlatformIcon: React.FC<{ className?: string; disabled?: boolean }> = ({
  className = 'h-6 w-6',
  disabled = false
}) => {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="none">
      {/* Your platform's SVG path */}
      <path
        d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z"
        fill={disabled ? 'currentColor' : '#YOUR_BRAND_COLOR'}
      />
    </svg>
  );
};
```

### Step 10: Update Documentation

**File:** `docs/PLATFORM_INTEGRATION.md`

**Action:** Add your platform to the examples section:

```markdown
### Example: Your Platform

\`\`\`typescript
export class YourPlatformStrategy extends PlatformStrategy {
  constructor() {
    super('yourplatform', 70, {
      selectors: ['textarea[data-testid="input"]'],
      priority: 70
    });
  }

  canHandle(_element: HTMLElement): boolean {
    return this.hostname === 'your-ai-platform.com';
  }

  async insert(element: HTMLElement, content: string): Promise<InsertionResult> {
    // Implementation details...
  }
}
\`\`\`
```

**File:** `README.md`

**Action:** Add to the supported platforms list:

```markdown
- **Your Platform** - Full support for prompt injection
```

## Common Pitfalls and Solutions

### Pitfall 1: Button Doesn't Appear

**Symptoms:** Icon doesn't show up on the platform

**Solutions:**
1. Check `canHandle()` returns true for your domain
2. Verify selectors match actual DOM elements using DevTools
3. Ensure strategy is registered in `platform-manager.ts`
4. Check browser console for injection errors
5. Verify hostname in `platforms.ts` matches exactly

**Debug:**
```javascript
// In browser console:
document.querySelectorAll('your-selector-here')
window.__promptLibraryDebug?.strategies
```

### Pitfall 2: Button Appears in Wrong Location

**Symptoms:** Icon is visible but poorly positioned

**Solutions:**
1. Update `buttonContainerSelector` in `platforms.ts`
2. Add platform-specific positioning in `src/content/core/injector.ts`
3. Use multiple container selectors (comma-separated) for fallbacks

**Example:**
```typescript
buttonContainerSelector: [
  '.primary-container',   // Try first
  '.fallback-container',  // Then this
  '.generic-container'    // Final fallback
].join(', ')
```

### Pitfall 3: Content Doesn't Insert

**Symptoms:** Icon works, but prompt doesn't appear in input

**Solut