Building a Browser Extension for Text Condensation with AI API Integration

I’m developing a browser extension called TextCondenser that lets users select text and get condensed versions using AI. The extension is mostly working but I’m having trouble with the AI service integration and managing API costs.

How it should work:

  • User selects some text on any webpage
  • A “Condense Text” button shows up next to the selection
  • Clicking it sends the text to an AI service for condensation
  • The result appears in a popup and gets stored in a sidebar panel

Most features are done but the condensation function isn’t working right. Here’s my code:

background.js

chrome.runtime.onInstalled.addListener(() => {
    chrome.sidePanel
        .setPanelBehavior({ openPanelOnActionClick: true })
        .catch((err) => console.error("Panel Setup Error:", err));
});

content.js

document.addEventListener("mouseup", function (evt) {
    let highlightedText = window.getSelection().toString().trim();
    let currentButton = document.getElementById("condense-btn");
    
    if (!highlightedText) {
        if (currentButton) currentButton.remove();
        return;
    }
    
    if (!currentButton) {
        currentButton = document.createElement("button");
        currentButton.id = "condense-btn";
        currentButton.innerText = "Condense Text";
        currentButton.style.position = "absolute";
        currentButton.style.zIndex = "9999";
        currentButton.style.backgroundColor = "#2196f3";
        currentButton.style.color = "white";
        currentButton.style.border = "none";
        currentButton.style.padding = "10px";
        currentButton.style.cursor = "pointer";
        currentButton.style.borderRadius = "4px";
        document.body.appendChild(currentButton);
    }
    
    let bounds = window.getSelection().getRangeAt(0).getBoundingClientRect();
    currentButton.style.top = `${bounds.bottom + window.scrollY + 8}px`;
    currentButton.style.left = `${bounds.left + window.scrollX}px`;

    currentButton.onclick = async function () {
        currentButton.innerText = "Processing...";
        let condensed = await condenseContent(highlightedText);
        alert("Condensed Version:\n" + condensed);
        currentButton.remove();
    };

    document.addEventListener("click", function hideBtn(evt) {
        if (!currentButton.contains(evt.target)) {
            currentButton.remove();
            document.removeEventListener("click", hideBtn);
        }
    });
});

async function condenseContent(content) {
    const token = "Your-API-Token-Here";
    const endpoint = "https://api.openai.com/v1/chat/completions";

    try {
        const result = await fetch(endpoint, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Authorization": `Bearer ${token}`
            },
            body: JSON.stringify({
                model: "gpt-3.5-turbo",
                messages: [{ role: "user", content: `Make this shorter: ${content}` }],
                max_tokens: 80
            })
        });

        if (!result.ok) {
            let errorData = await result.text();
            console.error("Request Failed:", errorData);
            throw new Error("Request Failed: " + errorData);
        }

        const responseData = await result.json();
        return responseData.choices?.[0]?.message?.content || "Failed to condense text.";
    } catch (err) {
        console.error("Condensation failed:", err);
        return "Failed to condense text.";
    }
}

manifest.json

{
    "manifest_version": 3,
    "name": "TextCondenser",
    "version": "1.0",
    "description": "Condense and store text snippets.",
    "permissions": ["storage", "sidePanel"],
    "action": {
        "default_icon": {
            "48": "icons/icon48.png",
            "128": "icons/icon128.png"
        }
    },
    "side_panel": {
        "default_path": "panel.html"
    },
    "background": {
        "service_worker": "background.js"
    },
    "content_scripts": [
        {
            "matches": ["<all_urls>"],
            "js": ["content.js"],
            "run_at": "document_end"
        }
    ]
}

panel.html

