// General Bots - Content Script for WhatsApp Web // Provides grammar correction, auto-mode, contact hiding, and LLM integration (function () { "use strict"; // Global settings let settings = { serverUrl: "https://api.generalbots.com", enableProcessing: true, hideContacts: false, autoMode: false, grammarCorrection: true, whatsappNumber: "", authenticated: false, }; // State management const state = { initialized: false, currentContact: null, autoModeContacts: new Set(), originalMessages: new Map(), processingQueue: [], isProcessing: false, }; // Selectors for WhatsApp Web elements const SELECTORS = { contactList: "#pane-side", chatContainer: ".copyable-area", messageInput: 'div[contenteditable="true"][data-tab="10"]', sendButton: 'button[data-tab="11"]', messageOut: ".message-out", messageIn: ".message-in", messageText: ".selectable-text", contactName: "header ._amig span", chatHeader: "header._amid", conversationPanel: "#main", searchBox: 'div[data-tab="3"]', }; // Initialize the extension async function init() { if (state.initialized) return; console.log("General Bots: Initializing content script..."); // Load settings from storage await loadSettings(); // Apply initial UI modifications applyUIModifications(); // Setup observers and listeners setupInputListener(); setupMessageObserver(); setupContactObserver(); // Inject custom UI elements injectControlPanel(); state.initialized = true; console.log("General Bots: Content script initialized"); } // Load settings from chrome storage async function loadSettings() { return new Promise((resolve) => { chrome.storage.sync.get(settings, (items) => { settings = { ...settings, ...items }; console.log("General Bots: Settings loaded", settings); resolve(settings); }); }); } // Apply UI modifications based on settings function applyUIModifications() { applyContactVisibility(); applyAutoModeIndicator(); } // Hide/show contact list function applyContactVisibility() { const contactList = document.querySelector(SELECTORS.contactList); if (contactList) { const parent = contactList.parentElement; if (settings.hideContacts) { parent.classList.add("gb-hide-contacts"); parent.style.cssText = "width: 0 !important; min-width: 0 !important; overflow: hidden !important;"; } else { parent.classList.remove("gb-hide-contacts"); parent.style.cssText = ""; } } } // Show auto-mode indicator function applyAutoModeIndicator() { let indicator = document.getElementById("gb-auto-mode-indicator"); if (settings.autoMode) { if (!indicator) { indicator = document.createElement("div"); indicator.id = "gb-auto-mode-indicator"; indicator.className = "gb-auto-indicator"; indicator.innerHTML = ` Auto Mode Active `; document.body.appendChild(indicator); } indicator.style.display = "flex"; } else if (indicator) { indicator.style.display = "none"; } } // Setup input field listener for message processing function setupInputListener() { const observer = new MutationObserver(() => { const inputField = document.querySelector(SELECTORS.messageInput); if (inputField && !inputField.getAttribute("gb-monitored")) { setupFieldMonitoring(inputField); inputField.setAttribute("gb-monitored", "true"); } }); observer.observe(document.body, { childList: true, subtree: true, }); // Check immediately const inputField = document.querySelector(SELECTORS.messageInput); if (inputField && !inputField.getAttribute("gb-monitored")) { setupFieldMonitoring(inputField); inputField.setAttribute("gb-monitored", "true"); } } // Monitor input field for messages function setupFieldMonitoring(inputField) { console.log("General Bots: Setting up input field monitoring"); // Listen for keydown events inputField.addEventListener("keydown", async (event) => { if (event.key === "Enter" && !event.shiftKey) { const originalText = inputField.textContent.trim(); if (originalText.length > 0 && settings.enableProcessing) { if (settings.grammarCorrection) { event.preventDefault(); event.stopPropagation(); try { showProcessingIndicator(inputField); const result = await processMessageWithLLM(originalText); if ( result.processedText && result.processedText !== originalText ) { // Show correction preview const shouldSend = await showCorrectionPreview( originalText, result.processedText, inputField, ); if (shouldSend) { setInputText(inputField, result.processedText); // Store original for reference state.originalMessages.set(Date.now(), { original: originalText, corrected: result.processedText, }); } } hideProcessingIndicator(); // Send the message simulateEnterPress(inputField); } catch (error) { console.error("General Bots: Error processing message", error); hideProcessingIndicator(); simulateEnterPress(inputField); } } } } }); // Add visual indicator that field is being monitored inputField.classList.add("gb-monitored-input"); } // Process message through LLM for grammar correction async function processMessageWithLLM(text) { return new Promise((resolve) => { chrome.runtime.sendMessage( { action: "correctGrammar", text: text, }, (response) => { if (chrome.runtime.lastError) { console.error( "General Bots: Runtime error", chrome.runtime.lastError, ); resolve({ processedText: text }); return; } resolve(response || { processedText: text }); }, ); }); } // Show correction preview modal async function showCorrectionPreview(original, corrected, inputField) { return new Promise((resolve) => { // If texts are very similar, auto-accept if (levenshteinDistance(original, corrected) < 3) { resolve(true); return; } const modal = document.createElement("div"); modal.className = "gb-correction-modal"; modal.innerHTML = `
Grammar Correction

