Créer une extension Chrome avec IA : Résumé de page web

tl;dr: Créez une extension Chrome qui résume n'importe quelle page web en 1 clic : setup Manifest V3, injection content script, extraction contenu, appel API LLM (GPT-4o-mini), popup interface, stockage historique. Publiez sur Chrome Web Store. Code complet fourni, temps: 2-3h.

💡 Ce tutoriel vous guide pour créer une extension Chrome professionnelle qui résume des articles web avec IA. En 2-3h, vous aurez une extension fonctionnelle avec UI élégante, appels API LLM, et stockage local. Publiable sur Chrome Web Store.

Les extensions Chrome permettent d’ajouter des fonctionnalités personnalisées au navigateur. Avec l’IA, on peut créer des extensions surpuissantes :

  • 📝 Résumer des articles en 1 clic
  • 🌐 Traduire instantanément
  • ✍️ Améliorer le texte sélectionné
  • 💡 Extraire insights d’une page
  • 🤖 Assistant de recherche contextuel

Dans ce tutoriel, nous allons créer “QuickSummarize” : une extension qui résume n’importe quelle page web avec un LLM.

Fonctionnalités :

  • ✅ Bouton dans toolbar Chrome
  • ✅ Clic → Extraction contenu page
  • ✅ Appel API (GPT-4o-mini, Claude, local)
  • ✅ Affichage résumé dans popup
  • ✅ Historique des résumés (storage local)
  • ✅ Copie en un clic
  • ✅ Support multilingue

Stack technique :

  • Manifest V3 (standard 2025)
  • JavaScript (vanilla, pas de framework)
  • Content Scripts (injection dans pages)
  • Service Worker (background logic)
  • Chrome Storage API (persistance)
  • OpenAI API (ou alternatives)

Temps estimé : 2-3 heures Coût : ~$0.01-0.05 par résumé (OpenAI) Niveau : Intermédiaire (JavaScript requis)

Tutoriel pratique étape par étape : le développement d’une extension Chrome avec IA avec exemples de code

Architecture de l’extension

Vue d’ensemble

┌────────────────────────────────────────────────────┐
│          CHROME EXTENSION : QuickSummarize         │
├────────────────────────────────────────────────────┤
│                                                    │
│  1. INTERFACE UTILISATEUR                         │
│  ┌──────────────┐         ┌─────────────┐        │
│  │   Toolbar    │  Click  │   Popup     │        │
│  │   Icon  🔵   │ ──────> │   popup.html│        │
│  └──────────────┘         └──────┬──────┘        │
│                                   │                │
│  2. CONTENT SCRIPT (injection)                    │
│  ┌─────────────────────────────────────┐          │
│  │  content.js                         │          │
│  │  - Injecté dans page web            │          │
│  │  - Extrait contenu (article, body)  │          │
│  │  - Communique avec background       │          │
│  └────────────┬────────────────────────┘          │
│               │ (message)                          │
│               v                                    │
│  3. SERVICE WORKER (background)                   │
│  ┌─────────────────────────────────────┐          │
│  │  background.js                      │          │
│  │  - Reçoit contenu extrait           │          │
│  │  - Appelle API LLM                  │          │
│  │  - Retourne résumé                  │          │
│  │  - Gère storage                     │          │
│  └────────────┬────────────────────────┘          │
│               │                                    │
│               v                                    │
│  4. API LLM (externe)                             │
│  ┌─────────────────────────────────────┐          │
│  │  OpenAI / Anthropic / Local         │          │
│  │  POST /chat/completions             │          │
│  │  { "messages": [...] }              │          │
│  └─────────────────────────────────────┘          │
│                                                    │
│  5. STORAGE (persistance)                         │
│  ┌─────────────────────────────────────┐          │
│  │  chrome.storage.local                │          │
│  │  - API key                           │          │
│  │  - Historique résumés                │          │
│  │  - Préférences utilisateur           │          │
│  └─────────────────────────────────────┘          │
│                                                    │
└────────────────────────────────────────────────────┘

Fichiers de l’extension

quick-summarize/
├── manifest.json          # Configuration extension
├── icons/
   ├── icon16.png         # 16x16 (toolbar)
   ├── icon48.png         # 48x48 (extensions page)
   └── icon128.png        # 128x128 (Web Store)
├── popup/
   ├── popup.html         # Interface popup
   ├── popup.css          # Styles
   └── popup.js           # Logique UI
├── content/
   └── content.js         # Script injecté dans pages
