Créez votre première extension Chrome alimentée par l'IA avec Manifest V3 et OpenAI

Les extensions Chrome comptent parmi les projets les plus impactants qu'un développeur web puisse réaliser. Elles vivent directement dans le navigateur — là où les utilisateurs passent la majeure partie de leur journée — et quand on y intègre de l'IA, elles deviennent de véritables outils surpuissants.
Dans ce tutoriel, nous allons construire SummarizeAI, une extension Chrome capable de :
- Résumer n'importe quelle page web en un clic
- Sélectionner du texte et obtenir une explication générée par l'IA
- Sauvegarder les résumés localement pour consultation ultérieure
- Fonctionner entièrement avec Manifest V3 (le standard actuel)
Pourquoi Manifest V3 ? Google a définitivement abandonné Manifest V2 depuis 2025. Toutes les nouvelles extensions doivent utiliser Manifest V3, qui remplace les pages d'arrière-plan par des Service Workers et impose des politiques de sécurité plus strictes.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Google Chrome (version 120 ou plus récente)
- Node.js (v18 minimum) — pour le bundling uniquement, pas nécessaire à l'exécution
- Une clé API OpenAI — obtenez-la ici
- Des connaissances de base en JavaScript, HTML et CSS
- Un éditeur de code (VS Code recommandé)
Architecture du projet
Voici la structure que nous allons construire :
summarize-ai/
├── manifest.json # Manifeste de l'extension (V3)
├── background.js # Service Worker
├── content.js # Script de contenu (injecté dans les pages)
├── popup/
│ ├── popup.html # Interface du popup
│ ├── popup.css # Styles du popup
│ └── popup.js # Logique du popup
├── options/
│ ├── options.html # Page de paramètres
│ └── options.js # Logique des paramètres
├── lib/
│ └── ai-client.js # Wrapper de l'API OpenAI
├── icons/
│ ├── icon-16.png
│ ├── icon-48.png
│ └── icon-128.png
└── README.md
Étape 1 : Créer le manifeste
Le manifeste est le cœur de toute extension Chrome. Créez manifest.json :
{
"manifest_version": 3,
"name": "SummarizeAI",
"version": "1.0.0",
"description": "Résumeur de pages et explicateur de texte propulsé par l'IA",
"permissions": [
"activeTab",
"storage",
"contextMenus",
"scripting"
],
"host_permissions": [
"https://api.openai.com/*"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"css": []
}
],
"options_page": "options/options.html",
"icons": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
}Différences clés de Manifest V3
| Fonctionnalité | Manifest V2 | Manifest V3 |
|---|---|---|
| Arrière-plan | Page persistante | Service Worker |
| Requêtes réseau | webRequest bloquant | declarativeNetRequest |
| Code distant | Autorisé | Interdit |
| Politique de sécurité | Souple | Stricte par défaut |
Avec Manifest V3, il est impossible de charger des scripts distants. Tout le code doit être inclus dans l'extension. Cela signifie : pas de liens CDN — tout est livré en local.
Étape 2 : Construire le client IA
Créez lib/ai-client.js — ce wrapper encapsule l'API OpenAI :
// lib/ai-client.js
const DEFAULT_MODEL = "gpt-4o-mini";
class AIClient {
constructor(apiKey, model = DEFAULT_MODEL) {
this.apiKey = apiKey;
this.model = model;
this.baseUrl = "https://api.openai.com/v1/chat/completions";
}
async complete(systemPrompt, userMessage, options = {}) {
const { maxTokens = 1000, temperature = 0.3 } = options;
const response = await fetch(this.baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
model: this.model,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userMessage },
],
max_tokens: maxTokens,
temperature,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(
`Erreur API OpenAI : ${error.error?.message || response.statusText}`
);
}
const data = await response.json();
return data.choices[0].message.content;
}
async summarize(text) {
return this.complete(
"Tu es un résumeur concis et efficace. Fournis un résumé clair et structuré du texte donné. Utilise des puces pour les points clés. Ne dépasse pas 200 mots.",
`Résume le texte suivant :\n\n${text}`
);
}
async explain(text) {
return this.complete(
"Tu es un pédagogue clair et accessible. Explique le texte donné de manière simple. S'il est technique, vulgarise-le pour un public général.",
`Explique ceci :\n\n${text}`,
{ maxTokens: 500 }
);
}
}
if (typeof globalThis !== "undefined") {
globalThis.AIClient = AIClient;
}Nous utilisons gpt-4o-mini par défaut car il est rapide, économique et largement suffisant pour la synthèse. L'utilisateur peut passer à gpt-4o dans les paramètres pour une qualité supérieure.
Étape 3 : Configurer le Service Worker (script d'arrière-plan)
Le Service Worker gère les menus contextuels, le passage de messages et l'orchestration :
// background.js
importScripts("lib/ai-client.js");
// Créer les menus contextuels à l'installation
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: "explain-selection",
title: "Expliquer avec l'IA",
contexts: ["selection"],
});
chrome.contextMenus.create({
id: "summarize-page",
title: "Résumer cette page",
contexts: ["page"],
});
chrome.storage.sync.get(["apiKey", "model"], (result) => {
if (!result.model) {
chrome.storage.sync.set({ model: "gpt-4o-mini" });
}
});
});
// Gérer les clics sur le menu contextuel
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
const { apiKey, model } = await chrome.storage.sync.get([
"apiKey",
"model",
]);
if (!apiKey) {
chrome.notifications.create({
type: "basic",
iconUrl: "icons/icon-48.png",
title: "SummarizeAI",
message:
"Veuillez configurer votre clé API OpenAI dans les paramètres de l'extension.",
});
return;
}
const client = new AIClient(apiKey, model);
if (info.menuItemId === "explain-selection" && info.selectionText) {
try {
const explanation = await client.explain(info.selectionText);
chrome.tabs.sendMessage(tab.id, {
type: "SHOW_RESULT",
title: "Explication IA",
content: explanation,
});
} catch (error) {
chrome.tabs.sendMessage(tab.id, {
type: "SHOW_ERROR",
message: error.message,
});
}
}
if (info.menuItemId === "summarize-page") {
chrome.scripting.executeScript(
{
target: { tabId: tab.id },
func: () => document.body.innerText,
},
async (results) => {
if (results && results[0]) {
const pageText = results[0].result.substring(0, 10000);
try {
const summary = await client.summarize(pageText);
chrome.tabs.sendMessage(tab.id, {
type: "SHOW_RESULT",
title: "Résumé de la page",
content: summary,
});
} catch (error) {
chrome.tabs.sendMessage(tab.id, {
type: "SHOW_ERROR",
message: error.message,
});
}
}
}
);
}
});
// Gérer les messages du popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "SUMMARIZE_TAB") {
handleSummarizeTab(message.tabId).then(sendResponse);
return true;
}
if (message.type === "GET_HISTORY") {
chrome.storage.local.get(["history"], (result) => {
sendResponse(result.history || []);
});
return true;
}
});
async function handleSummarizeTab(tabId) {
const { apiKey, model } = await chrome.storage.sync.get([
"apiKey",
"model",
]);
if (!apiKey) return { error: "Clé API non configurée" };
const results = await chrome.scripting.executeScript({
target: { tabId },
func: () => ({
text: document.body.innerText,
title: document.title,
url: window.location.href,
}),
});
if (!results || !results[0])
return { error: "Impossible de lire la page" };
const { text, title, url } = results[0].result;
const client = new AIClient(apiKey, model);
try {
const summary = await client.summarize(text.substring(0, 10000));
const { history = [] } = await chrome.storage.local.get(["history"]);
history.unshift({
title,
url,
summary,
timestamp: Date.now(),
});
await chrome.storage.local.set({
history: history.slice(0, 50),
});
return { summary, title };
} catch (error) {
return { error: error.message };
}
}Les Service Workers de Manifest V3 ne sont pas persistants. Ils peuvent être arrêtés après 30 secondes d'inactivité. Ne stockez jamais d'état dans des variables — utilisez systématiquement chrome.storage.
Étape 4 : Créer le script de contenu
Le script de contenu injecte un panneau flottant de résultats dans les pages web :
// content.js
(function () {
let resultPanel = null;
function createPanel() {
if (resultPanel) return resultPanel;
resultPanel = document.createElement("div");
resultPanel.id = "summarize-ai-panel";
resultPanel.innerHTML = `
<div class="sai-header">
<span class="sai-title">SummarizeAI</span>
<button class="sai-close">×</button>
</div>
<div class="sai-body">
<div class="sai-loading" style="display:none">
<div class="sai-spinner"></div>
<span>Analyse en cours...</span>
</div>
<div class="sai-content"></div>
</div>
`;
const style = document.createElement("style");
style.textContent = `
#summarize-ai-panel {
position: fixed;
bottom: 20px;
right: 20px;
width: 380px;
max-height: 500px;
background: #1a1a2e;
color: #eee;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
z-index: 999999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
overflow: hidden;
animation: sai-slide-in 0.3s ease-out;
}
@keyframes sai-slide-in {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.sai-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #16213e;
border-bottom: 1px solid #0f3460;
}
.sai-title { font-weight: 600; color: #e94560; }
.sai-close {
background: none; border: none; color: #888;
font-size: 20px; cursor: pointer; padding: 0 4px;
}
.sai-close:hover { color: #fff; }
.sai-body {
padding: 16px; max-height: 400px;
overflow-y: auto; line-height: 1.6;
}
.sai-content ul { padding-left: 20px; }
.sai-content li { margin-bottom: 6px; }
.sai-spinner {
width: 20px; height: 20px;
border: 2px solid #333; border-top: 2px solid #e94560;
border-radius: 50%;
animation: sai-spin 0.8s linear infinite;
display: inline-block; margin-right: 8px;
vertical-align: middle;
}
@keyframes sai-spin { to { transform: rotate(360deg); } }
.sai-loading { display: flex; align-items: center; color: #888; }
.sai-error {
color: #e94560; padding: 8px;
background: rgba(233,69,96,0.1); border-radius: 6px;
}
`;
document.head.appendChild(style);
document.body.appendChild(resultPanel);
resultPanel.querySelector(".sai-close").addEventListener("click", () => {
resultPanel.style.display = "none";
});
return resultPanel;
}
function showLoading() {
const panel = createPanel();
panel.style.display = "block";
panel.querySelector(".sai-loading").style.display = "flex";
panel.querySelector(".sai-content").innerHTML = "";
}
function showResult(title, content) {
const panel = createPanel();
panel.style.display = "block";
panel.querySelector(".sai-loading").style.display = "none";
panel.querySelector(".sai-title").textContent = title;
const formatted = content
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
.replace(/^- (.*)/gm, "<li>$1</li>")
.replace(/(<li>.*<\/li>)/s, "<ul>$1</ul>")
.replace(/\n/g, "<br>");
panel.querySelector(".sai-content").innerHTML = formatted;
}
function showError(message) {
const panel = createPanel();
panel.style.display = "block";
panel.querySelector(".sai-loading").style.display = "none";
panel.querySelector(".sai-content").innerHTML = `
<div class="sai-error">⚠️ ${message}</div>
`;
}
chrome.runtime.onMessage.addListener((message) => {
if (message.type === "SHOW_RESULT") {
showResult(message.title, message.content);
}
if (message.type === "SHOW_ERROR") {
showError(message.message);
}
if (message.type === "SHOW_LOADING") {
showLoading();
}
});
})();Étape 5 : Construire l'interface du popup
Le popup est ce que voit l'utilisateur lorsqu'il clique sur l'icône de l'extension.
<!-- popup/popup.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="popup.css" />
</head>
<body>
<div class="container">
<header>
<h1>🤖 SummarizeAI</h1>
</header>
<div id="no-key-warning" class="warning" style="display: none">
<p>⚠️ Clé API non configurée.</p>
<a href="#" id="open-settings">Ouvrir les paramètres</a>
</div>
<div id="main-content">
<button id="summarize-btn" class="primary-btn">
✨ Résumer cette page
</button>
<div id="result" style="display: none">
<h3 id="result-title"></h3>
<div id="result-content"></div>
</div>
<div id="loading" style="display: none">
<div class="spinner"></div>
<span>Analyse de la page...</span>
</div>
<hr />
<h3>📚 Résumés récents</h3>
<div id="history-list"></div>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>/* popup/popup.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 400px;
max-height: 550px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0f0f23;
color: #eee;
}
.container {
padding: 16px;
}
header {
text-align: center;
margin-bottom: 16px;
}
header h1 {
font-size: 18px;
color: #e94560;
}
.primary-btn {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #e94560, #0f3460);
color: white;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: transform 0.1s;
}
.primary-btn:hover {
transform: scale(1.02);
}
.primary-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.warning {
background: rgba(233, 69, 96, 0.1);
border: 1px solid #e94560;
padding: 12px;
border-radius: 8px;
margin-bottom: 12px;
text-align: center;
}
.warning a {
color: #e94560;
}
#result {
margin-top: 16px;
padding: 12px;
background: #1a1a2e;
border-radius: 8px;
max-height: 250px;
overflow-y: auto;
line-height: 1.6;
}
#result h3 {
font-size: 14px;
color: #e94560;
margin-bottom: 8px;
}
#loading {
text-align: center;
padding: 20px;
color: #888;
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid #333;
border-top: 3px solid #e94560;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 8px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
hr {
border: none;
border-top: 1px solid #1a1a2e;
margin: 16px 0;
}
.history-item {
padding: 10px;
background: #1a1a2e;
border-radius: 6px;
margin-bottom: 8px;
cursor: pointer;
transition: background 0.2s;
}
.history-item:hover {
background: #16213e;
}
.history-item .title {
font-weight: 600;
font-size: 13px;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-item .date {
font-size: 11px;
color: #666;
}// popup/popup.js
document.addEventListener("DOMContentLoaded", async () => {
const summarizeBtn = document.getElementById("summarize-btn");
const resultDiv = document.getElementById("result");
const resultTitle = document.getElementById("result-title");
const resultContent = document.getElementById("result-content");
const loadingDiv = document.getElementById("loading");
const historyList = document.getElementById("history-list");
const noKeyWarning = document.getElementById("no-key-warning");
const { apiKey } = await chrome.storage.sync.get(["apiKey"]);
if (!apiKey) {
noKeyWarning.style.display = "block";
summarizeBtn.disabled = true;
}
document.getElementById("open-settings").addEventListener("click", (e) => {
e.preventDefault();
chrome.runtime.openOptionsPage();
});
summarizeBtn.addEventListener("click", async () => {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true,
});
summarizeBtn.disabled = true;
loadingDiv.style.display = "block";
resultDiv.style.display = "none";
chrome.runtime.sendMessage(
{ type: "SUMMARIZE_TAB", tabId: tab.id },
(response) => {
loadingDiv.style.display = "none";
summarizeBtn.disabled = false;
if (response.error) {
resultTitle.textContent = "Erreur";
resultContent.textContent = response.error;
} else {
resultTitle.textContent = response.title;
resultContent.innerHTML = formatContent(response.summary);
}
resultDiv.style.display = "block";
loadHistory();
}
);
});
async function loadHistory() {
chrome.runtime.sendMessage({ type: "GET_HISTORY" }, (history) => {
historyList.innerHTML = history
.slice(0, 10)
.map(
(item) => `
<div class="history-item" data-url="${item.url}">
<div class="title">${item.title}</div>
<div class="date">${new Date(item.timestamp).toLocaleDateString("fr-FR")}</div>
</div>
`
)
.join("");
historyList.querySelectorAll(".history-item").forEach((el) => {
el.addEventListener("click", () => {
chrome.tabs.create({ url: el.dataset.url });
});
});
});
}
loadHistory();
});
function formatContent(text) {
return text
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
.replace(/^- (.*)/gm, "<li>$1</li>")
.replace(/\n/g, "<br>");
}Étape 6 : Créer la page de paramètres
<!-- options/options.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<title>Paramètres SummarizeAI</title>
<style>
body {
font-family: -apple-system, sans-serif;
max-width: 500px;
margin: 40px auto;
padding: 20px;
background: #0f0f23;
color: #eee;
}
h1 { color: #e94560; }
label { display: block; margin-top: 16px; font-weight: 600; }
input, select {
width: 100%;
padding: 10px;
margin-top: 6px;
background: #1a1a2e;
border: 1px solid #333;
border-radius: 6px;
color: #eee;
font-size: 14px;
}
button {
margin-top: 20px;
padding: 10px 24px;
background: #e94560;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.saved {
color: #4ecca3;
margin-left: 12px;
display: none;
}
</style>
</head>
<body>
<h1>⚙️ Paramètres SummarizeAI</h1>
<label for="apiKey">Clé API OpenAI</label>
<input type="password" id="apiKey" placeholder="sk-..." />
<label for="model">Modèle</label>
<select id="model">
<option value="gpt-4o-mini">GPT-4o Mini (Rapide et économique)</option>
<option value="gpt-4o">GPT-4o (Qualité supérieure)</option>
<option value="gpt-4.1-nano">GPT-4.1 Nano (Ultra rapide)</option>
</select>
<button id="save">Enregistrer</button>
<span class="saved" id="saved-msg">✓ Enregistré !</span>
<script src="options.js"></script>
</body>
</html>// options/options.js
document.addEventListener("DOMContentLoaded", async () => {
const apiKeyInput = document.getElementById("apiKey");
const modelSelect = document.getElementById("model");
const saveBtn = document.getElementById("save");
const savedMsg = document.getElementById("saved-msg");
const { apiKey, model } = await chrome.storage.sync.get([
"apiKey",
"model",
]);
if (apiKey) apiKeyInput.value = apiKey;
if (model) modelSelect.value = model;
saveBtn.addEventListener("click", () => {
chrome.storage.sync.set(
{
apiKey: apiKeyInput.value.trim(),
model: modelSelect.value,
},
() => {
savedMsg.style.display = "inline";
setTimeout(() => (savedMsg.style.display = "none"), 2000);
}
);
});
});Étape 7 : Charger et tester l'extension
Chargeons l'extension dans Chrome :
- Ouvrez Chrome et accédez à
chrome://extensions/ - Activez le "Mode développeur" via le bouton en haut à droite
- Cliquez sur "Charger l'extension non empaquetée" et sélectionnez le dossier
summarize-ai/ - L'icône de l'extension devrait apparaître dans votre barre d'outils
Parcours de test
- Configurer la clé API : Cliquez sur l'extension → remarquez l'avertissement → cliquez "Ouvrir les paramètres" → entrez votre clé → enregistrez
- Résumer une page : Naviguez vers un article → cliquez sur l'extension → cliquez "Résumer cette page"
- Expliquer du texte : Sélectionnez du texte sur une page → clic droit → "Expliquer avec l'IA"
- Consulter l'historique : Rouvrez le popup — vos résumés récents apparaissent en bas
Pour le débogage, ouvrez chrome://extensions/, trouvez SummarizeAI et cliquez sur "Service Worker" pour inspecter le script d'arrière-plan. Faites un clic droit sur le popup et choisissez "Inspecter" pour le déboguer.
Étape 8 : Ajouter des raccourcis clavier
Rendez l'extension plus rapide avec des raccourcis. Ajoutez dans manifest.json :
{
"commands": {
"summarize-page": {
"suggested_key": {
"default": "Alt+S",
"mac": "Alt+S"
},
"description": "Résumer la page active"
},
"_execute_action": {
"suggested_key": {
"default": "Alt+A",
"mac": "Alt+A"
},
"description": "Ouvrir le popup SummarizeAI"
}
}
}Gérez la commande dans background.js :
chrome.commands.onCommand.addListener(async (command) => {
if (command === "summarize-page") {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true,
});
const result = await handleSummarizeTab(tab.id);
if (result.summary) {
chrome.tabs.sendMessage(tab.id, {
type: "SHOW_RESULT",
title: "Résumé de la page",
content: result.summary,
});
}
}
});Étape 9 : Gérer les cas limites
Une extension réelle doit gérer les erreurs avec élégance :
// Ajouter dans background.js
// Limitation du débit
const rateLimiter = {
lastCall: 0,
minInterval: 2000,
canProceed() {
const now = Date.now();
if (now - this.lastCall < this.minInterval) {
return false;
}
this.lastCall = now;
return true;
},
};
// Encapsuler les appels API
async function safeApiCall(fn) {
if (!rateLimiter.canProceed()) {
return { error: "Veuillez patienter un instant avant de réessayer." };
}
try {
return await fn();
} catch (error) {
if (error.message.includes("429")) {
return {
error: "Limite de requêtes atteinte. Patientez une minute.",
};
}
if (error.message.includes("401")) {
return {
error: "Clé API invalide. Vérifiez vos paramètres.",
};
}
return { error: `Erreur inattendue : ${error.message}` };
}
}Étape 10 : Préparer la publication sur le Chrome Web Store
Quand vous êtes prêt à publier :
1. Créer les icônes
Vous aurez besoin d'icônes aux formats 16×16, 48×48 et 128×128 pixels en PNG. Utilisez un outil comme Figma ou même un générateur d'images IA.
2. Créer le fichier ZIP
zip -r summarize-ai.zip summarize-ai/ \
-x "*.DS_Store" \
-x "*node_modules*" \
-x "*.git*"3. Soumettre au Chrome Web Store
- Rendez-vous sur le Tableau de bord développeur Chrome Web Store
- Payez les frais d'inscription uniques de 5 $
- Cliquez « Nouvel élément » et uploadez votre ZIP
- Remplissez la fiche : description, captures d'écran, catégorie
- Soumettez pour révision (généralement 1 à 3 jours ouvrables)
Politique de confidentialité obligatoire : si votre extension envoie des données à des API externes (comme OpenAI), vous devez obligatoirement fournir une URL de politique de confidentialité. C'est une exigence du Chrome Web Store.
Aller plus loin
Voici des idées pour enrichir SummarizeAI :
- Multilingue : Détecter la langue de la page et résumer dans la langue préférée de l'utilisateur
- Prompts personnalisés : Permettre aux utilisateurs de définir leurs propres instructions pour des résumés spécialisés
- Support PDF : Utiliser
pdf.jspour extraire le texte des fichiers PDF - Export : Sauvegarder les résumés en Markdown ou les envoyer vers Notion
- Streaming : Utiliser l'API streaming d'OpenAI pour un affichage en temps réel
- IA locale : Intégrer Ollama pour un résumé entièrement hors ligne
Récapitulatif
Dans ce tutoriel, nous avons construit une extension Chrome complète propulsée par l'IA avec Manifest V3. Voici ce que nous avons couvert :
- ✅ La structure Manifest V3 et les différences clés avec V2
- ✅ Les Service Workers pour le traitement en arrière-plan
- ✅ Les scripts de contenu pour injecter une interface dans les pages web
- ✅ L'intégration de l'API OpenAI pour le résumé et l'explication
- ✅ L'interface du popup avec suivi de l'historique
- ✅ Les menus contextuels et raccourcis clavier
- ✅ La gestion des erreurs et la limitation du débit
- ✅ Le processus de publication sur le Chrome Web Store
Le code source complet est disponible sur GitHub. Les extensions Chrome sont un moyen fantastique de livrer l'IA directement là où les utilisateurs en ont besoin — dans leur navigateur. À vous de jouer ! 🚀
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Construire un Agent IA Autonome avec Agentic RAG et Next.js
Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

Construire des agents IA from scratch avec TypeScript : maîtriser le pattern ReAct avec le Vercel AI SDK
Apprenez à construire des agents IA depuis zéro avec TypeScript. Ce tutoriel couvre le pattern ReAct, l'appel d'outils, le raisonnement multi-étapes et les boucles d'agents prêtes pour la production avec le Vercel AI SDK.

Guide d'Integration de Chatbot IA : Construire des Interfaces Conversationnelles Intelligentes
Un guide complet pour integrer des chatbots IA dans vos applications en utilisant OpenAI, Anthropic Claude et ElevenLabs. Apprenez a construire des chatbots textuels et vocaux avec Next.js.