import { DOMAINS, ELEMENT_IDS, MESSAGE_TYPES, POSTHOG_EVENT_KEY, STORAGE_KEYS, UI_CONFIG, } from "../../utils/constants"; import { createChatGPTInputBarElement, DOMUtils, } from "../../utils/ui-components"; let chatGPTDebounceTimeout: NodeJS.Timeout | null = null; let chatGPTRouteObserver: MutationObserver | null = null; let chatGPTUrlCheckInterval: NodeJS.Timeout | null = null; let chatGPTObserverThrottle: NodeJS.Timeout | null = null; export function initializeChatGPT() { if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { return; } if (document.body.hasAttribute("data-chatgpt-initialized")) { return; } setTimeout(() => { addSupermemoryButtonToMemoriesDialog(); addSaveChatGPTElementBeforeComposerBtn(); setupChatGPTAutoFetch(); }, 2000); setupChatGPTPromptCapture(); setupChatGPTRouteChangeDetection(); document.body.setAttribute("data-chatgpt-initialized", "true"); } function setupChatGPTRouteChangeDetection() { if (chatGPTRouteObserver) { chatGPTRouteObserver.disconnect(); } if (chatGPTUrlCheckInterval) { clearInterval(chatGPTUrlCheckInterval); } if (chatGPTObserverThrottle) { clearTimeout(chatGPTObserverThrottle); chatGPTObserverThrottle = null; } let currentUrl = window.location.href; const checkForRouteChange = () => { if (window.location.href !== currentUrl) { currentUrl = window.location.href; console.log("ChatGPT route changed, re-adding supermemory elements"); setTimeout(() => { addSupermemoryButtonToMemoriesDialog(); addSaveChatGPTElementBeforeComposerBtn(); setupChatGPTAutoFetch(); }, 1000); } }; chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 2000); chatGPTRouteObserver = new MutationObserver((mutations) => { if (chatGPTObserverThrottle) { return; } let shouldRecheck = false; mutations.forEach((mutation) => { if (mutation.type === "childList" && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { const element = node as Element; if ( element.querySelector?.("#prompt-textarea") || element.querySelector?.("button.composer-btn") || element.querySelector?.('[role="dialog"]') || element.matches?.("#prompt-textarea") || element.id === "prompt-textarea" ) { shouldRecheck = true; } } }); } }); if (shouldRecheck) { chatGPTObserverThrottle = setTimeout(() => { try { chatGPTObserverThrottle = null; addSupermemoryButtonToMemoriesDialog(); addSaveChatGPTElementBeforeComposerBtn(); setupChatGPTAutoFetch(); } catch (error) { console.error("Error in ChatGPT observer callback:", error); } }, 300); } }); try { chatGPTRouteObserver.observe(document.body, { childList: true, subtree: true, }); } catch (error) { console.error("Failed to set up ChatGPT route observer:", error); if (chatGPTUrlCheckInterval) { clearInterval(chatGPTUrlCheckInterval); } chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 1000); } } async function getRelatedMemoriesForChatGPT(actionSource: string) { try { const userQuery = document.getElementById("prompt-textarea")?.textContent || ""; const icon = document.querySelectorAll( '[id*="sm-chatgpt-input-bar-element-before-composer"]', )[0]; const iconElement = icon as HTMLElement; if (!iconElement) { console.warn("ChatGPT icon element not found, cannot update feedback"); return; } updateChatGPTIconFeedback("Searching memories...", iconElement); const timeoutPromise = new Promise((_, reject) => setTimeout( () => reject(new Error("Memory search timeout")), UI_CONFIG.API_REQUEST_TIMEOUT, ), ); const response = await Promise.race([ browser.runtime.sendMessage({ action: MESSAGE_TYPES.GET_RELATED_MEMORIES, data: userQuery, actionSource: actionSource, }), timeoutPromise, ]); if (response?.success && response?.data) { const promptElement = document.getElementById("prompt-textarea"); if (promptElement) { promptElement.dataset.supermemories = `
Supermemories of user (only for the reference): ${response.data}
`; console.log( "Prompt element dataset:", promptElement.dataset.supermemories, ); iconElement.dataset.memoriesData = response.data; updateChatGPTIconFeedback("Included Memories", iconElement); } else { console.warn( "ChatGPT prompt element not found after successful memory fetch", ); updateChatGPTIconFeedback("Memories found", iconElement); } } else { console.warn("No memories found or API response invalid"); updateChatGPTIconFeedback("No memories found", iconElement); } } catch (error) { console.error("Error getting related memories:", error); try { const icon = document.querySelectorAll( '[id*="sm-chatgpt-input-bar-element-before-composer"]', )[0] as HTMLElement; if (icon) { updateChatGPTIconFeedback("Error fetching memories", icon); } } catch (feedbackError) { console.error("Failed to update error feedback:", feedbackError); } } } function addSupermemoryButtonToMemoriesDialog() { const dialogs = document.querySelectorAll('[role="dialog"]'); let memoriesDialog: HTMLElement | null = null; for (const dialog of dialogs) { const headerText = dialog.querySelector("h2"); if (headerText?.textContent?.includes("Saved memories")) { memoriesDialog = dialog as HTMLElement; break; } } if (!memoriesDialog) return; if (memoriesDialog.querySelector("#supermemory-save-button")) return; const deleteAllContainer = memoriesDialog.querySelector( ".mt-5.flex.justify-end", ); if (!deleteAllContainer) return; const supermemoryButton = document.createElement("button"); supermemoryButton.id = "supermemory-save-button"; supermemoryButton.className = "btn relative btn-primary-outline mr-2"; const iconUrl = browser.runtime.getURL("/icon-16.png"); supermemoryButton.innerHTML = `
supermemory Save to supermemory
`; supermemoryButton.style.cssText = ` background: #1C2026 !important; color: white !important; border: 1px solid #1C2026 !important; border-radius: 9999px !important; padding: 10px 16px !important; font-weight: 500 !important; font-size: 14px !important; margin-right: 8px !important; cursor: pointer !important; `; supermemoryButton.addEventListener("mouseenter", () => { supermemoryButton.style.backgroundColor = "#2B2E33"; }); supermemoryButton.addEventListener("mouseleave", () => { supermemoryButton.style.backgroundColor = "#1C2026"; }); supermemoryButton.addEventListener("click", async () => { await saveMemoriesToSupermemory(); }); deleteAllContainer.insertBefore( supermemoryButton, deleteAllContainer.firstChild, ); } async function saveMemoriesToSupermemory() { try { DOMUtils.showToast("loading"); const memoriesTable = document.querySelector('[role="dialog"] table tbody'); if (!memoriesTable) { DOMUtils.showToast("error"); return; } const memoryRows = memoriesTable.querySelectorAll("tr"); const memories: string[] = []; memoryRows.forEach((row) => { const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap"); if (memoryCell?.textContent) { memories.push(memoryCell.textContent.trim()); } }); console.log("Memories:", memories); if (memories.length === 0) { DOMUtils.showToast("error"); return; } const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}`; const response = await browser.runtime.sendMessage({ action: MESSAGE_TYPES.SAVE_MEMORY, data: { html: combinedContent, }, actionSource: "chatgpt_memories_dialog", }); console.log({ response }); if (response.success) { DOMUtils.showToast("success"); } else { DOMUtils.showToast("error"); } } catch (error) { console.error("Error saving memories to supermemory:", error); DOMUtils.showToast("error"); } } function updateChatGPTIconFeedback( message: string, iconElement: HTMLElement, resetAfter = 0, ) { if (!iconElement.dataset.originalHtml) { iconElement.dataset.originalHtml = iconElement.innerHTML; } const feedbackDiv = document.createElement("div"); feedbackDiv.style.cssText = ` display: flex; align-items: center; gap: 6px; padding: 4px 8px; background: #513EA9; border-radius: 12px; color: white; font-size: 12px; font-weight: 500; cursor: ${message === "Included Memories" ? "pointer" : "default"}; position: relative; `; feedbackDiv.innerHTML = ` ✓ ${message} `; if (message === "Included Memories" && iconElement.dataset.memoriesData) { const popup = document.createElement("div"); popup.style.cssText = ` position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background: #1a1a1a; color: white; padding: 0; border-radius: 12px; font-size: 13px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; max-width: 500px; max-height: 400px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); z-index: 999999; display: none; border: 1px solid #333; `; const header = document.createElement("div"); header.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #333; opacity: 0.8; `; header.innerHTML = ` Included Memories `; const content = document.createElement("div"); content.style.cssText = ` padding: 0; max-height: 300px; overflow-y: auto; `; const memoriesText = iconElement.dataset.memoriesData || ""; console.log("Memories text:", memoriesText); const individualMemories = memoriesText .split(/[,\n]/) .map((memory) => memory.trim()) .filter((memory) => memory.length > 0 && memory !== ","); console.log("Individual memories:", individualMemories); individualMemories.forEach((memory, index) => { const memoryItem = document.createElement("div"); memoryItem.style.cssText = ` display: flex; align-items: center; gap: 6px; padding: 10px; font-size: 13px; line-height: 1.4; `; const memoryText = document.createElement("div"); memoryText.style.cssText = ` flex: 1; color: #e5e5e5; `; memoryText.textContent = memory.trim(); const removeBtn = document.createElement("button"); removeBtn.style.cssText = ` background: transparent; color: #9ca3af; border: none; padding: 4px; border-radius: 4px; cursor: pointer; flex-shrink: 0; height: fit-content; display: flex; align-items: center; justify-content: center; `; removeBtn.innerHTML = ``; removeBtn.dataset.memoryIndex = index.toString(); removeBtn.addEventListener("mouseenter", () => { removeBtn.style.color = "#ef4444"; }); removeBtn.addEventListener("mouseleave", () => { removeBtn.style.color = "#9ca3af"; }); memoryItem.appendChild(memoryText); memoryItem.appendChild(removeBtn); content.appendChild(memoryItem); }); popup.appendChild(header); popup.appendChild(content); document.body.appendChild(popup); feedbackDiv.addEventListener("mouseenter", () => { const textSpan = feedbackDiv.querySelector("span:last-child"); if (textSpan) { textSpan.textContent = "Click to see memories"; } }); feedbackDiv.addEventListener("mouseleave", () => { const textSpan = feedbackDiv.querySelector("span:last-child"); if (textSpan) { textSpan.textContent = "Included Memories"; } }); feedbackDiv.addEventListener("click", (e) => { e.stopPropagation(); popup.style.display = "block"; }); document.addEventListener("click", (e) => { if (!popup.contains(e.target as Node)) { popup.style.display = "none"; } }); content.querySelectorAll("button[data-memory-index]").forEach((button) => { const htmlButton = button as HTMLButtonElement; htmlButton.addEventListener("click", () => { const index = Number.parseInt( htmlButton.dataset.memoryIndex || "0", 10, ); const memoryItem = htmlButton.parentElement; if (memoryItem) { content.removeChild(memoryItem); } const currentMemories = (iconElement.dataset.memoriesData || "") .split(/[,\n]/) .map((memory) => memory.trim()) .filter((memory) => memory.length > 0 && memory !== ","); currentMemories.splice(index, 1); const updatedMemories = currentMemories.join(" ,"); iconElement.dataset.memoriesData = updatedMemories; const promptElement = document.getElementById("prompt-textarea"); if (promptElement) { promptElement.dataset.supermemories = `
Supermemories of user (only for the reference): ${updatedMemories}
`; } content .querySelectorAll("button[data-memory-index]") .forEach((btn, newIndex) => { const htmlBtn = btn as HTMLButtonElement; htmlBtn.dataset.memoryIndex = newIndex.toString(); }); if (currentMemories.length <= 1) { if (promptElement?.dataset.supermemories) { delete promptElement.dataset.supermemories; delete iconElement.dataset.memoriesData; iconElement.innerHTML = iconElement.dataset.originalHtml || ""; delete iconElement.dataset.originalHtml; } popup.style.display = "none"; if (document.body.contains(popup)) { document.body.removeChild(popup); } } }); }); setTimeout(() => { if (document.body.contains(popup)) { document.body.removeChild(popup); } }, 300000); } iconElement.innerHTML = ""; iconElement.appendChild(feedbackDiv); if (resetAfter > 0) { setTimeout(() => { iconElement.innerHTML = iconElement.dataset.originalHtml || ""; delete iconElement.dataset.originalHtml; }, resetAfter); } } function addSaveChatGPTElementBeforeComposerBtn() { const composerButtons = document.querySelectorAll("button.composer-btn"); composerButtons.forEach((button) => { if (button.hasAttribute("data-supermemory-icon-added-before")) { return; } const parent = button.parentElement; if (!parent) return; const parentSiblings = parent.parentElement?.children; if (!parentSiblings) return; let hasSpeechButtonSibling = false; for (const sibling of parentSiblings) { if ( sibling.getAttribute("data-testid") === "composer-speech-button-container" ) { hasSpeechButtonSibling = true; break; } } if (!hasSpeechButtonSibling) return; const grandParent = parent.parentElement; if (!grandParent) return; const existingIcon = grandParent.querySelector( `#${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer`, ); if (existingIcon) { button.setAttribute("data-supermemory-icon-added-before", "true"); return; } const saveChatGPTElement = createChatGPTInputBarElement(async () => { await getRelatedMemoriesForChatGPT( POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_SEARCHED, ); }); saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; button.setAttribute("data-supermemory-icon-added-before", "true"); grandParent.insertBefore(saveChatGPTElement, parent); setupChatGPTAutoFetch(); }); } async function setupChatGPTAutoFetch() { const result = await chrome.storage.local.get([ STORAGE_KEYS.AUTO_SEARCH_ENABLED, ]); const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false; if (!autoSearchEnabled) { return; } const promptTextarea = document.getElementById("prompt-textarea"); if ( !promptTextarea || promptTextarea.hasAttribute("data-supermemory-auto-fetch") ) { return; } promptTextarea.setAttribute("data-supermemory-auto-fetch", "true"); const handleInput = () => { if (chatGPTDebounceTimeout) { clearTimeout(chatGPTDebounceTimeout); } chatGPTDebounceTimeout = setTimeout(async () => { const content = promptTextarea.textContent?.trim() || ""; if (content.length > 2) { await getRelatedMemoriesForChatGPT( POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED, ); } else if (content.length === 0) { const icons = document.querySelectorAll( '[id*="sm-chatgpt-input-bar-element-before-composer"]', ); icons.forEach((icon) => { const iconElement = icon as HTMLElement; if (iconElement.dataset.originalHtml) { iconElement.innerHTML = iconElement.dataset.originalHtml; delete iconElement.dataset.originalHtml; delete iconElement.dataset.memoriesData; } }); if (promptTextarea.dataset.supermemories) { delete promptTextarea.dataset.supermemories; } } }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY); }; promptTextarea.addEventListener("input", handleInput); } function setupChatGPTPromptCapture() { if (document.body.hasAttribute("data-chatgpt-prompt-capture-setup")) { return; } document.body.setAttribute("data-chatgpt-prompt-capture-setup", "true"); const capturePromptContent = async (source: string) => { const promptTextarea = document.getElementById("prompt-textarea"); let promptContent = ""; if (promptTextarea) { promptContent = promptTextarea.textContent || ""; } const storedMemories = promptTextarea?.dataset.supermemories; if ( storedMemories && promptTextarea && !promptContent.includes("Supermemories of user") ) { promptTextarea.innerHTML = `${promptTextarea.innerHTML} ${storedMemories}`; promptContent = promptTextarea.textContent || ""; } if (promptTextarea && promptContent.trim()) { console.log(`ChatGPT prompt submitted via ${source}:`, promptContent); try { await browser.runtime.sendMessage({ action: MESSAGE_TYPES.CAPTURE_PROMPT, data: { prompt: promptContent, platform: "chatgpt", source: source, }, }); } catch (error) { console.error("Error sending ChatGPT prompt to background:", error); } } const icons = document.querySelectorAll( '[id*="sm-chatgpt-input-bar-element-before-composer"]', ); icons.forEach((icon) => { const iconElement = icon as HTMLElement; if (iconElement.dataset.originalHtml) { iconElement.innerHTML = iconElement.dataset.originalHtml; delete iconElement.dataset.originalHtml; delete iconElement.dataset.memoriesData; } }); if (promptTextarea?.dataset.supermemories) { delete promptTextarea.dataset.supermemories; } }; document.addEventListener( "click", async (event) => { const target = event.target as HTMLElement; if ( target.id === "composer-submit-button" || target.closest("#composer-submit-button") ) { await capturePromptContent("button click"); } }, true, ); document.addEventListener( "keydown", async (event) => { const target = event.target as HTMLElement; if ( target.id === "prompt-textarea" && event.key === "Enter" && !event.shiftKey ) { await capturePromptContent("Enter key"); } }, true, ); }