${escapeHtml(original)}

${escapeHtml(corrected)}

`; document.body.appendChild(modal); // Auto-close after 5 seconds with corrected text const autoClose = setTimeout(() => { modal.remove(); resolve(true); }, 5000); document.getElementById("gb-accept").addEventListener("click", () => { clearTimeout(autoClose); modal.remove(); resolve(true); }); document.getElementById("gb-reject").addEventListener("click", () => { clearTimeout(autoClose); modal.remove(); resolve(false); }); }); } // Setup observer for incoming messages (for auto-reply) function setupMessageObserver() { const observer = new MutationObserver((mutations) => { if (!settings.autoMode) return; for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { const incomingMsg = node.querySelector ? node.querySelector(SELECTORS.messageIn) : null; if ( incomingMsg || (node.classList && node.classList.contains("message-in")) ) { handleIncomingMessage(incomingMsg || node); } } } } }); // Start observing when chat container is available const waitForChat = setInterval(() => { const chatContainer = document.querySelector(SELECTORS.chatContainer); if (chatContainer) { clearInterval(waitForChat); observer.observe(chatContainer, { childList: true, subtree: true, }); console.log("General Bots: Message observer started"); } }, 1000); } // Handle incoming message for auto-reply async function handleIncomingMessage(messageElement) { if (!settings.autoMode || !settings.authenticated) return; const currentContact = getCurrentContactName(); if (!currentContact) return; // Check if auto-mode is enabled for this contact if (!state.autoModeContacts.has(currentContact)) return; const messageText = messageElement.querySelector(SELECTORS.messageText); if (!messageText) return; const text = messageText.textContent.trim(); if (!text) return; console.log( "General Bots: Processing incoming message for auto-reply", text, ); // Get conversation context const context = getConversationContext(); // Request auto-reply from LLM chrome.runtime.sendMessage( { action: "generateAutoReply", context: { contact: currentContact, lastMessage: text, }, lastMessages: context, }, async (response) => { if (response && response.reply && response.autoSend) { await sendAutoReply(response.reply); } }, ); } // Get conversation context (last few messages) function getConversationContext() { const messages = []; const messageElements = document.querySelectorAll( `${SELECTORS.messageIn}, ${SELECTORS.messageOut}`, ); const recentMessages = Array.from(messageElements).slice(-10); for (const msg of recentMessages) { const textEl = msg.querySelector(SELECTORS.messageText); if (textEl) { messages.push({ type: msg.classList.contains("message-out") ? "sent" : "received", text: textEl.textContent.trim(), }); } } return messages; } // Send auto-reply async function sendAutoReply(text) { const inputField = document.querySelector(SELECTORS.messageInput); if (!inputField) return; setInputText(inputField, text); // Small delay before sending await new Promise((r) => setTimeout(r, 500)); simulateEnterPress(inputField); } // Setup contact observer for auto-mode toggle per contact function setupContactObserver() { const observer = new MutationObserver(() => { const header = document.querySelector(SELECTORS.chatHeader); if (header && !header.querySelector(".gb-contact-controls")) { injectContactControls(header); } }); observer.observe(document.body, { childList: true, subtree: true, }); } // Inject control buttons for current contact function injectContactControls(header) { const contactName = getCurrentContactName(); if (!contactName) return; const controls = document.createElement("div"); controls.className = "gb-contact-controls"; const isAutoEnabled = state.autoModeContacts.has(contactName); controls.innerHTML = ` `; header.appendChild(controls); document .getElementById("gb-toggle-auto") .addEventListener("click", function () { if (state.autoModeContacts.has(contactName)) { state.autoModeContacts.delete(contactName); this.classList.remove("active"); this.querySelector(".gb-label").textContent = "Auto OFF"; } else { state.autoModeContacts.add(contactName); this.classList.add("active"); this.querySelector(".gb-label").textContent = "Auto ON"; } }); } // Inject main control panel function injectControlPanel() { const panel = document.createElement("div"); panel.id = "gb-control-panel"; panel.className = "gb-panel"; panel.innerHTML = `
General Bots
${settings.authenticated ? "Connected" : "Not Connected"}
${ !settings.authenticated ? `

Connect with your General Bots account:

` : "" }
`; document.body.appendChild(panel); // Setup panel event listeners setupPanelListeners(); } // Setup control panel event listeners function setupPanelListeners() { // Panel toggle document .getElementById("gb-panel-toggle") ?.addEventListener("click", function () { const body = document.getElementById("gb-panel-body"); if (body.style.display === "none") { body.style.display = "block"; this.textContent = "−"; } else { body.style.display = "none"; this.textContent = "+"; } }); // Grammar toggle document .getElementById("gb-grammar-toggle") ?.addEventListener("change", function () { settings.grammarCorrection = this.checked; saveSettings(); }); // Contacts toggle document .getElementById("gb-contacts-toggle") ?.addEventListener("change", function () { settings.hideContacts = this.checked; applyContactVisibility(); saveSettings(); }); // Auto mode toggle document .getElementById("gb-auto-toggle") ?.addEventListener("change", function () { settings.autoMode = this.checked; applyAutoModeIndicator(); saveSettings(); }); // Auth button document .getElementById("gb-auth-btn") ?.addEventListener("click", async function () { const numberInput = document.getElementById("gb-whatsapp-number"); const number = numberInput?.value.trim(); if (!number) { alert("Please enter your WhatsApp number"); return; } this.disabled = true; this.textContent = "Authenticating..."; chrome.runtime.sendMessage( { action: "authenticate", whatsappNumber: number, }, (response) => { if (response.success) { this.textContent = "Check WhatsApp"; } else { this.textContent = "Authenticate"; this.disabled = false; alert( "Authentication failed: " + (response.error || "Unknown error"), ); } }, ); }); } // Save settings to storage function saveSettings() { chrome.storage.sync.set(settings); chrome.runtime.sendMessage({ action: "saveSettings", settings: settings, }); } // Get current contact name function getCurrentContactName() { const nameEl = document.querySelector(SELECTORS.contactName); return nameEl ? nameEl.textContent.trim() : null; } // Set text in input field function setInputText(inputField, text) { inputField.textContent = text; inputField.dispatchEvent(new InputEvent("input", { bubbles: true })); } // Simulate Enter key press function simulateEnterPress(element) { const enterEvent = new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true, cancelable: true, }); element.dispatchEvent(enterEvent); } // Show processing indicator function showProcessingIndicator(inputField) { let indicator = document.getElementById("gb-processing"); if (!indicator) { indicator = document.createElement("div"); indicator.id = "gb-processing"; indicator.className = "gb-processing-indicator"; indicator.innerHTML = `
Processing with AI... `; inputField.parentElement.appendChild(indicator); } indicator.style.display = "flex"; } // Hide processing indicator function hideProcessingIndicator() { const indicator = document.getElementById("gb-processing"); if (indicator) { indicator.style.display = "none"; } } // Utility: Escape HTML function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } // Utility: Levenshtein distance for text comparison function levenshteinDistance(str1, str2) { const m = str1.length; const n = str2.length; const dp = Array(m + 1) .fill(null) .map(() => Array(n + 1).fill(0)); for (let i = 0; i <= m; i++) dp[i][0] = i; for (let j = 0; j <= n; j++) dp[0][j] = j; for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { if (str1[i - 1] === str2[j - 1]) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1; } } } return dp[m][n]; } // Listen for messages from background script chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { switch (message.action) { case "tabReady": init(); break; case "settingsUpdated": settings = { ...settings, ...message.settings }; applyUIModifications(); break; case "enableAutoMode": settings.autoMode = true; settings.whatsappNumber = message.whatsappNumber; applyAutoModeIndicator(); break; case "replaceSelection": const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); range.deleteContents(); range.insertNode(document.createTextNode(message.text)); } break; case "authCompleted": settings.authenticated = true; // Refresh control panel const panel = document.getElementById("gb-control-panel"); if (panel) { panel.remove(); injectControlPanel(); } break; } sendResponse({ success: true }); return true; }); // Initialize on DOM ready if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } // Also try to initialize after a delay (WhatsApp Web loads dynamically) setTimeout(init, 2000); })();