├── background/
   └── background.js      # Service worker
├── options/
   ├── options.html       # Page settings
   └── options.js         # Config API key
└── README.md

Setup et manifest

Créer la structure

mkdir quick-summarize
cd quick-summarize

# Créer dossiers
mkdir -p icons popup content background options

# Fichiers principaux
touch manifest.json
touch popup/popup.html popup/popup.css popup/popup.js
touch content/content.js
touch background/background.js
touch options/options.html options/options.js

Manifest V3 (manifest.json)

{
  "manifest_version": 3,
  "name": "QuickSummarize - AI Web Page Summarizer",
  "version": "1.0.0",
  "description": "Summarize any web page with AI in one click",

  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },

  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png"
    },
    "default_title": "Summarize this page"
  },

  "permissions": [
    "storage",
    "activeTab",
    "scripting"
  ],

  "host_permissions": [
    "https://api.openai.com/*",
    "https://api.anthropic.com/*"
  ],

  "background": {
    "service_worker": "background/background.js"
  },

  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content/content.js"],
      "run_at": "document_idle"
    }
  ],

  "options_page": "options/options.html"
}

Explications :

  • manifest_version: 3 : Standard actuel (V2 deprecated 2025)
  • permissions : storage (local), activeTab (accès page courante), scripting (injection)
  • host_permissions : Autoriser calls API externes
  • service_worker : Remplace background pages (V2)
  • content_scripts : Auto-injecté dans toutes pages

Icônes

Créez 3 icônes ou utilisez un générateur (ex: favicon.io) :

  • icon16.png : 16×16px (toolbar)
  • icon48.png : 48×48px (page extensions)
  • icon128.png : 128×128px (Chrome Web Store)

Ou placeholder simple :

# Créer icônes simples (requiert ImageMagick)
convert -size 16x16 xc:blue icons/icon16.png
convert -size 48x48 xc:blue icons/icon48.png
convert -size 128x128 xc:blue icons/icon128.png

Interface popup

HTML (popup/popup.html)

<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>QuickSummarize</title>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div class="container">
    <!-- Header -->
    <header>
      <h1>🤖 QuickSummarize</h1>
      <button id="settings-btn" title="Settings">⚙️</button>
    </header>

    <!-- Status -->
    <div id="status" class="status hidden"></div>

    <!-- Actions -->
    <div class="actions">
      <button id="summarize-btn" class="btn-primary">
        📝 Summarize This Page
      </button>
    </div>

    <!-- Summary Display -->
    <div id="summary-container" class="summary-container hidden">
      <h3>Summary:</h3>
      <div id="summary-content" class="summary-content"></div>
      <div class="summary-actions">
        <button id="copy-btn" class="btn-secondary">📋 Copy</button>
        <button id="clear-btn" class="btn-secondary">🗑️ Clear</button>
      </div>
    </div>

    <!-- History -->
    <div id="history-container" class="history-container hidden">
      <h3>Recent Summaries:</h3>
      <ul id="history-list"></ul>
    </div>

    <!-- Loading -->
    <div id="loading" class="loading hidden">
      <div class="spinner"></div>
      <p>Summarizing...</p>
    </div>
  </div>

  <script src="popup.js"></script>
</body>
</html>

CSS (popup/popup.css)

/* popup.css - Styles pour popup extension */

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  width: 400px;
  min-height: 300px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: #333;
}

.container {
  background: white;
  padding: 20px;
  border-radius: 12px;
  margin: 10px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}

/* Header */
header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  padding-bottom: 15px;
  border-bottom: 2px solid #f0f0f0;
}

h1 {
  font-size: 1.5rem;
  color: #667eea;
}

#settings-btn {
  background: none;
  border: none;
  font-size: 1.2rem;
  cursor: pointer;
  padding: 5px;
  opacity: 0.6;
  transition: opacity 0.2s;
}

#settings-btn:hover {
  opacity: 1;
}

/* Status */
.status {
  padding: 10px;
  border-radius: 6px;
  margin-bottom: 15px;
  font-size: 0.9rem;
}

.status.success {
  background: #d4edda;
  color: #155724;
}

.status.error {
  background: #f8d7da;
  color: #721c24;
}

/* Actions */
.actions {
  margin-bottom: 20px;
}

.btn-primary {
  width: 100%;
  padding: 12px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.2s;
}

.btn-primary:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}

.btn-primary:active {
  transform: translateY(0);
}

.btn-primary:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

