diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts index cd64c8e2..12126115 100644 --- a/apps/browser-extension/entrypoints/background.ts +++ b/apps/browser-extension/entrypoints/background.ts @@ -106,6 +106,44 @@ export default defineBackground(() => { } }; + const getRelatedMemories = async (data: any): Promise<{ success: boolean; data?: any; error?: string }> => { + try { + const result = await browser.storage.local.get(['bearerToken']); + const bearerToken = result.bearerToken; + + if (!bearerToken) { + return { success: false, error: 'No authentication token found' }; + } + + const response = await fetch(`${API_ENDPOINTS.SUPERMEMORY_API}/v3/search`, { + method: 'POST', + credentials: 'omit', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${bearerToken}`, + }, + body: JSON.stringify({ + q: data + }), + }); + + if (!response.ok) { + return { success: false, error: `API call failed: ${response.status}` }; + } + + const responseData = await response.json(); + const content = responseData.results[0].chunks[0].content; + console.log('Content:', content); + return { success: true, data: content }; + } + catch(error){ + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + /** * Handle extension messages */ @@ -141,5 +179,20 @@ export default defineBackground(() => { })(); return true; } + + if (message.action === MESSAGE_TYPES.GET_RELATED_MEMORIES) { + (async () => { + try { + const result = await getRelatedMemories(message.data); + sendResponse(result); + } catch (error) { + sendResponse({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + })(); + return true; + } }); }); \ No newline at end of file diff --git a/apps/browser-extension/entrypoints/content.ts b/apps/browser-extension/entrypoints/content.ts index e9a31697..c4cb512b 100644 --- a/apps/browser-extension/entrypoints/content.ts +++ b/apps/browser-extension/entrypoints/content.ts @@ -1,4 +1,10 @@ -import { createTwitterImportButton, createTwitterImportUI, createSaveTweetElement, DOMUtils } from '../utils/ui-components'; +import { + createTwitterImportButton, + createTwitterImportUI, + createSaveTweetElement, + createChatGPTInputBarElement, + DOMUtils, +} from '../utils/ui-components'; import { DOMAINS, ELEMENT_IDS, MESSAGE_TYPES } from '../utils/constants'; export default defineContentScript({ @@ -23,6 +29,7 @@ export default defineContentScript({ const observer = new MutationObserver(() => { if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { addSupermemoryButtonToMemoriesDialog(); + addSaveChatGPTElementBeforeComposerBtn(); } if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) { addTwitterImportButton(); @@ -35,16 +42,23 @@ export default defineContentScript({ subtree: true, }); - if (window.location.hostname === 'chatgpt.com' || window.location.hostname === 'chat.openai.com') { + if ( + window.location.hostname === 'chatgpt.com' || + window.location.hostname === 'chat.openai.com' + ) { addSupermemoryButtonToMemoriesDialog(); + addSaveChatGPTElementBeforeComposerBtn(); } - if (window.location.hostname === 'x.com' || window.location.hostname === 'twitter.com') { + if ( + window.location.hostname === 'x.com' || + window.location.hostname === 'twitter.com' + ) { addTwitterImportButton(); //addSaveTweetElement(); } }; - if (window.location.hostname === 'x.com' || window.location.hostname === 'twitter.com') { + if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) { setTimeout(() => { addTwitterImportButton(); // Wait 2 seconds for page to load //addSaveTweetElement(); @@ -88,10 +102,33 @@ export default defineContentScript({ } } + async function getRelatedMemories() { + try { + const userQuery = + document.getElementById('prompt-textarea')?.textContent || ''; + + const response = await browser.runtime.sendMessage({ + action: MESSAGE_TYPES.GET_RELATED_MEMORIES, + data: userQuery, + }); + + + if (response.success && response.data) { + const promptElement = document.getElementById('prompt-textarea'); + if (promptElement) { + const currentContent = promptElement.innerHTML; + promptElement.innerHTML = currentContent + '
' + "Supermemories: " + response.data; + } + } + } catch (error) { + console.error('Error getting related memories:', error); + } + } + 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 && headerText.textContent?.includes('Saved memories')) { @@ -99,27 +136,29 @@ export default defineContentScript({ break; } } - + if (!memoriesDialog) return; - + if (memoriesDialog.querySelector('#supermemory-save-button')) return; - - const deleteAllContainer = memoriesDialog.querySelector('.mt-5.flex.justify-end'); + + 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; @@ -131,36 +170,41 @@ export default defineContentScript({ 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); + + deleteAllContainer.insertBefore( + supermemoryButton, + deleteAllContainer.firstChild + ); } async function saveMemoriesToSupermemory() { try { DOMUtils.showToast('loading'); - - const memoriesTable = document.querySelector('[role="dialog"] table tbody'); + + 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 => { + + memoryRows.forEach((row) => { const memoryCell = row.querySelector('td .py-2.whitespace-pre-wrap'); if (memoryCell && memoryCell.textContent) { memories.push(memoryCell.textContent.trim()); @@ -168,21 +212,21 @@ export default defineContentScript({ }); 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: 'saveMemory', data: { html: combinedContent, }, }); - + if (response.success) { DOMUtils.showToast('success'); } else { @@ -194,7 +238,6 @@ export default defineContentScript({ } } - function addTwitterImportButton() { if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { return; @@ -207,7 +250,7 @@ export default defineContentScript({ const button = createTwitterImportButton(() => { showTwitterImportUI(); }); - + document.body.appendChild(button); } @@ -217,23 +260,25 @@ export default defineContentScript({ } isTwitterImportOpen = true; - + // Check if user is authenticated browser.storage.local.get(['bearerToken'], ({ bearerToken }) => { const isAuthenticated = !!bearerToken; - + twitterImportUI = createTwitterImportUI( hideTwitterImportUI, async () => { try { - await browser.runtime.sendMessage({ type: MESSAGE_TYPES.BATCH_IMPORT_ALL }); + await browser.runtime.sendMessage({ + type: MESSAGE_TYPES.BATCH_IMPORT_ALL, + }); } catch (error) { console.error('Error starting import:', error); } }, isAuthenticated ); - + document.body.appendChild(twitterImportUI); }); } @@ -251,7 +296,7 @@ export default defineContentScript({ const statusDiv = twitterImportUI.querySelector('#twitter-import-status'); const button = twitterImportUI.querySelector('#twitter-import-button'); - + if (message.type === 'import-update') { if (statusDiv) { statusDiv.innerHTML = ` @@ -266,7 +311,7 @@ export default defineContentScript({ (button as HTMLButtonElement).textContent = 'Importing...'; } } - + if (message.type === 'import-done') { if (statusDiv) { statusDiv.innerHTML = ` @@ -276,28 +321,88 @@ export default defineContentScript({ `; } - + setTimeout(() => { hideTwitterImportUI(); }, 3000); } } + function addSaveChatGPTElementBeforeComposerBtn() { + if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { + return; + } + + 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 getRelatedMemories(); + }); + + 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); + }); + } + // TODO: Add Tweet Capture Functionality function addSaveTweetElement() { if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { return; } - const targetDivs = document.querySelectorAll('div.css-175oi2r.r-18u37iz.r-1h0z5md.r-1wron08'); - + const targetDivs = document.querySelectorAll( + 'div.css-175oi2r.r-18u37iz.r-1h0z5md.r-1wron08' + ); + targetDivs.forEach((targetDiv) => { if (targetDiv.hasAttribute('data-supermemory-icon-added')) { return; } const previousElement = targetDiv.previousElementSibling; - if (previousElement && previousElement.id && previousElement.id.startsWith(ELEMENT_IDS.SAVE_TWEET_ELEMENT)) { + if ( + previousElement && + previousElement.id && + previousElement.id.startsWith(ELEMENT_IDS.SAVE_TWEET_ELEMENT) + ) { targetDiv.setAttribute('data-supermemory-icon-added', 'true'); return; } @@ -305,11 +410,11 @@ export default defineContentScript({ const saveTweetElement = createSaveTweetElement(async () => { await saveMemory(); }); - + saveTweetElement.id = `${ELEMENT_IDS.SAVE_TWEET_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; - + targetDiv.setAttribute('data-supermemory-icon-added', 'true'); - + targetDiv.parentNode?.insertBefore(saveTweetElement, targetDiv); }); } diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts index dad6530c..184a3cb7 100644 --- a/apps/browser-extension/utils/constants.ts +++ b/apps/browser-extension/utils/constants.ts @@ -30,6 +30,7 @@ export const ELEMENT_IDS = { SUPERMEMORY_TOAST: 'supermemory-toast', SUPERMEMORY_SAVE_BUTTON: 'supermemory-save-button', SAVE_TWEET_ELEMENT: 'supermemory-save-tweet-element', + CHATGPT_INPUT_BAR_ELEMENT: 'supermemory-chatgpt-input-bar-element', } as const; /** @@ -68,6 +69,7 @@ export const MESSAGE_TYPES = { BATCH_IMPORT_ALL: 'batchImportAll', IMPORT_UPDATE: 'import-update', IMPORT_DONE: 'import-done', + GET_RELATED_MEMORIES: 'getRelatedMemories', } as const; export const CONTEXT_MENU_IDS = { diff --git a/apps/browser-extension/utils/ui-components.ts b/apps/browser-extension/utils/ui-components.ts index 062b809b..73422d55 100644 --- a/apps/browser-extension/utils/ui-components.ts +++ b/apps/browser-extension/utils/ui-components.ts @@ -287,6 +287,49 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement { return iconButton; } +/** + * Creates a save element button for ChatGPT input bar + * @param onClick - Click handler for the button + * @returns HTMLElement - The save button element + */ +export function createChatGPTInputBarElement(onClick: () => void): HTMLElement { + const iconButton = document.createElement('div'); + iconButton.style.cssText = ` + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + cursor: pointer; + transition: opacity 0.2s ease; + border-radius: 50%; + `; + + // Use appropriate icon based on theme + const isDark = DOMUtils.isDarkMode(); + const iconFileName = isDark ? '/dark-mode-icon.png' : '/light-mode-icon.png'; + const iconUrl = browser.runtime.getURL(iconFileName); + iconButton.innerHTML = ` + Save to Memory + `; + + iconButton.addEventListener('mouseenter', () => { + iconButton.style.opacity = '0.8'; + }); + + iconButton.addEventListener('mouseleave', () => { + iconButton.style.opacity = '1'; + }); + + iconButton.addEventListener('click', (event) => { + event.stopPropagation(); + event.preventDefault(); + onClick(); + }); + + return iconButton; +} + /** * Utility functions for DOM manipulation */ @@ -300,6 +343,16 @@ export const DOMUtils = { return domains.includes(window.location.hostname); }, + /** + * Detect if the page is in dark mode based on color-scheme style + * @returns boolean - true if dark mode, false if light mode + */ + isDarkMode(): boolean { + const htmlElement = document.documentElement; + const style = htmlElement.getAttribute('style'); + return style?.includes('color-scheme: dark') || false; + }, + /** * Check if element exists in DOM * @param id - Element ID to check diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts index d71bd0fc..8efd7a53 100644 --- a/apps/browser-extension/wxt.config.ts +++ b/apps/browser-extension/wxt.config.ts @@ -19,6 +19,8 @@ export default defineConfig({ '*://twitter.com/*', '*://supermemory.ai/*', '*://api.supermemory.ai/*', + '*://chatgpt.com/*', + '*://chat.openai.com/*', ], web_accessible_resources: [ { diff --git a/bun.lockb b/bun.lockb index e20c9c8d..8454fa3d 100755 Binary files a/bun.lockb and b/bun.lockb differ