Building a Text Condensing Chrome Plugin with GPT API Integration

I’m developing a Chrome plugin called TextCondenser that lets users select text and get condensed versions. I’m using GPT API for the text processing but running into problems with API calls and pricing concerns. I need assistance debugging my implementation to get the condensing feature working properly.

How the plugin functions:

  • User selects text on any webpage
  • A “Condense Text” button shows up next to selection
  • Clicking triggers API call to process the selected content
  • Processed result appears in popup and gets stored in extension history

Most functionality is complete but I’m struggling with the text processing logic. Here are my source files:

service-worker.js

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

page-script.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.background = "#2196f3";
        currentButton.style.color = "white";
        currentButton.style.border = "none";
        currentButton.style.padding = "10px";
        currentButton.style.cursor = "pointer";
        currentButton.style.borderRadius = "4px";
        currentButton.style.fontSize = "12px";
        document.body.appendChild(currentButton);
    }
    
    let selection = window.getSelection().getRangeAt(0).getBoundingClientRect();
    currentButton.style.top = `${selection.bottom + window.scrollY + 10}px`;
    currentButton.style.left = `${selection.left + window.scrollX}px`;

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

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

async function processText(content) {
    const token = "Your_API_Token_Here";
    const endpoint = "https://api.openai.com/v1/chat/completions";

    try {
        const apiResponse = await fetch(endpoint, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Authorization": `Bearer ${token}`
            },
            body: JSON.stringify({
                model: "gpt-4",
                messages: [{ role: "user", content: `Please condense this text: ${content}` }],
                max_tokens: 150
            })
        });

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

        const responseData = await apiResponse.json();
        return responseData.choices?.[0]?.message?.content || "Processing failed.";
    } catch (err) {
        console.error("Text processing error:", err);
        return "Unable to process text.";
    }
}

extension.json

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

panel.html

<!DOCTYPE html>
<html>
<head>
    <title>Text History</title>
    <style>
        body {
            font-family: Verdana, sans-serif;
            margin: 15px;
            width: 280px;
        }
        .history-entry {
            border-bottom: 2px solid #eee;
            padding: 15px;
            margin-bottom: 15px;
        }
        .remove-btn {
            background-color: #e91e63;
            color: white;
            border: none;
            padding: 8px;
            cursor: pointer;
            border-radius: 3px;
        }
    </style>
</head>
<body>
    <h3>Condensed Text History</h3>
    <div id="history-container"></div>
    <script src="panel-script.js"></script>
</body>
</html>

panel-script.js

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

    function refreshHistory() {
        chrome.storage.local.get(["textHistory"], function (data) {
            historyContainer.innerHTML = "";
            const history = data.textHistory || [];
            history.forEach((entry, idx) => {
                showHistoryEntry(entry, idx);
            });
        });
    }

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

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

        historyContainer.appendChild(entryDiv);
    }

    function removeHistoryEntry(idx) {
        chrome.storage.local.get(["textHistory"], function (data) {
            let history = data.textHistory || [];
            history.splice(idx, 1);

            chrome.storage.local.set({ textHistory: history }, function () {
                refreshHistory();
            });
        });
    }

    refreshHistory();
});

Your button click handler has a nasty bug - you’re adding the event listener inside the mouseup event. If someone selects text multiple times, you’ll stack multiple handlers on the same button.

But the real problem is your prompt. “Please condense this text:” is way too vague and you’ll get garbage results. Try this instead: “Summarize the following text in 2-3 sentences, preserving the main points and key information.”

Also, add a character limit before hitting the API. I learned this when users started selecting entire articles and destroyed my budget. Cap it at 2000 characters - covers most reasonable selections without breaking the bank.

yea, sounds like your main issue is gonna be CSP violations. many sites block external api calls from content scripts now, so CORS will def shut you down. move the fetch to your service worker and use chrome.runtime.sendMessage for communication instead. and instead of just console.error, give users some actual feedback when stuff goes wrong!

Found your problem - you’re not saving the condensed text to storage after processing. Your processText function gets the result but doesn’t store it in chrome.storage.local where your panel looks for it. Add this after getting the API response:

chrome.storage.local.get(['textHistory'], function(data) {
    let history = data.textHistory || [];
    history.unshift(condensedText);
    chrome.storage.local.set({textHistory: history});
});

Quick tip on API costs - you’re using gpt-4 which gets expensive fast. Try gpt-3.5-turbo instead. For text condensation, the quality’s basically the same but you’ll save around 90% on costs. I built something similar and that switch was a game-changer.

One more thing - don’t hardcode your API key in content scripts. Anyone can view source and grab it. Move the API call to your service worker and use message passing instead.