/* Summary */
.summary-container {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 2px solid #f0f0f0;
}

.summary-container h3 {
  font-size: 1.1rem;
  margin-bottom: 10px;
  color: #667eea;
}

.summary-content {
  background: #f8f9fa;
  padding: 15px;
  border-radius: 8px;
  line-height: 1.6;
  max-height: 300px;
  overflow-y: auto;
  margin-bottom: 15px;
  font-size: 0.95rem;
}

.summary-content::-webkit-scrollbar {
  width: 6px;
}

.summary-content::-webkit-scrollbar-thumb {
  background: #667eea;
  border-radius: 3px;
}

.summary-actions {
  display: flex;
  gap: 10px;
}

.btn-secondary {
  flex: 1;
  padding: 8px;
  background: #f0f0f0;
  border: none;
  border-radius: 6px;
  font-size: 0.9rem;
  cursor: pointer;
  transition: background 0.2s;
}

.btn-secondary:hover {
  background: #e0e0e0;
}

/* Loading */
.loading {
  text-align: center;
  padding: 30px;
}

.spinner {
  width: 40px;
  height: 40px;
  margin: 0 auto 15px;
  border: 4px solid #f0f0f0;
  border-top: 4px solid #667eea;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* History */
.history-container {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 2px solid #f0f0f0;
}

.history-container h3 {
  font-size: 1.1rem;
  margin-bottom: 10px;
  color: #667eea;
}

#history-list {
  list-style: none;
}

#history-list li {
  padding: 10px;
  background: #f8f9fa;
  border-radius: 6px;
  margin-bottom: 8px;
  font-size: 0.85rem;
  cursor: pointer;
  transition: background 0.2s;
}

#history-list li:hover {
  background: #e9ecef;
}

/* Utility */
.hidden {
  display: none !important;
}

JavaScript (popup/popup.js)

/**
 * popup.js - Logique de l'interface popup
 */

// Elements
const summarizeBtn = document.getElementById('summarize-btn');
const summaryContainer = document.getElementById('summary-container');
const summaryContent = document.getElementById('summary-content');
const loadingDiv = document.getElementById('loading');
const statusDiv = document.getElementById('status');
const copyBtn = document.getElementById('copy-btn');
const clearBtn = document.getElementById('clear-btn');
const settingsBtn = document.getElementById('settings-btn');

// État
let currentSummary = '';

/**
 * Afficher message de status
 */
function showStatus(message, type = 'success') {
  statusDiv.textContent = message;
  statusDiv.className = `status ${type}`;
  statusDiv.classList.remove('hidden');

  setTimeout(() => {
    statusDiv.classList.add('hidden');
  }, 3000);
}

/**
 * Bouton Summarize
 */
summarizeBtn.addEventListener('click', async () => {
  try {
    // Vérifier API key configurée
    const { apiKey } = await chrome.storage.local.get(['apiKey']);
    if (!apiKey) {
      showStatus('⚠️ Please configure API key in settings', 'error');
      return;
    }

    // Désactiver bouton, afficher loading
    summarizeBtn.disabled = true;
    loadingDiv.classList.remove('hidden');
    summaryContainer.classList.add('hidden');

    // Obtenir tab active
    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

    // Envoyer message au content script pour extraire contenu
    const response = await chrome.tabs.sendMessage(tab.id, {
      action: 'extractContent'
    });

    if (!response || !response.content) {
      throw new Error('Failed to extract page content');
    }

    // Envoyer au background pour résumer
    const summaryResponse = await chrome.runtime.sendMessage({
      action: 'summarize',
      content: response.content,
      title: response.title,
      url: tab.url
    });

    if (summaryResponse.error) {
      throw new Error(summaryResponse.error);
    }

    // Afficher résumé
    currentSummary = summaryResponse.summary;
    summaryContent.textContent = currentSummary;
    summaryContainer.classList.remove('hidden');
    showStatus('✅ Summary generated!');

  } catch (error) {
    showStatus(`❌ Error: ${error.message}`, 'error');
    console.error('Summary error:', error);
  } finally {
    summarizeBtn.disabled = false;
    loadingDiv.classList.add('hidden');
  }
});

/**
 * Copier résumé
 */
copyBtn.addEventListener('click', async () => {
  try {
    await navigator.clipboard.writeText(currentSummary);
    showStatus('📋 Copied to clipboard!');
  } catch (error) {
    showStatus('❌ Failed to copy', 'error');
  }
});

/**
 * Clear résumé
 */