<!DOCTYPE html>
<html>
<head>
    <title>Condensed Text History</title>
    <style>
        body {
            font-family: sans-serif;
            padding: 15px;
            width: 320px;
        }
        .text-item {
            border-bottom: 1px solid #ccc;
            padding: 12px;
            margin-bottom: 8px;
        }
        .remove-btn {
            background-color: #e53e3e;
            color: white;
            border: none;
            padding: 6px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <h2>Condensed History</h2>
    <div id="text-history"></div>
    <script src="panel.js"></script>
</body>
</html>

panel.js

document.addEventListener("DOMContentLoaded", function () {
    const historyContainer = document.getElementById("text-history");

    function refreshHistory() {
        chrome.storage.local.get(["condensedTexts"], function (data) {
            historyContainer.innerHTML = "";
            const texts = data.condensedTexts || [];
            texts.forEach((text, idx) => {
                showTextItem(text, idx);
            });
        });
    }

    function showTextItem(text, idx) {
        const itemDiv = document.createElement("div");
        itemDiv.classList.add("text-item");
        itemDiv.innerHTML = `
            <p>${text}</p>
            <button class="remove-btn" data-index="${idx}">Remove</button>
        `;

        itemDiv.querySelector(".remove-btn").addEventListener("click", function () {
            removeTextItem(idx);
        });

        historyContainer.appendChild(itemDiv);
    }

    function removeTextItem(idx) {
        chrome.storage.local.get(["condensedTexts"], function (data) {
            let texts = data.condensedTexts || [];
            texts.splice(idx, 1);

            chrome.storage.local.set({ condensedTexts: texts }, function () {
                refreshHistory();
            });
        });
    }

    refreshHistory();
});

Any ideas what might be wrong with my condensation logic?

You’re making direct API calls from the content script - Chrome blocks that. Even if you move to background script messaging, you’ll face tons of headaches with API management.

I hit this same wall building internal text processing tools. API key management, rate limiting, error handling, cost monitoring - it gets messy fast. Plus every user needs their own OpenAI account.

Latenode saved my sanity. I switched the AI integration there instead of fighting Chrome’s security restrictions. Created a simple Latenode scenario that handles OpenAI calls. Your extension sends a webhook request to Latenode with selected text, gets back the condensed version.

Latenode handles all the API mess, you can set proper rate limiting, and add multiple AI providers as backups when OpenAI goes down. You monitor usage and costs in one dashboard instead of digging through OpenAI billing.

Just replace that condenseContent function with a simple fetch to your Latenode webhook. Way cleaner and more reliable than managing AI APIs directly in a browser extension.

Quick fix - add "host_permissions": ["https://api.openai.com/*"] to your manifest for API calls. That hardcoded API key will cause problems though. Store it in chrome.storage so users can add their own key instead.

I built something similar last year and hit the same wall. Your content script can’t call external APIs directly - Chrome blocks cross-origin requests for security reasons.

Move your API calls to the background script instead. Have your content script message the background script with the text, let the background handle the OpenAI call, then send results back. Don’t forget to add host permissions for OpenAI’s API in your manifest.

On costs - learn from my mistake and set usage limits on your OpenAI account right now. Users will spam that button and burn through your credits fast. Add some rate limiting or cache common phrases. Also validate text length first so you’re not wasting tokens on tiny selections.

Your event listener setup has a memory leak issue. Every mouseup event adds a new click listener without removing the old one, which will eventually break everything. I hit this same problem building a text highlighter. Store the click listener reference so you can properly remove it. Also, your button positioning breaks on pages with CSS transforms or sticky headers - use getBoundingClientRect() with window.pageYOffset instead of scrollY. For the API integration - yeah, there’s the cross-origin issue others mentioned, but you’ve also got a token limit problem. 80 max_tokens isn’t enough for longer text. The AI cuts off mid-sentence. I use 150-200 tokens for most cases. One more thing - check for empty selections after trimming whitespace. Users constantly select just spaces or line breaks by accident, which burns through API calls.

Yeah, it’s the cross-origin issue others mentioned, but there’s another problem everyone missed - you’re not actually storing the condensed results. Your panel.js looks for condensed texts in chrome.storage, but your condenseContent function never saves them there.

After you get the AI response, add this:

chrome.storage.local.get(['condensedTexts'], function(data) {
    let texts = data.condensedTexts || [];
    texts.push(condensed);
    chrome.storage.local.set({condensedTexts: texts});
});

For the API calls, I’ve hit similar restrictions. You’ll need messaging between content and background scripts. Just heads up though - OpenAI responses take 3-10 seconds, so users will be sitting there waiting forever.

Ditch the button text change and use a proper loading indicator. Also throw in a character limit before sending or you’ll get crushed with API costs. Trust me, I learned this when someone selected entire Wikipedia articles.