clearBtn.addEventListener('click', () => {
  summaryContainer.classList.add('hidden');
  currentSummary = '';
});

/**
 * Ouvrir settings
 */
settingsBtn.addEventListener('click', () => {
  chrome.runtime.openOptionsPage();
});

/**
 * Charger historique au démarrage
 */
async function loadHistory() {
  const { history = [] } = await chrome.storage.local.get(['history']);

  if (history.length > 0) {
    const historyContainer = document.getElementById('history-container');
    const historyList = document.getElementById('history-list');

    historyList.innerHTML = '';

    // Afficher 5 derniers
    history.slice(-5).reverse().forEach(item => {
      const li = document.createElement('li');
      li.textContent = `${item.title} - ${new Date(item.timestamp).toLocaleDateString()}`;
      li.title = item.summary;
      li.addEventListener('click', () => {
        currentSummary = item.summary;
        summaryContent.textContent = currentSummary;
        summaryContainer.classList.remove('hidden');
      });
      historyList.appendChild(li);
    });

    historyContainer.classList.remove('hidden');
  }
}

// Init
loadHistory();

Content script (extraction)

Content script (content/content.js)

/**
 * content.js - Script injecté dans pages web
 * Extrait contenu de la page
 */

/**
 * Extraire contenu principal de la page
 */
function extractPageContent() {
  // Titre
  const title = document.title;

  // Essayer d'extraire article principal
  let content = '';

  // Stratégie 1 : Chercher balises <article>
  const articles = document.querySelectorAll('article');
  if (articles.length > 0) {
    content = Array.from(articles)
      .map(article => article.innerText)
      .join('\n\n');
  }

  // Stratégie 2 : Chercher main content
  if (!content) {
    const main = document.querySelector('main') ||
                 document.querySelector('[role="main"]') ||
                 document.querySelector('#main') ||
                 document.querySelector('.main-content');

    if (main) {
      content = main.innerText;
    }
  }

  // Stratégie 3 : Fallback sur body (filtrer nav, footer, etc.)
  if (!content) {
    const body = document.body.cloneNode(true);

    // Supprimer éléments non pertinents
    const selectorsToRemove = [
      'nav', 'header', 'footer', 'aside',
      '.nav', '.menu', '.sidebar', '.ads',
      'script', 'style', 'noscript'
    ];

    selectorsToRemove.forEach(selector => {
      body.querySelectorAll(selector).forEach(el => el.remove());
    });

    content = body.innerText;
  }

  // Nettoyer
  content = content
    .replace(/\s+/g, ' ')              // Multiple spaces → 1 space
    .replace(/\n{3,}/g, '\n\n')        // Multiple newlines → 2
    .trim();

  // Limiter taille (premiers 10,000 chars)
  if (content.length > 10000) {
    content = content.substring(0, 10000) + '...';
  }

  return {
    title,
    content,
    url: window.location.href
  };
}

/**
 * Écouter messages depuis popup
 */
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'extractContent') {
    try {
      const data = extractPageContent();
      sendResponse(data);
    } catch (error) {
      sendResponse({ error: error.message });
    }
  }
  return true;  // Async response
});

Service worker (background)

Background script (background/background.js)

/**
 * background.js - Service worker background
 * Gère appels API et storage
 */

// Configuration API
const API_ENDPOINTS = {
  openai: 'https://api.openai.com/v1/chat/completions',
  anthropic: 'https://api.anthropic.com/v1/messages'
};

/**
 * Appeler OpenAI API
 */
async function callOpenAI(apiKey, content, title) {
  const prompt = `Summarize this web page in 3-5 concise bullet points.

Title: ${title}

Content:
${content}

Provide a clear, informative summary in French.`;

  const response = await fetch(API_ENDPOINTS.openai, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${apiKey}`
    },
    body: JSON.stringify({
      model: 'gpt-4o-mini',
      messages: [
        {
          role: 'user',
          content: prompt
        }
      ],
      max_tokens: 300,
      temperature: 0.5
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error?.message || 'API call failed');
  }

  const data = await response.json();
  return data.choices[0].message.content.trim();
}

/**
 * Appeler Anthropic API (Claude)
 */
async function callAnthropic(apiKey, content, title) {
  const prompt = `Summarize this web page in 3-5 concise bullet points.

Title: ${title}

Content:
${content}

Provide a clear, informative summary in French.`;

  const response = await fetch(API_ENDPOINTS.anthropic, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': apiKey,
      'anthropic-version': '2023-06-01'
    },
    body: JSON.stringify({
      model: 'claude-3-haiku-20240307',
      max_tokens: 300,
      messages: [
        {
          role: 'user',
          content: prompt
        }
      ]
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error?.message || 'API call failed');
  }

  const data = await response.json();
  return data.content[0].text.trim();
}

/**
 * Sauvegarder dans historique
 */
async function saveToHistory(title, url, summary) {
  const { history = [] } = await chrome.storage.local.get(['history']);

  history.push({
    title,
    url,
    summary,
    timestamp: Date.now()
  });

  // Garder seulement 50 derniers
  if (history.length > 50) {
    history.shift();
  }

  await chrome.storage.local.set({ history });
}

/**
 * Message listener
 */
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'summarize') {
    (async () => {
      try {
        // Récupérer config
        const { apiKey, apiProvider = 'openai' } = await chrome.storage.local.get(['apiKey', 'apiProvider']);

        if (!apiKey) {
          throw new Error('API key not configured');
        }

        // Appeler API selon provider
        let summary;
        if (apiProvider === 'anthropic') {
          summary = await callAnthropic(apiKey, request.content, request.title);
        } else {
          summary = await callOpenAI(apiKey, request.content, request.title);
        }

        // Sauvegarder historique
        await saveToHistory(request.title, request.url, summary);

        sendResponse({ summary });

      } catch (error) {
        sendResponse({ error: error.message });
      }
    })();

    return true;  // Async response
  }
});

Page options (settings)

Options HTML (options/options.html)

<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>QuickSummarize Settings</title>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      max-width: 600px;
      margin: 50px auto;
      padding: 30px;
      background: #f5f5f5;
    }

    .container {
      background: white;
      padding: 30px;
      border-radius: 12px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    }

    h1 {
      color: #667eea;
      margin-bottom: 30px;
    }

    .form-group {
      margin-bottom: 25px;
    }

    label {
      display: block;
      font-weight: 600;
      margin-bottom: 8px;
      color: #333;
    }

    input[type="text"],
    select {
      width: 100%;
      padding: 12px;
      border: 2px solid #e0e0e0;
      border-radius: 8px;
      font-size: 1rem;
      transition: border-color 0.2s;
    }

    input[type="text"]:focus,
    select:focus {
      outline: none;
      border-color: #667eea;
    }

    .help-text {
      font-size: 0.85rem;
      color: #666;
      margin-top: 5px;
    }

    .help-text a {
      color: #667eea;
      text-decoration: none;
    }

    .help-text a:hover {
      text-decoration: underline;
    }

    button {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      border: none;
      padding: 12px 30px;
      border-radius: 8px;
      font-size: 1rem;
      font-weight: 600;
      cursor: pointer;
      transition: transform 0.2s;
    }

    button:hover {
      transform: translateY(-2px);
    }

    #status {
      margin-top: 20px;
      padding: 12px;
      border-radius: 8px;
      display: none;
    }

    #status.success {
      background: #d4edda;
      color: #155724;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>⚙️ QuickSummarize Settings</h1>

    <form id="settings-form">
      <div class="form-group">
        <label for="api-provider">API Provider:</label>
        <select id="api-provider" name="apiProvider">
          <option value="openai">OpenAI (GPT-4o-mini)</option>
          <option value="anthropic">Anthropic (Claude Haiku)</option>
        </select>
      </div>

      <div class="form-group">
        <label for="api-key">API Key:</label>
        <input type="text" id="api-key" name="apiKey" placeholder="sk-proj-..." required>
        <p class="help-text">
          Get your API key:
          <a href="https://platform.openai.com/api-keys" target="_blank">OpenAI</a> |
          <a href="https://console.anthropic.com/settings/keys" target="_blank">Anthropic</a>
        </p>
      </div>

      <button type="submit">💾 Save Settings</button>
    </form>

    <div id="status"></div>
  </div>

  <script src="options.js"></script>
</body>
</html>

Options JS (options/options.js)

/**
 * options.js - Configuration settings
 */

const form = document.getElementById('settings-form');
const statusDiv = document.getElementById('status');

// Charger settings existants
async function loadSettings() {
  const { apiKey, apiProvider = 'openai' } = await chrome.storage.local.get(['apiKey', 'apiProvider']);

  if (apiKey) {
    document.getElementById('api-key').value = apiKey;
  }
  document.getElementById('api-provider').value = apiProvider;
}

// Sauvegarder settings
form.addEventListener('submit', async (e) => {
  e.preventDefault();

  const apiKey = document.getElementById('api-key').value;
  const apiProvider = document.getElementById('api-provider').value;

  await chrome.storage.local.set({ apiKey, apiProvider });

  // Show success
  statusDiv.textContent = '✅ Settings saved successfully!';
  statusDiv.className = 'success';
  statusDiv.style.display = 'block';

  setTimeout(() => {
    statusDiv.style.display = 'none';
  }, 3000);
});

// Init
loadSettings();

Test et debug

Charger l’extension en dev

  1. Ouvrir Chrome : chrome://extensions/
  2. Activer Mode Développeur (toggle en haut à droite)
  3. Cliquer “Load unpacked”
  4. Sélectionner dossier quick-summarize/

✅ Extension apparaît dans toolbar

Configurer API Key

  1. Clic droit sur icône extension → Options
  2. Entrer votre API key OpenAI
  3. Save

3 Tester

  1. Ouvrir article : ex. Wikipedia, blog
  2. Cliquer icône extension
  3. Cliquer “Summarize This Page”
  4. Attendre 2-5s
  5. ✅ Résumé s’affiche

Debug

Console background :

chrome://extensions/ → QuickSummarize → "service worker" (lien)

Console popup :

Clic droit sur popup → Inspect

Console content script :

F12 sur page web → Console

Publication Chrome Web Store (optionnel)

Préparer pour publication

Créer README.md :

# QuickSummarize - AI Web Page Summarizer

Summarize any web page with AI in one click.

## Features {#features}
- One-click summarization
- Support for OpenAI & Anthropic
- Local history
- Copy to clipboard

## Setup {#setup}
1. Install extension
2. Configure API key in settings
3. Click extension icon on any page

## Privacy {#privacy}
- API key stored locally
- No data collection

Screenshots :

  • Créer 1-5 screenshots (1280×800px)
  • Montrer interface, résumé, settings

Icônes haute résolution :

  • 128×128px minimum (déjà fait)

Publication

  1. Créer compte développeur : Chrome Web Store Developer ($5 one-time)
  2. Nouveau élément → Upload ZIP
  3. Remplir :
    • Titre : “QuickSummarize - AI Summarizer”
    • Description (détaillée)
    • Screenshots
    • Catégorie : Productivity
    • Langue : Français + English
  4. Review : 1-3 jours
  5. Publiée !

Extensions et améliorations

Fonctionnalités avancées

1. Support Multilingue

// Détection langue automatique
const language = document.documentElement.lang || 'en';
prompt = `Summarize in ${language}...`;

2. Export PDF

// jsPDF
import { jsPDF } from 'jspdf';
const doc = new jsPDF();
doc.text(summary, 10, 10);
doc.save('summary.pdf');

3. Sélection Texte

// Résumer texte sélectionné
document.addEventListener('mouseup', () => {
  const selected = window.getSelection().toString();
  if (selected.length > 100) {
    // Afficher bouton "Summarize selection"
  }
});

4. Intégration RAG

// Sauvegarder résumés dans base vectorielle
await saveToVectorDB(summary, url);
// Recherche sémantique dans historique

Ressources complémentaires

Articles liés :

Documentation officielle :


Questions fréquentes

Q: Combien coûte l’utilisation ? R: Avec GPT-4o-mini :

  • ~1500 tokens par résumé
  • $0.15/1M input tokens
  • $0.60/1M output tokens
  • $0.001-0.002 par résumé

Q: Puis-je utiliser un modèle local ? R: Oui ! Déployez Ollama localement :

const response = await fetch('http://localhost:11434/api/generate', {
  method: 'POST',
  body: JSON.stringify({
    model: 'llama3',
    prompt: `Summarize: ${content}`
  })
});

Q: Latence attendue ? R:

  • Extraction : <100ms
  • API call : 1-3s
  • Total : 1.5-3.5s

Q: Fonctionne sur tous sites ? R: Oui, sauf :

  • Sites avec protection anti-scraping
  • Contenu dynamique heavy JS (SPAs)
  • Pages nécessitant auth

Q: Privacy ? R:

  • API key stockée localement (chrome.storage)
  • Contenu envoyé à API LLM (OpenAI/Anthropic)
  • Pas de tracking, pas d’analytics
  • Open source = auditable

💡 Félicitations ! Vous avez créé une extension Chrome professionnelle avec IA. Explorez d’autres use cases : traduction, amélioration texte, extraction données structurées…

Série tutoriels :