Repository: deshraj/claude-memory Branch: main Commit: 54a882ab6f25 Files: 43 Total size: 726.5 KB Directory structure: gitextract_b1wvur0_/ ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── docs/ │ └── types.md ├── manifest.json ├── package.json ├── privacy-policy.md ├── src/ │ ├── background.ts │ ├── chatgpt/ │ │ └── content.ts │ ├── claude/ │ │ └── content.ts │ ├── context-menu-memory.ts │ ├── deepseek/ │ │ └── content.ts │ ├── direct-url-tracker.ts │ ├── gemini/ │ │ └── content.ts │ ├── grok/ │ │ └── content.ts │ ├── mem0/ │ │ └── content.ts │ ├── perplexity/ │ │ └── content.ts │ ├── popup.html │ ├── popup.ts │ ├── replit/ │ │ └── content.ts │ ├── search_tracker.ts │ ├── selection_context.ts │ ├── sidebar.ts │ ├── types/ │ │ ├── api.ts │ │ ├── browser.ts │ │ ├── dom.ts │ │ ├── memory.ts │ │ ├── messages.ts │ │ ├── organizations.ts │ │ ├── providers.ts │ │ ├── settings.ts │ │ └── storage.ts │ └── utils/ │ ├── background_search.ts │ ├── llm_prompts.ts │ ├── site_config.ts │ ├── util_functions.ts │ └── util_positioning.ts ├── tsconfig.json └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ dist/ node_modules/ *.min.js icons/ package-lock.json *.log .env* ================================================ FILE: .eslintrc.json ================================================ { "root": true, "env": { "browser": true, "es2022": true, "webextensions": true }, "extends": [ "eslint:recommended", "prettier" ], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, "plugins": ["@typescript-eslint", "prettier", "import"], "rules": { "prettier/prettier": "error", "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-non-null-assertion": "warn", "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-empty-function": "warn", "@typescript-eslint/no-inferrable-types": "off", "no-var": "error", "no-console": "off", "no-debugger": "error", "no-duplicate-imports": "error", "no-unused-expressions": "error", "eqeqeq": ["error", "always"], "curly": ["error", "all"], "import/order": [ "error", { "groups": [ "builtin", "external", "internal", "parent", "sibling", "index" ], "newlines-between": "always", "alphabetize": { "order": "asc", "caseInsensitive": true } } ] }, "ignorePatterns": [ "dist/", "node_modules/", "*.min.js", "icons/" ] } ================================================ FILE: .gitignore ================================================ # Node modules node_modules/ # Build output dist/ build/ # Logs npm-debug.log* yarn-debug.log* yarn-error.log* # OS generated files .DS_Store Thumbs.db # Environment files .env .env.local .env.*.local # Chrome extension specific *.crx *.pem # IDE files .vscode/ .idea/ *.sublime-workspace *.sublime-project ================================================ FILE: .prettierignore ================================================ dist/ node_modules/ *.min.js icons/ package-lock.json *.log .env* ================================================ FILE: .prettierrc ================================================ { "semi": true, "trailingComma": "es5", "singleQuote": true, "printWidth": 100, "tabWidth": 2, "useTabs": false, "bracketSpacing": true, "arrowParens": "avoid", "endOfLine": "lf", "quoteProps": "as-needed", "bracketSameLine": false, "proseWrap": "preserve", "overrides": [ { "files": "*.html", "options": { "parser": "html" } } ] } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 Deshraj Yadav Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ > [!CAUTION] > ## This project has been archived > > **mem0-chrome-extension** is no longer actively maintained and this repository is now a public archive. > > Thank you to the 600+ stargazers, 97 forkers, and every contributor who helped shape this project. Your support and feedback meant the world to us. > > **Looking for memory across your AI assistants?** Check out [Mem0](https://www.mem0.ai) for the latest. > > You're welcome to fork this repo and build on it — the MIT license still applies. # Mem0 Chrome Extension — Cross-LLM Memory Mem0 brings ChatGPT-style memory to all your favorite AI assistants. Share context seamlessly across ChatGPT, Claude, Perplexity, and more, making your AI interactions more personalized and efficient. Chrome logo Add to Chrome, It's Free

Built using [Mem0](https://www.mem0.ai) ❤️ ## Demo Watch the Mem0 Chrome Extension in action (full-resolution video available [here](https://www.youtube.com/watch?v=cByzXztn-YY)): https://github.com/user-attachments/assets/a069a178-631e-4b35-a182-9f4fef7735c4 ## Features - **Universal Memory Layer:** Share context across ChatGPT, Claude, Perplexity, and more - **Smart Context Detection:** Automatically captures relevant information from your conversations - **Intelligent Memory Retrieval:** Surfaces relevant memories at the right time - **One-click sync** with existing ChatGPT memories - **Memory dashboard** to manage all memories ## Installation > **Note:** Make sure you have [Node.js](https://nodejs.org/) installed before proceeding. 1. Clone this repository. 2. Navigate to the directory where you cloned the repository. 3. Run `npm install` to install dependencies. 4. Run `npm run build` to build the extension. 5. The built files will be in the `dist` directory. 6. Open Google Chrome and navigate to `chrome://extensions`. 7. Enable "Developer mode" in the top right corner. 8. Click "Load unpacked" and select the `dist` directory containing the extension files. 9. The Mem0 Chrome Extension should now appear in your Chrome toolbar. ## Usage 1. After installation, look for the Mem0 icon in your Chrome toolbar 2. Sign in with Google 3. Start chatting with any supported AI assistant 4. For ChatGPT and Perplexity, just press enter while chatting as you would normally 5. On Claude, click the Mem0 button or use shortcut ^ + M ## ❤️ Free to Use Mem0 is completely free with: - No usage limits - No ads - All features included ## Configuration - API Key: Required for connecting to the Mem0 API. Obtain this from your Mem0 Dashboard. - User ID: Your unique identifier in the Mem0 system. If not provided, it defaults to 'chrome-extension-user'. ## Troubleshooting If you encounter any issues: - Check your internet connection - Verify you're signed in correctly - Clear your browser cache if needed - Contact support if issues persist ## Privacy and Data Security Your messages are sent to the Mem0 API for extracting and retrieving memories. ## Contributing Contributions to improve Mem0 Chrome Extension are welcome. Please feel free to submit pull requests or open issues for bugs and feature requests. ## License MIT License ================================================ FILE: docs/types.md ================================================ # Types Documentation This section contains TypeScript type definitions for the Mem0 Chrome Extension. The types are organized into several files based on their functionality. ## Overview ### `types/api.ts` Defines types for API interactions: - `MessageRole` - Enum for message roles (User, Assistant) - `ApiMessage` - Message structure with role and content - `ApiMemoryRequest` - Request payload for memory API calls - `MemorySearchResponse` - Array of memory search results - `LoginData` - User authentication data structure - `DEFAULT_USER_ID` - Default user ID constant - `SOURCE` - Extension source identifier ### `types/browser.ts` Browser-specific type definitions: - `OnCommittedDetails` - Web navigation event details - `JsonPrimitive` - JSON primitive value types - `JsonValue` - Recursive JSON value type - `JsonObject` - JSON object structure - `HistoryStateData` - Browser history state data - `HistoryUrl` - Browser history URL type ### `types/dom.ts` DOM-related type extensions and global declarations: - `ExtendedHTMLElement` - Extended HTML element with cleanup methods - `ExtendedDocument` - Extended document with mem0-specific properties - `ExtendedElement` - Extended element with additional properties - `ModalDimensions` - Modal size and pagination settings - `ModalPosition` - Modal positioning coordinates - `MutableMutationObserver` - Extended mutation observer with timers - Global interface extensions for `Element`, `CSSStyleDeclaration`, and `Window` ### `types/memory.ts` Core memory-related types: - `MemoryItem` - Individual memory structure with id, text, and categories - `Memory` - Simplified memory structure - `MemorySearchItem` - Search result item from API - `MemoriesResponse` - API response wrapper for memories - `OpenMemoryPrompts` - Prompt templates and regex patterns - `OptionalApiParams` - Optional parameters for API calls (org_id, project_id) ### `types/messages.ts` Message passing between extension components: - `ToastVariant` - Enum for toast notification types (SUCCESS, ERROR) - `MessageType` - Enum for different message types - `SidebarAction` - Enum for sidebar actions (TOGGLE_SIDEBAR, OPEN_POPUP, etc.) - `SelectionContextPayload` - Payload for selection context messages - `SelectionContextResponse` - Response structure for selection context - `GetSelectionContextMessage` - Message type for getting selection context - `ToastMessage` - Toast notification message structure - `SelectionContextMessage` - Union type for selection context messages - `SendResponse` - Response callback type - Various sidebar action message types ### `types/organizations.ts` Organization and project management: - `Organization` - Organization structure with org_id and name - `Project` - Project structure with project_id and name ### `types/providers.ts` AI provider definitions: - `Provider` - Enum for supported AI providers - `Category` - Enum for memory categories (BOOKMARK, NAVIGATION, SEARCH) ### `types/settings.ts` User settings and preferences: - `UserSettings` - User preference structure with API keys, memory settings, and thresholds - `SidebarSettings` - Sidebar-specific settings with organization and project info - `Settings` - Legacy settings structure for compatibility ### `types/storage.ts` Chrome storage key definitions: - `StorageKey` - Enum for all storage keys used in the extension - `StorageItems` - Type mapping for storage values (required fields) - `StorageData` - Type mapping for storage values (optional fields) ================================================ FILE: manifest.json ================================================ { "manifest_version": 3, "name": "OpenMemory", "version": "1.3.17", "description": "🧠 OpenMemory keeps your conversations in sync. 🔄 No more repeating yourself—just seamless AI collaboration! ✨", "icons": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" }, "permissions": ["storage", "activeTab", "contextMenus", "scripting", "webNavigation"], "host_permissions": [ "https://api.mem0.ai/*", "https://app.mem0.ai/*", "https://claude.ai/*" ], "action": { "default_popup": "src/popup.html", "default_icon": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" } }, "background": { "service_worker": "src/background.ts", "type": "module" }, "content_scripts": [ { "matches": ["https://claude.ai/*"], "js": ["src/claude/content.ts"] }, { "matches": ["https://chat.openai.com/*", "https://chatgpt.com/*"], "js": ["src/chatgpt/content.ts"] }, { "matches": ["https://www.perplexity.ai/*"], "js": ["src/perplexity/content.ts"] }, { "matches": ["https://app.mem0.ai/*"], "js": ["src/mem0/content.ts"] }, { "matches": ["https://grok.com/*", "https://x.com/i/grok*"], "js": ["src/grok/content.ts"] }, { "matches": ["https://chat.deepseek.com/*"], "js": ["src/deepseek/content.ts"] }, { "matches": ["https://gemini.google.com/*"], "js": ["src/gemini/content.ts"] }, { "matches": ["https://replit.com/*"], "js": ["src/replit/content.ts"] }, { "matches": [""], "js": ["src/sidebar.ts"], "run_at": "document_end" }, { "matches": [""], "js": ["src/selection_context.ts"], "run_at": "document_idle", "all_frames": true } , { "matches": [""], "js": ["src/search_tracker.ts"], "run_at": "document_idle" } ], "web_accessible_resources": [ { "resources": ["icons/*"], "matches": [""] } ] } ================================================ FILE: package.json ================================================ { "name": "mem0-extension", "description": "🧠 OpenMemory keeps your conversations in sync. 🔄 No more repeating yourself—just seamless AI collaboration! ✨", "private": true, "type": "module", "scripts": { "build": "vite build", "lint": "eslint src --ext .ts,.tsx --fix", "lint:check": "eslint src --ext .ts,.tsx", "format": "prettier --write src/**/*.{ts,html}", "format:check": "prettier --check src/**/*.{ts,html}", "type-check": "tsc --noEmit" }, "devDependencies": { "@crxjs/vite-plugin": "^2.2.0", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", "chrome-types": "^0.1.375", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-prettier": "^5.1.3", "prettier": "^3.3.3", "typescript": "^5.9.2", "vite": "^7.1.4" } } ================================================ FILE: privacy-policy.md ================================================ # Privacy Policy for Claude Memory Last updated: 09-06-2024 ## Introduction Welcome to Claude Memory ("we", "our", or "us"). We are committed to protecting your personal information and your right to privacy. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our Chrome extension. ## Information We Collect We may collect the following types of information: Personal Information: Full name and email address through which you sign up on the Mem0 platform. Usage Data: Every time you use the extension, we collect the message that you are sending to Claude. ## How We Use Your Information We use the collected information for various purposes, including: - To provide and maintain our extension - To notify you about changes to our extension - To provide customer support - To gather analysis or valuable information so that we can improve our extension - To monitor the usage of our extension - To detect, prevent and address technical issues ## Data Retention We will retain your information for as long as necessary to fulfill the purposes outlined in this Privacy Policy, unless a longer retention period is required or permitted by law. ## Data Security We use commercially acceptable means to protect your personal information. However, no method of transmission over the Internet or method of electronic storage is 100% secure. ## Your Data Protection Rights Depending on your location, you may have certain rights regarding your personal information, such as the right to access, correct, or delete your data. Please contact us to exercise these rights. ## Changes to This Privacy Policy We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the "Last updated" date. ## Contact Us If you have any questions about this Privacy Policy, please contact us at: deshraj@mem0.ai ================================================ FILE: src/background.ts ================================================ import { initContextMenuMemory } from './context-menu-memory'; import { initDirectUrlTracking } from './direct-url-tracker'; import { type OpenDashboardMessage, SidebarAction } from './types/messages'; chrome.runtime.onInstalled.addListener(() => { chrome.storage.sync.set({ memory_enabled: true }, () => { console.log('Memory enabled set to true on install/update'); }); }); chrome.runtime.onMessage.addListener((request: OpenDashboardMessage) => { if (request.action === SidebarAction.OPEN_DASHBOARD && request.url) { chrome.tabs.create({ url: request.url }); } return undefined; }); chrome.runtime.onMessage.addListener( (request: { action?: string }, sender: chrome.runtime.MessageSender) => { if (request.action === SidebarAction.SIDEBAR_SETTINGS) { const tabId = sender.tab?.id; if (tabId !== null && tabId !== undefined) { chrome.tabs.sendMessage(tabId, { action: SidebarAction.SIDEBAR_SETTINGS }); } } return undefined; } ); initContextMenuMemory(); initDirectUrlTracking(); ================================================ FILE: src/chatgpt/content.ts ================================================ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { DEFAULT_USER_ID, type LoginData, MessageRole } from '../types/api'; import type { HistoryStateData } from '../types/browser'; import type { MemoryItem, MemorySearchItem, OptionalApiParams } from '../types/memory'; import { SidebarAction } from '../types/messages'; import { type StorageData, StorageKey } from '../types/storage'; import { createOrchestrator, type SearchStorage } from '../utils/background_search'; import { OPENMEMORY_PROMPTS } from '../utils/llm_prompts'; import { SITE_CONFIG } from '../utils/site_config'; import { getBrowser, sendExtensionEvent } from '../utils/util_functions'; import { OPENMEMORY_UI } from '../utils/util_positioning'; export {}; let isProcessingMem0: boolean = false; // Initialize the MutationObserver variable let observer: MutationObserver; let memoryModalShown: boolean = false; // Global variable to store all memories let allMemories: string[] = []; // Track added memories by ID const allMemoriesById: Set = new Set(); // Reference to the modal overlay for updates let currentModalOverlay: HTMLDivElement | null = null; // Store dragged position let draggedPosition: { top: number; left: number } | null = null; let inputValueCopy: string = ''; let currentModalSourceButtonId: string | null = null; const chatgptSearch = createOrchestrator({ fetch: async function (query: string, opts: { signal?: AbortSignal }) { const data = await new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, StorageKey.SIMILARITY_THRESHOLD, StorageKey.TOP_K, ], function (items) { resolve(items as SearchStorage); } ); }); const apiKey = data[StorageKey.API_KEY]; const accessToken = data[StorageKey.ACCESS_TOKEN]; if (!apiKey && !accessToken) { return []; } const authHeader = accessToken ? `Bearer ${accessToken}` : `Token ${apiKey}`; const userId = data[StorageKey.USER_ID_CAMEL] || data[StorageKey.USER_ID] || 'chrome-extension-user'; const threshold = data[StorageKey.SIMILARITY_THRESHOLD] !== undefined ? data[StorageKey.SIMILARITY_THRESHOLD] : 0.1; const topK = data[StorageKey.TOP_K] !== undefined ? data[StorageKey.TOP_K] : 10; const optionalParams: OptionalApiParams = {}; if (data[StorageKey.SELECTED_ORG]) { optionalParams.org_id = data[StorageKey.SELECTED_ORG]; } if (data[StorageKey.SELECTED_PROJECT]) { optionalParams.project_id = data[StorageKey.SELECTED_PROJECT]; } const payload = { query, filters: { user_id: userId }, rerank: true, threshold: threshold, top_k: topK, filter_memories: false, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }; const res = await fetch('https://api.mem0.ai/v2/memories/search/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify(payload), signal: opts && opts.signal, }); if (!res.ok) { throw new Error(`API request failed with status ${res.status}`); } return await res.json(); }, // Don't render on prefetch. When modal is open, update it. onSuccess: function (normQuery: string, responseData: MemorySearchItem[]) { if (!memoryModalShown) { return; } const memoryItems = ((responseData as MemorySearchItem[]) || []).map( (item: MemorySearchItem) => ({ id: String(item.id), text: item.memory, categories: item.categories || [], }) ); createMemoryModal(memoryItems, false, currentModalSourceButtonId); }, onError: function () { if (memoryModalShown) { createMemoryModal([], false, currentModalSourceButtonId); } }, minLength: 3, debounceMs: 75, cacheTTL: 60000, }); function createMemoryModal( memoryItems: MemoryItem[], isLoading: boolean = false, sourceButtonId: string | null = null ): void { // Close existing modal if it exists if (memoryModalShown && currentModalOverlay) { document.body.removeChild(currentModalOverlay); } memoryModalShown = true; let currentMemoryIndex = 0; // Calculate modal dimensions (estimated) const modalWidth = 447; let modalHeight = 400; // Default height let memoriesPerPage = 3; // Default number of memories per page let topPosition: number = 0; let leftPosition: number = 0; // Use dragged position if available, otherwise calculate based on button if (draggedPosition) { topPosition = draggedPosition.top; leftPosition = draggedPosition.left; } else if (sourceButtonId === 'mem0-icon-button') { // Position relative to the mem0-icon-button (in the input area) const iconButton = document.querySelector('#mem0-icon-button'); if (iconButton) { const buttonRect = iconButton.getBoundingClientRect(); // Determine if there's enough space above the button const spaceAbove = buttonRect.top; const viewportHeight = window.innerHeight; // Calculate position - for icon button, prefer to show ABOVE leftPosition = buttonRect.left - modalWidth + buttonRect.width; // Make sure modal doesn't go off-screen to the left leftPosition = Math.max(leftPosition, 10); // For icon button, show above if enough space, otherwise below if (spaceAbove >= modalHeight + 10) { // Place above topPosition = buttonRect.top - modalHeight - 10; } else { // Not enough space above, place below topPosition = buttonRect.bottom + 10; // Check if it's in the lower half of the screen if (buttonRect.bottom > viewportHeight / 2) { modalHeight = 300; // Reduced height memoriesPerPage = 2; // Show only 2 memories } } } else { // Fallback to input-based positioning positionRelativeToInput(); } } else if (sourceButtonId === 'sync-button') { // Position relative to the sync button const syncButton = document.querySelector('#sync-button'); if (syncButton) { const buttonRect = syncButton.getBoundingClientRect(); const viewportHeight = window.innerHeight; // Position below the sync button by default leftPosition = buttonRect.left; topPosition = buttonRect.bottom + 10; // Check if it's in the lower half of the screen if (buttonRect.bottom > viewportHeight / 2) { modalHeight = 300; // Reduced height memoriesPerPage = 2; // Show only 2 memories } // Make sure modal doesn't go off-screen to the right leftPosition = Math.min(leftPosition, window.innerWidth - modalWidth - 10); } else { // Fallback to input-based positioning positionRelativeToInput(); } } else { // Default positioning relative to the input field positionRelativeToInput(); } // Helper function to position modal relative to input field function positionRelativeToInput() { const inputElement = document.querySelector('#prompt-textarea') || document.querySelector('div[contenteditable="true"]') || document.querySelector('textarea'); if (!inputElement) { console.error('Input element not found'); return; } // Get the position and dimensions of the input field const inputRect = inputElement.getBoundingClientRect(); // Determine if there's enough space below the input field const viewportHeight = window.innerHeight; const spaceBelow = viewportHeight - inputRect.bottom; // Position the modal aligned to the right of the input leftPosition = Math.max(inputRect.right - 20 - modalWidth, 10); // 20px offset from right edge // Decide whether to place modal above or below based on available space if (spaceBelow >= modalHeight) { // Place below the input topPosition = inputRect.bottom + 10; // Check if it's in the lower half of the screen if (inputRect.bottom > viewportHeight / 2) { modalHeight = 300; // Reduced height memoriesPerPage = 2; // Show only 2 memories } } else { // Place above the input if not enough space below topPosition = inputRect.top - modalHeight - 10; } } // Create modal overlay const modalOverlay = document.createElement('div'); modalOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: transparent; display: flex; z-index: 10000; pointer-events: auto; `; // Save reference to current modal overlay currentModalOverlay = modalOverlay; // Add event listener to close modal when clicking outside modalOverlay.addEventListener('click', event => { // Only close if clicking directly on the overlay, not its children if (event.target === modalOverlay) { closeModal(); } }); // Create modal container with positioning const modalContainer = document.createElement('div'); modalContainer.style.cssText = ` background-color: #1C1C1E; border-radius: 12px; width: ${modalWidth}px; height: ${modalHeight}px; display: flex; flex-direction: column; color: white; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); position: absolute; top: ${topPosition}px; left: ${leftPosition}px; pointer-events: auto; border: 1px solid #27272A; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; overflow: hidden; `; // Create modal header const modalHeader = document.createElement('div'); modalHeader.style.cssText = ` display: flex; align-items: center; padding: 10px 16px; justify-content: space-between; background-color: #232325; flex-shrink: 0; cursor: move; user-select: none; `; // Create header left section with just the logo const headerLeft = document.createElement('div'); headerLeft.style.cssText = ` display: flex; flex-direction: row; align-items: center; `; // Add Mem0 logo (updated to SVG) const logoImg = document.createElement('img'); logoImg.src = chrome.runtime.getURL('icons/mem0-claude-icon.png'); logoImg.style.cssText = ` width: 26px; height: 26px; border-radius: 50%; margin-right: 8px; `; // Add "OpenMemory" title const title = document.createElement('div'); title.textContent = 'OpenMemory'; title.style.cssText = ` font-size: 16px; font-weight: 600; color: white; `; // Create header right section const headerRight = document.createElement('div'); headerRight.style.cssText = ` display: flex; flex-direction: row; align-items: center; gap: 8px; `; // Create Add to Prompt button with arrow const addToPromptBtn = document.createElement('button'); addToPromptBtn.style.cssText = ` display: flex; flex-direction: row; align-items: center; padding: 5px 16px; gap: 8px; background-color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 12px; font-weight: 600; color: black; `; addToPromptBtn.textContent = 'Add to Prompt'; // Add arrow icon to button const arrowIcon = document.createElement('span'); arrowIcon.innerHTML = ` `; addToPromptBtn.appendChild(arrowIcon); // (Removed) LLM button – auto-rerank is now handled on modal open // Create settings button const settingsBtn = document.createElement('button'); settingsBtn.style.cssText = ` background: none; border: none; cursor: pointer; padding: 8px; opacity: 0.6; transition: opacity 0.2s; `; settingsBtn.innerHTML = ` `; // Add click event to open app.mem0.ai in a new tab settingsBtn.addEventListener('click', () => { if (currentModalOverlay && document.body.contains(currentModalOverlay)) { document.body.removeChild(currentModalOverlay); memoryModalShown = false; currentModalOverlay = null; } chrome.runtime.sendMessage({ action: SidebarAction.SIDEBAR_SETTINGS }); }); // Add hover effect for the settings button settingsBtn.addEventListener('mouseenter', () => { settingsBtn.style.opacity = '1'; }); settingsBtn.addEventListener('mouseleave', () => { settingsBtn.style.opacity = '0.6'; }); // Add drag functionality to modal header let isDragging = false; const dragOffset = { x: 0, y: 0 }; modalHeader.addEventListener('mousedown', (e: MouseEvent) => { // Don't start dragging if clicking on buttons const target = e.target as HTMLElement; if (target?.closest('button')) { return; } isDragging = true; modalHeader.style.cursor = 'grabbing'; const modalRect = modalContainer.getBoundingClientRect(); dragOffset.x = e.clientX - modalRect.left; dragOffset.y = e.clientY - modalRect.top; e.preventDefault(); }); document.addEventListener('mousemove', e => { if (!isDragging) { return; } const newLeft = e.clientX - dragOffset.x; const newTop = e.clientY - dragOffset.y; // Constrain to viewport const maxLeft = window.innerWidth - modalWidth; const maxTop = window.innerHeight - modalHeight; const constrainedLeft = Math.max(0, Math.min(newLeft, maxLeft)); const constrainedTop = Math.max(0, Math.min(newTop, maxTop)); modalContainer.style.left = `${constrainedLeft}px`; modalContainer.style.top = `${constrainedTop}px`; // Store the dragged position draggedPosition = { left: constrainedLeft, top: constrainedTop, }; e.preventDefault(); }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; modalHeader.style.cursor = 'move'; } }); // Content section const contentSection = document.createElement('div'); const contentSectionHeight = modalHeight - 130; // Account for header and navigation contentSection.style.cssText = ` display: flex; flex-direction: column; padding: 0 16px; gap: 12px; overflow: hidden; flex: 1; height: ${contentSectionHeight}px; `; // Create memories counter const memoriesCounter = document.createElement('div'); memoriesCounter.style.cssText = ` font-size: 16px; font-weight: 600; color: #FFFFFF; margin-top: 16px; flex-shrink: 0; `; // Update counter text based on loading state and number of memories if (isLoading) { memoriesCounter.textContent = `Loading Relevant Memories...`; } else { memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`; } // Calculate max height for memories content based on modal height const memoriesContentMaxHeight = contentSectionHeight - 40; // Account for memories counter // Create memories content container with adjusted height const memoriesContent = document.createElement('div'); memoriesContent.style.cssText = ` display: flex; flex-direction: column; gap: 8px; overflow-y: auto; flex: 1; max-height: ${memoriesContentMaxHeight}px; padding-right: 8px; margin-right: -8px; scrollbar-width: none; -ms-overflow-style: none; `; memoriesContent.style.cssText += '::-webkit-scrollbar { display: none; }'; // Track currently expanded memory let currentlyExpandedMemory: HTMLElement | null = null; // Function to create skeleton loading items (adjusted for different heights) function createSkeletonItems() { memoriesContent.innerHTML = ''; for (let i = 0; i < memoriesPerPage; i++) { const skeletonItem = document.createElement('div'); skeletonItem.style.cssText = ` display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; padding: 12px; background-color: #27272A; border-radius: 8px; height: 72px; flex-shrink: 0; animation: pulse 1.5s infinite ease-in-out; `; const skeletonText = document.createElement('div'); skeletonText.style.cssText = ` background-color: #383838; border-radius: 4px; height: 14px; width: 85%; margin-bottom: 8px; `; const skeletonText2 = document.createElement('div'); skeletonText2.style.cssText = ` background-color: #383838; border-radius: 4px; height: 14px; width: 65%; `; const skeletonActions = document.createElement('div'); skeletonActions.style.cssText = ` display: flex; gap: 4px; margin-left: 10px; `; const skeletonButton1 = document.createElement('div'); skeletonButton1.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; background-color: #383838; `; const skeletonButton2 = document.createElement('div'); skeletonButton2.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; background-color: #383838; `; skeletonActions.appendChild(skeletonButton1); skeletonActions.appendChild(skeletonButton2); const textContainer = document.createElement('div'); textContainer.style.cssText = ` display: flex; flex-direction: column; flex-grow: 1; `; textContainer.appendChild(skeletonText); textContainer.appendChild(skeletonText2); skeletonItem.appendChild(textContainer); skeletonItem.appendChild(skeletonActions); memoriesContent.appendChild(skeletonItem); } // Add keyframe animation to document if not exists if (!document.getElementById('skeleton-animation')) { const style = document.createElement('style'); style.id = 'skeleton-animation'; style.innerHTML = ` @keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 0.8; } 100% { opacity: 0.6; } } `; document.head.appendChild(style); } } // Function to expand memory function expandMemory( memoryContainer: HTMLDivElement, memoryText: HTMLDivElement, contentWrapper: HTMLDivElement, removeButton: HTMLButtonElement, isExpanded: { value: boolean } ) { if (currentlyExpandedMemory && currentlyExpandedMemory !== memoryContainer) { currentlyExpandedMemory.dispatchEvent(new Event('collapse')); } isExpanded.value = true; memoryText.style.webkitLineClamp = 'unset'; memoryText.style.height = 'auto'; contentWrapper.style.overflowY = 'auto'; contentWrapper.style.maxHeight = '240px'; // Limit height to prevent overflow contentWrapper.style.scrollbarWidth = 'none'; // contentWrapper.style.msOverflowStyle is non-standard; omit to satisfy TS contentWrapper.style.cssText += '::-webkit-scrollbar { display: none; }'; memoryContainer.style.backgroundColor = '#1C1C1E'; memoryContainer.style.maxHeight = '300px'; // Allow expansion but within container memoryContainer.style.overflow = 'hidden'; removeButton.style.display = 'flex'; currentlyExpandedMemory = memoryContainer; // Scroll to make expanded memory visible if needed memoriesContent.scrollTop = memoryContainer.offsetTop - memoriesContent.offsetTop; } // Function to collapse memory function collapseMemory( memoryContainer: HTMLDivElement, memoryText: HTMLDivElement, contentWrapper: HTMLDivElement, removeButton: HTMLButtonElement, isExpanded: { value: boolean } ) { isExpanded.value = false; memoryText.style.webkitLineClamp = '2'; memoryText.style.height = '42px'; contentWrapper.style.overflowY = 'visible'; memoryContainer.style.backgroundColor = '#27272A'; memoryContainer.style.maxHeight = '72px'; memoryContainer.style.overflow = 'hidden'; removeButton.style.display = 'none'; currentlyExpandedMemory = null; } // Function to show memories with adjusted count based on modal position function showMemories() { memoriesContent.innerHTML = ''; if (isLoading) { createSkeletonItems(); return; } if (memoryItems.length === 0) { showEmptyState(); // Disable navigation buttons when there are no memories updateNavigationState(0, 0); return; } // Use the dynamically set memoriesPerPage value const memoriesToShow = Math.min(memoriesPerPage, memoryItems.length); // Calculate total pages and current page const totalPages = Math.ceil(memoryItems.length / memoriesToShow); const currentPage = Math.floor(currentMemoryIndex / memoriesToShow) + 1; // Update navigation buttons state updateNavigationState(currentPage, totalPages); for (let i = 0; i < memoriesToShow; i++) { const memoryIndex = currentMemoryIndex + i; if (memoryIndex >= memoryItems.length) { break; } // Stop if we've reached the end const memory = memoryItems[memoryIndex]!; // Skip memories that have been added already if (allMemoriesById.has(String(memory.id))) { continue; } // Ensure memory has an ID if (!memory.id) { memory.id = `memory-${Date.now()}-${memoryIndex}`; } const memoryContainer = document.createElement('div'); memoryContainer.style.cssText = ` display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; padding: 12px; background-color: #27272A; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; min-height: 72px; max-height: 72px; overflow: hidden; flex-shrink: 0; `; const memoryText = document.createElement('div'); memoryText.style.cssText = ` font-size: 14px; line-height: 1.5; color: #D4D4D8; flex-grow: 1; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; transition: all 0.2s ease; height: 42px; /* Height for 2 lines of text */ `; memoryText.textContent = memory.text || ''; const actionsContainer = document.createElement('div'); actionsContainer.style.cssText = ` display: flex; gap: 4px; margin-left: 10px; flex-shrink: 0; `; // Add button const addButton = document.createElement('button'); addButton.style.cssText = ` border: none; cursor: pointer; padding: 4px; background:rgb(66, 66, 69); color:rgb(199, 199, 201); border-radius: 100%; transition: all 0.2s ease; `; addButton.innerHTML = ` `; // Add click handler for add button addButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); sendExtensionEvent('memory_injection', { provider: 'chatgpt', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), injected_all: false, memory_id: memory.id, }); // Add this memory allMemoriesById.add(String(memory.id)); allMemories.push(String(memory.text || '')); updateInputWithMemories(); // Remove this memory from the list const index = memoryItems.findIndex((m: MemoryItem) => m.id === memory.id); if (index !== -1) { memoryItems.splice(index, 1); // Recalculate pagination after removing an item // If we're on a page that's now empty, go to previous page if (currentMemoryIndex > 0 && currentMemoryIndex >= memoryItems.length) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); } memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`; showMemories(); } }); // Menu button const menuButton = document.createElement('button'); menuButton.style.cssText = ` background: none; border: none; cursor: pointer; padding: 4px; color: #A1A1AA; `; menuButton.innerHTML = ` `; // Track expanded state using object to maintain reference const isExpanded = { value: false }; // Create remove button (hidden by default) const removeButton = document.createElement('button'); removeButton.style.cssText = ` display: none; align-items: center; gap: 6px; background:rgb(66, 66, 69); color:rgb(199, 199, 201); border-radius: 8px; padding: 2px 4px; border: none; cursor: pointer; font-size: 13px; margin-top: 12px; width: fit-content; `; removeButton.innerHTML = ` Remove `; // Create content wrapper for text and remove button const contentWrapper = document.createElement('div'); contentWrapper.style.cssText = ` display: flex; flex-direction: column; flex-grow: 1; `; contentWrapper.appendChild(memoryText); contentWrapper.appendChild(removeButton); memoryContainer.addEventListener('collapse', () => { collapseMemory(memoryContainer, memoryText, contentWrapper, removeButton, isExpanded); }); menuButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); if (isExpanded.value) { collapseMemory(memoryContainer, memoryText, contentWrapper, removeButton, isExpanded); } else { expandMemory(memoryContainer, memoryText, contentWrapper, removeButton, isExpanded); } }); // Add click handler for remove button removeButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); // Remove from memoryItems const index = memoryItems.findIndex((m: MemoryItem) => m.id === memory.id); if (index !== -1) { memoryItems.splice(index, 1); // Recalculate pagination after removing an item // If we're on the last page and it's now empty, go to previous page if (currentMemoryIndex > 0 && currentMemoryIndex >= memoryItems.length) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); } memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`; showMemories(); } }); actionsContainer.appendChild(addButton); actionsContainer.appendChild(menuButton); memoryContainer.appendChild(contentWrapper); memoryContainer.appendChild(actionsContainer); memoriesContent.appendChild(memoryContainer); // Add hover effect memoryContainer.addEventListener('mouseenter', () => { memoryContainer.style.backgroundColor = isExpanded.value ? '#18181B' : '#323232'; }); memoryContainer.addEventListener('mouseleave', () => { memoryContainer.style.backgroundColor = isExpanded.value ? '#1C1C1E' : '#27272A'; }); } // If after filtering for already added memories, there are no items to show, // check if we need to go to previous page if (memoriesContent.children.length === 0 && memoryItems.length > 0) { if (currentMemoryIndex > 0) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); showMemories(); } else { showEmptyState(); } } } // Function to show empty state function showEmptyState(): void { memoriesContent.innerHTML = ''; const emptyContainer = document.createElement('div'); emptyContainer.style.cssText = ` display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 32px 16px; text-align: center; flex: 1; min-height: 200px; `; const emptyIcon = document.createElement('div'); emptyIcon.innerHTML = ` `; emptyIcon.style.marginBottom = '16px'; const emptyText = document.createElement('div'); emptyText.textContent = 'No relevant memories found'; emptyText.style.cssText = ` color: #71717A; font-size: 14px; font-weight: 500; `; emptyContainer.appendChild(emptyIcon); emptyContainer.appendChild(emptyText); memoriesContent.appendChild(emptyContainer); } // Update navigation button states function updateNavigationState(currentPage: number, totalPages: number): void { // If there are no memories or total pages is 0, disable both buttons if (memoryItems.length === 0 || totalPages === 0) { prevButton.disabled = true; prevButton.style.opacity = '0.5'; prevButton.style.cursor = 'not-allowed'; nextButton.disabled = true; nextButton.style.opacity = '0.5'; nextButton.style.cursor = 'not-allowed'; return; } if (currentPage <= 1) { prevButton.disabled = true; prevButton.style.opacity = '0.5'; prevButton.style.cursor = 'not-allowed'; } else { prevButton.disabled = false; prevButton.style.opacity = '1'; prevButton.style.cursor = 'pointer'; } if (currentPage >= totalPages) { nextButton.disabled = true; nextButton.style.opacity = '0.5'; nextButton.style.cursor = 'not-allowed'; } else { nextButton.disabled = false; nextButton.style.opacity = '1'; nextButton.style.cursor = 'pointer'; } } // Navigation section at bottom const navigationSection = document.createElement('div'); navigationSection.style.cssText = ` display: flex; justify-content: center; gap: 12px; padding: 10px; border-top: none; flex-shrink: 0; `; // Navigation buttons const prevButton = document.createElement('button'); prevButton.innerHTML = ` `; prevButton.style.cssText = ` background: #27272A; border: none; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.2s; `; const nextButton = document.createElement('button'); nextButton.innerHTML = ` `; nextButton.style.cssText = prevButton.style.cssText; // Add navigation button handlers prevButton.addEventListener('click', () => { if (currentMemoryIndex >= memoriesPerPage) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); showMemories(); } }); nextButton.addEventListener('click', () => { if (currentMemoryIndex + memoriesPerPage < memoryItems.length) { currentMemoryIndex = currentMemoryIndex + memoriesPerPage; showMemories(); } }); // Add hover effects [prevButton, nextButton].forEach(button => { button.addEventListener('mouseenter', () => { if (!button.disabled) { button.style.backgroundColor = '#323232'; } }); button.addEventListener('mouseleave', () => { if (!button.disabled) { button.style.backgroundColor = '#27272A'; } }); }); // Assemble modal headerLeft.appendChild(logoImg); headerLeft.appendChild(title); headerRight.appendChild(addToPromptBtn); headerRight.appendChild(settingsBtn); // No LLM button; auto-rerank happens below if enabled modalHeader.appendChild(headerLeft); modalHeader.appendChild(headerRight); contentSection.appendChild(memoriesCounter); contentSection.appendChild(memoriesContent); navigationSection.appendChild(prevButton); navigationSection.appendChild(nextButton); modalContainer.appendChild(modalHeader); modalContainer.appendChild(contentSection); modalContainer.appendChild(navigationSection); modalOverlay.appendChild(modalContainer); // Append to body document.body.appendChild(modalOverlay); // Show initial memories showMemories(); // Function to close the modal function closeModal() { if (currentModalOverlay && document.body.contains(currentModalOverlay)) { document.body.removeChild(currentModalOverlay); } currentModalOverlay = null; memoryModalShown = false; // Reset dragged position when modal is explicitly closed draggedPosition = null; } // Update Add to Prompt button click handler addToPromptBtn.addEventListener('click', () => { // Only add memories that are not already added const newMemories = memoryItems .filter(memory => !allMemoriesById.has(String(memory.id)) && !memory.removed) .map(memory => { allMemoriesById.add(String(memory.id)); return String(memory.text || ''); }); sendExtensionEvent('memory_injection', { provider: 'chatgpt', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), injected_all: true, memory_count: newMemories.length, }); // Add all new memories to allMemories allMemories.push(...newMemories); // Update the input with all memories if (allMemories.length > 0) { updateInputWithMemories(); closeModal(); } else { // If no new memories were added but we have existing ones, just close if (allMemoriesById.size > 0) { closeModal(); } } // Remove all added memories from the memoryItems list for (let i = memoryItems.length - 1; i >= 0; i--) { if (allMemoriesById.has(String(memoryItems[i]?.id))) { memoryItems.splice(i, 1); } } }); } // Shared function to update the input field with all collected memories function updateInputWithMemories(): void { const inputElement = document.querySelector('#prompt-textarea') || document.querySelector('div[contenteditable="true"]') || document.querySelector('textarea'); if (inputElement && allMemories.length > 0) { // Get the content without any existing memory wrappers const baseContent = getContentWithoutMemories(); // Create the memory wrapper with all collected memories let memoriesContent = '
'; memoriesContent += OPENMEMORY_PROMPTS.memory_header_html_strong; // Add all memories to the content allMemories.forEach((mem, idx) => { const safe = (mem || '').toString(); memoriesContent += `
- ${safe}
`; }); memoriesContent += '
'; // Add the final content to the input if (inputElement.tagName.toLowerCase() === 'div') { inputElement.innerHTML = `${baseContent}

${memoriesContent}`; } else { (inputElement as HTMLTextAreaElement).value = `${baseContent}\n${memoriesContent}`; } // Make only the wrapper non-editable; allow user to select/copy text inside try { const wrapper = document.getElementById('mem0-wrapper'); if (wrapper) { wrapper.setAttribute('contenteditable', 'false'); wrapper.style.userSelect = 'text'; } } catch { // Ignore errors when setting contenteditable } inputElement.dispatchEvent(new Event('input', { bubbles: true })); } } // Function to get the content without any memory wrappers function getContentWithoutMemories(message?: string): string { if (typeof message === 'string') { return message; } const inputElement = (document.querySelector('#prompt-textarea') as HTMLTextAreaElement | HTMLDivElement) || (document.querySelector('div[contenteditable="true"]') as HTMLDivElement) || (document.querySelector('textarea') as HTMLTextAreaElement); if (!inputElement) { return ''; } let content = (inputElement as HTMLTextAreaElement)?.value || inputElement.textContent || (inputElement as HTMLDivElement).innerHTML; if ( message && (!content || content.trim() === '


') ) { content = message; } // Remove any memory wrappers content = content.replace(/
/g, ''); // Remove any memory headers using shared prompts (HTML and plain variants) try { const MEM0_PLAIN = OPENMEMORY_PROMPTS.memory_header_plain_regex; const MEM0_HTML = OPENMEMORY_PROMPTS.memory_header_html_regex; content = content.replace(MEM0_HTML, ''); content = content.replace(MEM0_PLAIN, ''); } catch { // Ignore errors during re-initialization } // Clean up any leftover paragraph markers content = content.replace(/


<\/p>

$/g, ''); // Replace

with nothing content = content.replace(/

[\s\S]*?<\/p>/g, ''); return content.trim(); } // Add an event listener for the send button to clear memories after sending function addSendButtonListener(): void { const sendButton = document.querySelector('#composer-submit-button') as HTMLButtonElement; if (sendButton && !sendButton.dataset.mem0Listener) { sendButton.dataset.mem0Listener = 'true'; sendButton.addEventListener('click', function () { // Capture and save memory asynchronously captureAndStoreMemory(); // Clear all memories after sending setTimeout(() => { allMemories = []; allMemoriesById.clear(); }, 100); }); // Also handle Enter key press const inputElement = (document.querySelector('#prompt-textarea') as HTMLTextAreaElement | HTMLDivElement) || (document.querySelector('div[contenteditable="true"]') as HTMLDivElement) || (document.querySelector('textarea') as HTMLTextAreaElement); if (inputElement && !inputElement.dataset.mem0KeyListener) { inputElement.dataset.mem0KeyListener = 'true'; (inputElement as HTMLElement).addEventListener('keydown', function (event: KeyboardEvent) { // Check if Enter was pressed without Shift (standard send behavior) inputValueCopy = (inputElement as HTMLTextAreaElement)?.value || inputElement.textContent || inputValueCopy; if (event.key === 'Enter' && !event.shiftKey) { // Capture and save memory asynchronously captureAndStoreMemory(); // Clear all memories after sending setTimeout(() => { allMemories = []; allMemoriesById.clear(); }, 100); } }); } } } // Function to capture and store memory asynchronously function captureAndStoreMemory(): void { // Get the message content // id is prompt-textarea const inputElement = (document.querySelector('#prompt-textarea') as HTMLTextAreaElement | HTMLDivElement) || (document.querySelector('div[contenteditable="true"]') as HTMLDivElement) || (document.querySelector('textarea') as HTMLTextAreaElement) || (document.querySelector('textarea[data-virtualkeyboard="true"]') as HTMLTextAreaElement); if (!inputElement) { return; } // Get raw content from the input element let message = inputElement.textContent || (inputElement as HTMLTextAreaElement)?.value; if (!message || message.trim() === '') { message = inputValueCopy; } if (!message || message.trim() === '') { return; } // Clean the message of any memory wrapper content message = getContentWithoutMemories(message); // Skip if message is empty after cleaning if (!message || message.trim() === '') { return; } // Asynchronously store the memory chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.MEMORY_ENABLED, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, ], function (items) { // Skip if memory is disabled or no credentials if ( items[StorageKey.MEMORY_ENABLED] === false || (!items[StorageKey.API_KEY] && !items[StorageKey.ACCESS_TOKEN]) ) { return; } const authHeader = items[StorageKey.ACCESS_TOKEN] ? `Bearer ${items[StorageKey.ACCESS_TOKEN]}` : `Token ${items[StorageKey.API_KEY]}`; const userId = items[StorageKey.USER_ID_CAMEL] || items[StorageKey.USER_ID] || DEFAULT_USER_ID; // Get recent messages for context (if available) const messages = getLastMessages(2); messages.push({ role: MessageRole.User, content: message }); const optionalParams: OptionalApiParams = {}; if (items[StorageKey.SELECTED_ORG]) { optionalParams.org_id = items[StorageKey.SELECTED_ORG]; } if (items[StorageKey.SELECTED_PROJECT]) { optionalParams.project_id = items[StorageKey.SELECTED_PROJECT]; } // Send memory to mem0 API asynchronously without waiting for response const storagePayload = { messages: messages, user_id: userId, infer: true, metadata: { provider: 'ChatGPT', }, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }; fetch('https://api.mem0.ai/v1/memories/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify(storagePayload), }).catch(error => { console.error('Error saving memory:', error); }); } ); } async function updateNotificationDot(): Promise { const memoryEnabled = await getMemoryEnabledState(); if (!memoryEnabled) { return; } const input = document.querySelector('#prompt-textarea') || document.querySelector('div[contenteditable="true"]') || document.querySelector('textarea'); const host = document.getElementById('mem0-icon-button'); // shadow host if (!input || !host) { setTimeout(updateNotificationDot, 1000); return; } const set = () => { const txt = input.textContent || input.value || ''; host.setAttribute('data-has-text', txt.trim() ? '1' : '0'); }; const mo = new MutationObserver(set); mo.observe(input, { childList: true, characterData: true, subtree: true }); input.addEventListener('input', set); input.addEventListener('keyup', set); input.addEventListener('focus', set); set(); } // Modified function to handle Mem0 modal instead of direct injection async function handleMem0Modal(sourceButtonId: string | null = null): Promise { const memoryEnabled = await getMemoryEnabledState(); if (!memoryEnabled) { return; } // Check if user is logged in const loginData = await new Promise(resolve => { chrome.storage.sync.get( [StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN], function (items) { resolve(items); } ); }); // If no API key and no access token, show login popup if (!loginData[StorageKey.API_KEY] && !loginData[StorageKey.ACCESS_TOKEN]) { showLoginPopup(); return; } const mem0Button = document.querySelector('#mem0-icon-button') as HTMLElement; let message = getInputValue(); // If no message, show a popup and return if (!message || message.trim() === '') { if (mem0Button) { showButtonPopup(mem0Button as HTMLElement, 'Please enter some text first'); } return; } try { const MEM0_PLAIN = OPENMEMORY_PROMPTS.memory_header_plain_regex; message = message.replace(MEM0_PLAIN, '').trim(); } catch { // Ignore errors during re-initialization } const endIndex = message.indexOf('

'); if (endIndex !== -1) { message = message.slice(0, endIndex + 4); } if (isProcessingMem0) { return; } isProcessingMem0 = true; // Show the loading modal immediately with the source button ID createMemoryModal([], true, sourceButtonId); try { const data = await new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, StorageKey.SIMILARITY_THRESHOLD, StorageKey.TOP_K, ], function (items) { resolve(items); } ); }); const apiKey = data[StorageKey.API_KEY]; const accessToken = data[StorageKey.ACCESS_TOKEN]; if (!apiKey && !accessToken) { isProcessingMem0 = false; return; } sendExtensionEvent('modal_clicked', { provider: 'chatgpt', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), }); const messages = getLastMessages(2); messages.push({ role: MessageRole.User, content: message }); const optionalParams: OptionalApiParams = {}; if (data[StorageKey.SELECTED_ORG]) { optionalParams.org_id = data[StorageKey.SELECTED_ORG]; } if (data[StorageKey.SELECTED_PROJECT]) { optionalParams.project_id = data[StorageKey.SELECTED_PROJECT]; } currentModalSourceButtonId = sourceButtonId; chatgptSearch.runImmediate(message); } catch (error) { console.error('Error:', error); // Still show the modal but with empty state if there was an error createMemoryModal([], false, sourceButtonId); throw error; } finally { isProcessingMem0 = false; } } // Function to show a small popup message near the button function showButtonPopup(button: HTMLElement, message: string): void { let host = button || document.getElementById('mem0-icon-button'); if (!host) { return; } let root = host.shadowRoot || host; // Remove any existing popups const existingPopup = root.querySelector('.mem0-button-popup'); if (existingPopup) { existingPopup.remove(); } // Also hide any hover popover that might be showing const hoverPopover = document.querySelector('.mem0-button-popover') as HTMLElement; if (hoverPopover) { hoverPopover.style.opacity = '0'; hoverPopover.style.display = 'none'; } const popup = document.createElement('div'); popup.className = 'mem0-button-popup'; popup.style.cssText = ` position: absolute; top: -40px; left: 50%; transform: translateX(-50%); background-color: #1C1C1E; border: 1px solid #27272A; color: white; padding: 8px 12px; border-radius: 6px; font-size: 12px; white-space: nowrap; z-index: 10001; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); `; popup.textContent = message; // Create arrow const arrow = document.createElement('div'); arrow.style.cssText = ` position: absolute; bottom: -5px; left: 50%; transform: translateX(-50%) rotate(45deg); width: 10px; height: 10px; background-color: #1C1C1E; border-right: 1px solid #27272A; border-bottom: 1px solid #27272A; `; popup.appendChild(arrow); root.appendChild(popup); setTimeout(function () { if (popup.isConnected) { popup.remove(); } }, 3000); // Position relative to button // button.style.position = 'relative'; // button.appendChild(popup); // // Auto-remove after 3 seconds // setTimeout(() => { // if (document.body.contains(popup)) { // popup.remove(); // } // }, 3000); } // Safe no-op to prevent ReferenceError if auto-inject prefetch isn't defined elsewhere function setupAutoInjectPrefetch() { try { // Intentionally left blank; legacy callers expect this to exist. // Inline hint handles lightweight suggestion awareness. } catch { // Ignore errors during re-initialization } } (function () { if (!OPENMEMORY_UI || !OPENMEMORY_UI.mountOnEditorFocus) { return; } // 1) Try to mount immediately from cached anchor on page load (before focus) try { // Skip if already mounted (e.g., hot reload / rapid SPA replace) if (!document.getElementById('mem0-icon-button')) { OPENMEMORY_UI.resolveCachedAnchor( { learnKey: location.host + ':' + location.pathname }, null, 24 * 60 * 60 * 1000 ) .then(function (hit) { if (!hit || !hit.el) { return; } // Reuse the same render and placement as the focus-driven path let hs = OPENMEMORY_UI.createShadowRootHost('mem0-root'); let host = hs.host, shadow = hs.shadow; host.id = 'mem0-icon-button'; let unplace = OPENMEMORY_UI.applyPlacement({ container: host, anchor: hit.el, placement: hit.placement || { strategy: 'inline', where: 'beforeend', inlineAlign: 'end', }, }); let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); // Nudge to the left of mic if present in same anchor try { let mic = hit.el && (hit.el.querySelector('button[aria-label="Dictate button"]') || hit.el.querySelector('button[aria-label*="mic" i]') || hit.el.querySelector('button[aria-label*="voice" i]')); if (mic && hit.el) { let child: Element | null = mic; while (child && child.parentElement !== hit.el) { child = child.parentElement; } if (child && child.parentElement === hit.el) { hit.el.insertBefore(host, child); } } } catch { // Ignore errors during re-initialization } btn.addEventListener('click', function () { handleMem0Modal('mem0-icon-button'); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } // If the anchor disappears, allow normal focus flow to re-mount const removal = new MutationObserver(function () { if (!document.contains(hit.el) || !document.contains(host)) { try { unplace(); } catch { // Ignore errors during re-initialization } try { removal.disconnect(); } catch { // Ignore errors during re-initialization } } }); removal.observe(document.documentElement, { childList: true, subtree: true }); }) .catch(function () { // Ignore errors during re-initialization }); } } catch { // Ignore errors during re-initialization } // 2) Standard focus-driven mount OPENMEMORY_UI.mountOnEditorFocus({ existingHostSelector: '#mem0-icon-button', editorSelector: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.chatgpt && SITE_CONFIG.chatgpt.editorSelector ? SITE_CONFIG.chatgpt.editorSelector : 'textarea, [contenteditable="true"], input[type="text"]', deriveAnchor: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.chatgpt && typeof SITE_CONFIG.chatgpt.deriveAnchor === 'function' ? SITE_CONFIG.chatgpt.deriveAnchor : function (editor) { return editor.closest('form') || editor.parentElement; }, placement: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.chatgpt && SITE_CONFIG.chatgpt.placement ? SITE_CONFIG.chatgpt.placement : { strategy: 'inline', where: 'beforeend', inlineAlign: 'end' }, render: function (shadow: ShadowRoot, host: HTMLElement, anchor: Element | null) { host.id = 'mem0-icon-button'; // existing code relies on this let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); try { let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.chatgpt ? SITE_CONFIG.chatgpt : null; let mic = null; if (cfg && Array.isArray(cfg.adjacentTargets)) { for (let i = 0; i < cfg.adjacentTargets.length; i++) { let sel = cfg.adjacentTargets[i]; if (sel) { mic = anchor && anchor.querySelector(sel); if (mic) { break; } } } } if (mic && anchor) { let child: Element | null = mic; while (child && child.parentElement !== anchor) { child = child.parentElement; } if (child && child.parentElement === anchor) { anchor.insertBefore(host, child); } host.style.marginRight = ''; // rely on container gap host.style.marginLeft = ''; } else { host.style.marginLeft = '4px'; // mild fallback spacing } } catch (_e) { // Ignore errors during re-initialization } btn.addEventListener('click', function () { handleMem0Modal('mem0-icon-button'); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } }, // Optional safety net if deriveAnchor fails fallback: function () { let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.chatgpt ? SITE_CONFIG.chatgpt : null; OPENMEMORY_UI.mountResilient({ anchors: [ { find: function () { let sel = (cfg && cfg.editorSelector) || 'textarea, [contenteditable="true"], input[type="text"]'; let ed = document.querySelector(sel); if (!ed) { return null; } try { return cfg && typeof cfg.deriveAnchor === 'function' ? cfg.deriveAnchor(ed) : ed.closest('form') || ed.parentElement; } catch (_) { return ed.closest('form') || ed.parentElement; } }, }, ], placement: (cfg && cfg.placement) || { strategy: 'inline', where: 'beforeend', inlineAlign: 'end', }, enableFloatingFallback: true, render: function (shadow: ShadowRoot, host: HTMLElement, anchor: Element | null) { host.id = 'mem0-icon-button'; // host is the shadow root container let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal('mem0-icon-button'); }); // Move host to the left of the mic inside the same toolbar container try { let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.chatgpt ? SITE_CONFIG.chatgpt : null; let mic = null; if (cfg && Array.isArray(cfg.adjacentTargets)) { for (let i = 0; i < cfg.adjacentTargets.length; i++) { let sel = cfg.adjacentTargets[i]; if (sel) { mic = anchor && anchor.querySelector(sel); if (mic) { break; } } } } else { mic = anchor && (anchor.querySelector('button[aria-label="Dictate button"]') || anchor.querySelector('button[aria-label*="mic" i]') || anchor.querySelector('button[aria-label*="voice" i]')); } if (mic && anchor) { let child: Element | null = mic; while (child && child.parentElement !== anchor) { child = child.parentElement; } if (child && child.parentElement === anchor) { anchor.insertBefore(host, child); } host.style.marginRight = ''; // rely on container gap host.style.marginLeft = ''; } else { host.style.marginLeft = '4px'; // mild fallback spacing } } catch (_e) { // Ignore errors during re-initialization } if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } }, }); }, persistCache: true, cacheTtlMs: 24 * 60 * 60 * 1000, }); })(); function getLastMessages(count: number): Array<{ role: MessageRole; content: string }> { const messageContainer = document.querySelector('.flex.flex-col.text-sm.md\\:pb-9'); if (!messageContainer) { return []; } const messageElements = Array.from(messageContainer.children).reverse(); const messages: Array<{ role: MessageRole; content: string }> = []; for (const element of messageElements) { if (messages.length >= count) { break; } const userElement = element.querySelector('[data-message-author-role="user"]'); const assistantElement = element.querySelector('[data-message-author-role="assistant"]'); if (userElement) { const content = userElement.querySelector('.whitespace-pre-wrap')?.textContent?.trim() || ''; messages.unshift({ role: MessageRole.User, content }); } else if (assistantElement) { const content = assistantElement.querySelector('.markdown')?.textContent?.trim() || ''; messages.unshift({ role: MessageRole.Assistant, content }); } } return messages; } function getInputValue(): string { const inputElement = (document.querySelector('#prompt-textarea') as HTMLTextAreaElement | HTMLDivElement) || (document.querySelector('div[contenteditable="true"]') as HTMLDivElement) || (document.querySelector('textarea') as HTMLTextAreaElement); return inputElement ? inputElement.textContent || (inputElement as HTMLTextAreaElement)?.value || '' : ''; } let chatgptBackgroundSearchHandler: ((this: Element, ev: Event) => void) | null = null; function hookBackgroundSearchTyping() { const inputElement = document.querySelector('#prompt-textarea') || document.querySelector('div[contenteditable="true"]') || document.querySelector('textarea'); if (!inputElement) { return; } if (inputElement.dataset.mem0BackgroundHooked) { return; } inputElement.dataset.mem0BackgroundHooked = 'true'; if (!chatgptBackgroundSearchHandler) { chatgptBackgroundSearchHandler = function () { const text = getInputValue() || ''; console.log("Background search for:", text); chatgptSearch.setText(text); }; } inputElement.addEventListener( 'input', chatgptBackgroundSearchHandler as (this: Element, ev: Event) => void ); inputElement.addEventListener( 'keyup', chatgptBackgroundSearchHandler as (this: Element, ev: Event) => void ); } function addSyncButton(): void { const buttonContainer = document.querySelector('div.mt-5.flex.justify-end'); if (buttonContainer) { let syncButton = document.querySelector('#sync-button') as HTMLButtonElement; // If the syncButton does not exist, create it if (!syncButton) { syncButton = document.createElement('button'); syncButton.id = 'sync-button'; syncButton.className = 'btn relative btn-neutral mr-2'; syncButton.style.color = 'rgb(213, 213, 213)'; syncButton.style.backgroundColor = 'transparent'; syncButton.innerHTML = '
Sync Memory
'; syncButton.style.border = '1px solid rgb(213, 213, 213)'; syncButton.style.fontSize = '12px'; syncButton.style.fontWeight = '500'; // add margin right to syncButton syncButton.style.marginRight = '8px'; const syncIcon = document.createElement('img'); syncIcon.src = chrome.runtime.getURL('icons/mem0-claude-icon.png'); syncIcon.style.width = '16px'; syncIcon.style.height = '16px'; syncIcon.style.marginRight = '8px'; syncButton.prepend(syncIcon); syncButton.addEventListener('click', handleSyncClick); syncButton.addEventListener('mouseenter', () => { if (!syncButton!.disabled) { syncButton!.style.filter = 'opacity(0.7)'; } }); syncButton.addEventListener('mouseleave', () => { if (!syncButton!.disabled) { syncButton!.style.filter = 'opacity(1)'; } }); } if (!buttonContainer.contains(syncButton)) { buttonContainer.insertBefore(syncButton, buttonContainer.firstChild); } // Update sync button state const updateSyncButtonState = (): void => { // Define when the sync button should be enabled or disabled (syncButton as HTMLButtonElement).disabled = false; // For example, always enabled // Update opacity or pointer events if needed if ((syncButton as HTMLButtonElement).disabled) { (syncButton as HTMLButtonElement).style.opacity = '0.5'; (syncButton as HTMLButtonElement).style.pointerEvents = 'none'; } else { (syncButton as HTMLButtonElement).style.opacity = '1'; (syncButton as HTMLButtonElement).style.pointerEvents = 'auto'; } }; updateSyncButtonState(); } else { // If resetMemoriesButton or specificTable is not found, remove syncButton from DOM const existingSyncButton = document.querySelector('#sync-button'); if (existingSyncButton && existingSyncButton.parentNode) { existingSyncButton.parentNode.removeChild(existingSyncButton); } } } function handleSyncClick(): void { getMemoryEnabledState().then(memoryEnabled => { if (!memoryEnabled) { const btn = document.querySelector('#sync-button') as HTMLElement; if (btn) { showSyncPopup(btn, 'Memory is disabled'); } return; } const table = document.querySelector('table.w-full.border-separate.border-spacing-0'); const syncButton = document.querySelector('#sync-button') as HTMLButtonElement; if (table && syncButton) { const rows = table.querySelectorAll('tbody tr'); const memories: Array<{ role: string; content: string }> = []; // Change sync button state to loading setSyncButtonLoadingState(true); let syncedCount = 0; const totalCount = rows.length; rows.forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length >= 1 && cells[0]) { const content = cells[0].querySelector('div.whitespace-pre-wrap')?.textContent?.trim() || ''; const memory = { role: MessageRole.User, content: `Remember this about me: ${content}`, }; memories.push(memory); sendMemoryToMem0(memory, false) .then(() => { syncedCount++; if (syncedCount === totalCount) { showSyncPopup(syncButton, `${syncedCount} memories synced`); setSyncButtonLoadingState(false); // Open the modal with memories after syncing // handleMem0Modal('sync-button'); } }) .catch(() => { if (syncedCount === totalCount) { showSyncPopup(syncButton, `${syncedCount}/${totalCount} memories synced`); setSyncButtonLoadingState(false); // Open the modal with memories after syncing // handleMem0Modal('sync-button'); } }); } }); sendMemoriesToMem0(memories) .then(() => { if (syncButton) { showSyncPopup(syncButton, `${memories.length} memories synced`); } setSyncButtonLoadingState(false); // Open the modal with memories after syncing handleMem0Modal('sync-button'); }) .catch(error => { console.error('Error syncing memories:', error); if (syncButton) { showSyncPopup(syncButton, 'Error syncing memories'); } setSyncButtonLoadingState(false); // Open the modal even if there was an error handleMem0Modal('sync-button'); }); } else { console.error('Table or Sync button not found'); } }); } // New function to send memories in batch function sendMemoriesToMem0(memories: Array<{ role: string; content: string }>): Promise { return new Promise((resolve, reject) => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, ], function (items) { if (items[StorageKey.API_KEY] || items[StorageKey.ACCESS_TOKEN]) { const authHeader = items[StorageKey.ACCESS_TOKEN] ? `Bearer ${items[StorageKey.ACCESS_TOKEN]}` : `Token ${items[StorageKey.API_KEY]}`; const userId = items[StorageKey.USER_ID_CAMEL] || items[StorageKey.USER_ID] || DEFAULT_USER_ID; const optionalParams: OptionalApiParams = {}; if (items[StorageKey.SELECTED_ORG]) { optionalParams.org_id = items[StorageKey.SELECTED_ORG]; } if (items[StorageKey.SELECTED_PROJECT]) { optionalParams.project_id = items[StorageKey.SELECTED_PROJECT]; } fetch('https://api.mem0.ai/v1/memories/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify({ messages: memories, user_id: userId, infer: true, metadata: { provider: 'ChatGPT', }, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }), }) .then(response => { if (!response.ok) { reject(`Failed to add memories: ${response.status}`); } else { resolve(); } }) .catch(error => reject(`Error sending memories to Mem0: ${error}`)); } else { reject('API Key/Access Token not set'); } } ); }); } function setSyncButtonLoadingState(isLoading: boolean): void { const syncButton = document.querySelector('#sync-button') as HTMLButtonElement; const syncButtonContent = document.querySelector('#sync-button-content') as HTMLElement; if (syncButton) { if (isLoading) { syncButton.disabled = true; syncButton.style.cursor = 'wait'; document.body.style.cursor = 'wait'; syncButton.style.opacity = '0.7'; if (syncButtonContent) { syncButtonContent.textContent = 'Syncing...'; } } else { syncButton.disabled = false; syncButton.style.cursor = 'pointer'; syncButton.style.opacity = '1'; document.body.style.cursor = 'default'; if (syncButtonContent) { syncButtonContent.textContent = 'Sync Memory'; } } } } function showSyncPopup(button: HTMLElement, message: string): void { const popup = document.createElement('div'); // Create and add the (i) icon const infoIcon = document.createElement('span'); infoIcon.textContent = 'ⓘ '; infoIcon.style.marginRight = '3px'; popup.appendChild(infoIcon); popup.appendChild(document.createTextNode(message)); popup.style.cssText = ` position: absolute; top: 50%; left: -160px; transform: translateY(-50%); background-color: #171717; color: white; padding: 6px 8px; border-radius: 6px; font-size: 12px; white-space: nowrap; z-index: 1000; `; button.style.position = 'relative'; button.appendChild(popup); setTimeout(() => { popup.remove(); }, 3000); } function sendMemoryToMem0( memory: { role: string; content: string }, infer: boolean = true ): Promise { return new Promise((resolve, reject) => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, ], function (items) { if (items[StorageKey.API_KEY] || items[StorageKey.ACCESS_TOKEN]) { const authHeader = items[StorageKey.ACCESS_TOKEN] ? `Bearer ${items[StorageKey.ACCESS_TOKEN]}` : `Token ${items[StorageKey.API_KEY]}`; const userId = items[StorageKey.USER_ID_CAMEL] || items[StorageKey.USER_ID] || DEFAULT_USER_ID; const optionalParams: OptionalApiParams = {}; if (items[StorageKey.SELECTED_ORG]) { optionalParams.org_id = items[StorageKey.SELECTED_ORG]; } if (items[StorageKey.SELECTED_PROJECT]) { optionalParams.project_id = items[StorageKey.SELECTED_PROJECT]; } fetch('https://api.mem0.ai/v1/memories/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify({ messages: [{ content: memory.content, role: MessageRole.User }], user_id: userId, infer: infer, metadata: { provider: 'ChatGPT', }, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }), }) .then(response => { if (!response.ok) { reject(`Failed to add memory: ${response.status}`); } else { resolve(); } }) .catch(error => reject(`Error sending memory to Mem0: ${error}`)); } else { reject('API Key/Access Token not set'); } } ); }); } // Add this new function to get the memory_enabled state function getMemoryEnabledState(): Promise { return new Promise(resolve => { chrome.storage.sync.get([StorageKey.MEMORY_ENABLED], function (result) { resolve(result.memory_enabled !== false); // Default to true if not set }); }); } // Returns whether auto-inject is enabled (default: false if not present) // (auto-inject helpers removed) // Update the initialization function to add the Mem0 icon button but not intercept Enter key function initializeMem0Integration(): void { document.addEventListener('DOMContentLoaded', () => { addSyncButton(); // (async () => await addMem0IconButton())(); addSendButtonListener(); // (async () => await updateNotificationDot())(); hookBackgroundSearchTyping(); setupAutoInjectPrefetch(); }); document.addEventListener('keydown', function (event) { if (event.ctrlKey && event.key === 'm') { event.preventDefault(); (async () => { await handleMem0Modal('mem0-icon-button'); })(); } }); // Remove global Enter interception previously added for auto-inject observer = new MutationObserver(() => { addSyncButton(); // (async () => await addMem0IconButton())(); addSendButtonListener(); // (async () => await updateNotificationDot())(); hookBackgroundSearchTyping(); setupAutoInjectPrefetch(); }); observer.observe(document.body, { childList: true, subtree: true }); // Add a MutationObserver to watch for changes in the DOM but don't intercept Enter key const observerForUI = new MutationObserver(() => { // (async () => await addMem0IconButton())(); addSendButtonListener(); // (async () => await updateNotificationDot())(); hookBackgroundSearchTyping(); setupAutoInjectPrefetch(); }); observerForUI.observe(document.body, { childList: true, subtree: true, }); } // (global auto-inject interceptors removed) // Function to show login popup function showLoginPopup() { // First remove any existing popups const existingPopup = document.querySelector('#mem0-login-popup'); if (existingPopup) { existingPopup.remove(); } // Create popup container const popupOverlay = document.createElement('div'); popupOverlay.id = 'mem0-login-popup'; popupOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10001; `; const popupContainer = document.createElement('div'); popupContainer.style.cssText = ` background-color: #1C1C1E; border-radius: 12px; width: 320px; padding: 24px; color: white; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; // Close button const closeButton = document.createElement('button'); closeButton.style.cssText = ` position: absolute; top: 16px; right: 16px; background: none; border: none; color: #A1A1AA; font-size: 16px; cursor: pointer; `; closeButton.innerHTML = '×'; closeButton.addEventListener('click', () => { document.body.removeChild(popupOverlay); }); // Logo and heading const logoContainer = document.createElement('div'); logoContainer.style.cssText = ` display: flex; align-items: center; justify-content: center; margin-bottom: 16px; `; const logo = document.createElement('img'); logo.src = chrome.runtime.getURL('icons/mem0-claude-icon.png'); logo.style.cssText = ` width: 24px; height: 24px; border-radius: 50%; margin-right: 12px; `; const logoDark = document.createElement('img'); logoDark.src = chrome.runtime.getURL('icons/mem0-icon-black.png'); logoDark.style.cssText = ` width: 24px; height: 24px; border-radius: 50%; margin-right: 12px; `; const heading = document.createElement('h2'); heading.textContent = 'Sign in to OpenMemory'; heading.style.cssText = ` margin: 0; font-size: 18px; font-weight: 600; `; logoContainer.appendChild(heading); // Message const message = document.createElement('p'); message.textContent = 'Please sign in to access your memories and personalize your conversations!'; message.style.cssText = ` margin-bottom: 24px; color: #D4D4D8; font-size: 14px; line-height: 1.5; text-align: center; `; // Sign in button const signInButton = document.createElement('button'); signInButton.style.cssText = ` display: flex; align-items: center; justify-content: center; width: 100%; padding: 10px; background-color: white; color: black; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: background-color 0.2s; `; // Add text in span for better centering const signInText = document.createElement('span'); signInText.textContent = 'Sign in with Mem0'; signInButton.appendChild(logoDark); signInButton.appendChild(signInText); signInButton.addEventListener('mouseenter', () => { signInButton.style.backgroundColor = '#f5f5f5'; }); signInButton.addEventListener('mouseleave', () => { signInButton.style.backgroundColor = 'white'; }); // Open sign-in page when clicked signInButton.addEventListener('click', () => { window.open('https://app.mem0.ai/login', '_blank'); document.body.removeChild(popupOverlay); }); // Assemble popup popupContainer.appendChild(logoContainer); popupContainer.appendChild(message); popupContainer.appendChild(signInButton); popupOverlay.appendChild(popupContainer); popupOverlay.appendChild(closeButton); // Add click event to close when clicking outside popupOverlay.addEventListener('click', e => { if (e.target === popupOverlay) { document.body.removeChild(popupOverlay); } }); // Add to body document.body.appendChild(popupOverlay); } initializeMem0Integration(); // --- SPA navigation handling and extension context guard (mirrors Claude) --- let chatgptExtensionContextValid = true; let chatgptCurrentUrl = window.location.href; function chatgptCheckExtensionContext() { try { // chrome.runtime may throw if context invalidated // Using optional chaining to avoid ReferenceError // lastError exists only after an API call; treat presence of runtime as validity const isValid = !!(chrome && chrome.runtime); if (chatgptExtensionContextValid && !isValid) { chatgptExtensionContextValid = false; } return isValid; } catch { chatgptExtensionContextValid = false; return false; } } function chatgptDetectNavigation() { const newUrl = window.location.href; if (newUrl !== chatgptCurrentUrl) { chatgptCurrentUrl = newUrl; // Re-initialize UI after small delay for DOM to settle setTimeout(() => { try { addSyncButton(); // (async () => await addMem0IconButton())(); addSendButtonListener(); // (async () => await updateNotificationDot())(); } catch { // Ignore errors when setting contenteditable } }, 300); } } // Poll for SPA navigations and context validity setInterval(() => { chatgptCheckExtensionContext(); chatgptDetectNavigation(); }, 1000); // Hook browser history navigation window.addEventListener('popstate', () => setTimeout(chatgptDetectNavigation, 100)); const chatgptOriginalPushState = history.pushState; history.pushState = function (data: HistoryStateData, unused: string, url?: string | URL | null) { chatgptOriginalPushState.call(history, data, unused, url); setTimeout(chatgptDetectNavigation, 100); }; const chatgptOriginalReplaceState = history.replaceState; history.replaceState = function ( data: HistoryStateData, unused: string, url?: string | URL | null ) { chatgptOriginalReplaceState.call(history, data, unused, url); setTimeout(chatgptDetectNavigation, 100); }; ================================================ FILE: src/claude/content.ts ================================================ import { MessageRole } from '../types/api'; import type { HistoryStateData } from '../types/browser'; import type { ExtendedDocument, ExtendedElement } from '../types/dom'; import type { MemoryItem, MemorySearchItem, OptionalApiParams } from '../types/memory'; import { SidebarAction } from '../types/messages'; import { type StorageData, StorageKey } from '../types/storage'; import { createOrchestrator, type SearchStorage } from '../utils/background_search'; import { OPENMEMORY_PROMPTS } from '../utils/llm_prompts'; import { SITE_CONFIG } from '../utils/site_config'; import { getBrowser, sendExtensionEvent } from '../utils/util_functions'; import { OPENMEMORY_UI, type Placement } from '../utils/util_positioning'; // Chrome runtime types interface ChromeRuntimeLastError { message?: string; } interface ChromeRuntimeWithLastError { lastError?: ChromeRuntimeLastError; } interface ChromeRuntime extends ChromeRuntimeWithLastError { // Add other chrome.runtime properties as needed } export {}; // Global variables to store all memories let allMemories: string[] = []; let memoryModalShown: boolean = false; let isProcessingMem0: boolean = false; let memoryEnabled: boolean = true; // Cache of the latest typed text to avoid race when the editor is cleared let lastTyped = ''; // Timestamp of when a send was initiated (to prevent duplicate fallback posts) let lastSendInitiatedAt = 0; let currentModalSourceButtonId: string | null = null; const claudeSearch = createOrchestrator({ fetch: async function (query: string, opts: { signal?: AbortSignal }) { const data = await new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, StorageKey.SIMILARITY_THRESHOLD, StorageKey.TOP_K, ], function (items) { resolve(items as SearchStorage); } ); }); const apiKey = data[StorageKey.API_KEY]; const accessToken = data[StorageKey.ACCESS_TOKEN]; if (!apiKey && !accessToken) { return []; } const authHeader = accessToken ? `Bearer ${accessToken}` : `Token ${apiKey}`; const userId = data[StorageKey.USER_ID_CAMEL] || data[StorageKey.USER_ID] || 'chrome-extension-user'; const threshold = data[StorageKey.SIMILARITY_THRESHOLD] !== undefined ? data[StorageKey.SIMILARITY_THRESHOLD] : 0.1; const topK = data[StorageKey.TOP_K] !== undefined ? data[StorageKey.TOP_K] : 10; const optionalParams: OptionalApiParams = {}; if (data[StorageKey.SELECTED_ORG]) { optionalParams.org_id = data[StorageKey.SELECTED_ORG]; } if (data[StorageKey.SELECTED_PROJECT]) { optionalParams.project_id = data[StorageKey.SELECTED_PROJECT]; } // Clean query by stripping any appended memory header/content (debounced path) const cleanQuery = (function () { try { const MEM0_PLAIN = OPENMEMORY_PROMPTS.memory_header_plain_regex; return String(query).replace(MEM0_PLAIN, '').trim(); } catch (_e) { return query; } })(); const payload = { query: cleanQuery, filters: { user_id: userId }, rerank: true, threshold, top_k: topK, filter_memories: false, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }; const res = await fetch('https://api.mem0.ai/v2/memories/search/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify(payload), signal: opts && opts.signal, }); if (!res.ok) { throw new Error(`API request failed with status ${res.status}`); } return await res.json(); }, onSuccess: function (normQuery: string, responseData: MemorySearchItem[]) { if (!memoryModalShown) { return; } const memoryItems = (responseData || []).map((item: MemorySearchItem, index: number) => ({ id: String(item.id || `memory-${Date.now()}-${index}`), text: item.memory, categories: item.categories || [], })); createMemoryModal(memoryItems, false, currentModalSourceButtonId); }, onError: function () { if (memoryModalShown) { createMemoryModal([], false, currentModalSourceButtonId); } }, minLength: 3, debounceMs: 75, cacheTTL: 60000, }); // Sliding window for conversation context let conversationHistory: Array<{ role: MessageRole; content: string; timestamp: number }> = []; const MAX_CONVERSATION_HISTORY = 12; // Keep last 12 messages (6 pairs of user/assistant) // Function to add message to conversation history with sliding window function addToConversationHistory(role: MessageRole, content: string) { if (!content || !content.trim()) { return; } const trimmedContent = content.trim(); // Check for duplicate - don't add if the last message is identical if (conversationHistory.length > 0) { const lastMessage = conversationHistory[conversationHistory.length - 1]; if (lastMessage && lastMessage.role === role && lastMessage.content === trimmedContent) { return; } } const message = { role: role, content: trimmedContent, timestamp: Date.now(), }; // Add to history conversationHistory.push(message); // Maintain sliding window - remove oldest messages if we exceed limit if (conversationHistory.length > MAX_CONVERSATION_HISTORY) { conversationHistory.splice(0, conversationHistory.length - MAX_CONVERSATION_HISTORY); } } // Function to get conversation context for memory creation function getConversationContext(includeCurrent: boolean = true) { if (conversationHistory.length === 0) { return []; } // Get the last 6 messages for context (excluding current if requested) const contextSize = 6; let contextMessages = [...conversationHistory]; if (!includeCurrent && contextMessages.length > 0) { // Remove the last message if it's the current user message contextMessages = contextMessages.slice(0, -1); } // Get last N messages const context = contextMessages.slice(-contextSize).map(msg => ({ role: msg.role, content: msg.content, })); return context; } // Function to initialize conversation history from existing messages on page function initializeConversationHistoryFromDOM() { const messageContainer = document.querySelector( '.flex-1.flex.flex-col.gap-3.px-4.max-w-3xl.mx-auto.w-full' ); if (!messageContainer) { return; } const messageElements = Array.from(messageContainer.children); // Process existing messages in chronological order messageElements.forEach(element => { const userElement = element.querySelector('.font-user-message'); const assistantElement = element.querySelector('.font-claude-message'); if (userElement) { const content = (userElement.textContent || '').trim(); if (content) { addToConversationHistory(MessageRole.User, content); } } else if (assistantElement) { const content = (assistantElement.textContent || '').trim(); if (content) { addToConversationHistory(MessageRole.Assistant, content); } } }); } // Initialize the MutationObserver variable // let observer: MutationObserver; // let inputObserver: MutationObserver; // let debounceTimer: number | undefined; // Track added memories by ID const allMemoriesById: Set = new Set(); // Reference to the modal overlay for updates let currentModalOverlay: | HTMLDivElement | (HTMLDivElement & { _cleanupDragEvents?: () => void }) | null = null; // Track the current modal container and placement cleanup let currentModalContainer: HTMLDivElement | null = null; let currentModalUnplace: (() => void) | null = null; // Function to get memory enabled state from storage async function getMemoryEnabledState() { return new Promise(resolve => { // Check if extension context is valid if (!chrome || !chrome.storage || !chrome.storage.sync) { console.log('⚠️ Chrome extension context invalid, defaulting to enabled'); resolve(true); // Default to enabled if we can't check return; } try { chrome.storage.sync.get(StorageKey.MEMORY_ENABLED, function (data) { try { // @ts-ignore if (chrome.runtime && chrome.runtime.lastError) { console.log( '⚠️ Chrome storage error, defaulting to enabled:', (chrome.runtime as ChromeRuntime).lastError ); resolve(true); // Default to enabled if error return; } console.log('🧠 Memory enabled from storage:', data.memory_enabled); resolve(data.memory_enabled); } catch { // Ignore errors when checking chrome.runtime.lastError } }); } catch (error) { console.log('⚠️ Exception getting memory state, defaulting to enabled:', error); resolve(true); // Default to enabled if exception } }); } // Function to remove mem0 button if it exists function removeMemButton(): void { const mem0Button = document.querySelector('#mem0-button'); if (mem0Button) { const buttonContainer = mem0Button.closest('div'); if (buttonContainer) { buttonContainer.remove(); } else { mem0Button.remove(); } } // Also remove tooltip if it exists const tooltip = document.querySelector('#mem0-tooltip'); if (tooltip) { tooltip.remove(); } } // function addMem0Button(): void { // // Legacy addMem0Button removed; OPENMEMORY_UI handles icon mounting // } function createPopup(container: HTMLElement, position: string = 'top'): HTMLElement { const popup = document.createElement('div'); popup.className = 'mem0-popup'; let positionStyles = ''; if (position === 'top') { positionStyles = ` bottom: 100%; left: 50%; transform: translateX(-40%); margin-bottom: 11px; `; } else if (position === 'right') { positionStyles = ` top: 50%; left: 100%; transform: translateY(-50%); margin-left: 11px; `; } popup.style.cssText = ` display: none; position: absolute; background-color: #21201C; color: white; padding: 6px 8px; border-radius: 6px; font-size: 12px; z-index: 10000; white-space: nowrap; box-shadow: 0 2px 5px rgba(0,0,0,0.2); ${positionStyles} `; container.appendChild(popup); return popup; } const sendButton = document.querySelector('button[aria-label="Send Message"]'); const sendUpButton = document.querySelector('button[aria-label="Send message"]'); const screenshotButton = document.querySelector('button[aria-label="Capture screenshot"]'); const inputToolsMenuButton = document.querySelector('#input-tools-menu-trigger'); // Check for any existing mem0 buttons before creating legacy button const hasAnyMem0Button = document.querySelector('#mem0-button') || document.getElementById('mem0-icon-button') || document.querySelector('[id*="mem0"], .mem0-btn'); if (inputToolsMenuButton && !hasAnyMem0Button) { const buttonContainer = document.createElement('div'); buttonContainer.style.position = 'relative'; buttonContainer.style.display = 'inline-block'; const mem0Button = document.createElement('button'); mem0Button.id = 'mem0-button'; mem0Button.className = inputToolsMenuButton.className; mem0Button.style.marginLeft = '0px'; mem0Button.setAttribute('aria-label', 'Add memories to your prompt'); const mem0Icon = document.createElement('img'); mem0Icon.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); mem0Icon.style.width = '16px'; mem0Icon.style.height = '16px'; mem0Icon.style.borderRadius = '50%'; const popup = createPopup(buttonContainer, 'top'); mem0Button.appendChild(mem0Icon); mem0Button.addEventListener('click', () => { if (memoryEnabled) { // Hide the tooltip if it's showing const tooltip = document.querySelector('#mem0-tooltip'); if (tooltip) { tooltip.style.display = 'none'; } handleMem0Modal(popup); } }); // Create notification dot const notificationDot = document.createElement('div'); notificationDot.id = 'mem0-notification-dot'; notificationDot.style.cssText = ` position: absolute; top: -3px; right: -3px; width: 10px; height: 10px; background-color: rgb(128, 221, 162); border-radius: 50%; border: 2px solid #1C1C1E; display: none; z-index: 1001; pointer-events: none; `; mem0Button.appendChild(notificationDot); // Add keyframe animation for the dot if (!document.getElementById('notification-dot-animation')) { const style = document.createElement('style'); style.id = 'notification-dot-animation'; style.innerHTML = ` @keyframes popIn { 0% { transform: scale(0); } 50% { transform: scale(1.2); } 100% { transform: scale(1); } } #mem0-notification-dot.active { display: block !important; animation: popIn 0.3s ease-out forwards; } `; document.head.appendChild(style); } buttonContainer.appendChild(mem0Button); const tooltip = document.createElement('div'); tooltip.id = 'mem0-tooltip'; tooltip.textContent = 'Add memories to your prompt'; tooltip.style.cssText = ` display: none; position: fixed; background-color: black; color: white; padding: 3px 7px; border-radius: 6px; font-size: 12px; z-index: 10000; pointer-events: none; white-space: nowrap; transform: translateX(-50%); `; document.body.appendChild(tooltip); mem0Button.addEventListener('mouseenter', () => { // Hide any existing popup first const existingMem0Popup = document.querySelector('.mem0-popup[style*="display: block"]'); if (existingMem0Popup && existingMem0Popup !== popup) { existingMem0Popup.style.display = 'none'; } const rect = mem0Button.getBoundingClientRect(); const buttonCenterX = rect.left + rect.width / 2; // Set initial tooltip properties tooltip.style.display = 'block'; // Once displayed, we can get its height and set proper positioning const tooltipHeight = (tooltip as HTMLElement).offsetHeight || 24; // Default height if not yet rendered tooltip.style.left = `${buttonCenterX}px`; tooltip.style.top = `${rect.top - tooltipHeight - 10}px`; // Position 10px above button }); mem0Button.addEventListener('mouseleave', () => { tooltip.style.display = 'none'; }); // Find the parent container to place the button at the same level as input-tools-menu const parentContainer = inputToolsMenuButton.closest('.relative.flex-1.flex.items-center.gap-2') || inputToolsMenuButton.closest('.relative.flex-1') || ((inputToolsMenuButton.parentNode as HTMLElement)?.parentNode?.parentNode?.parentNode ?.parentNode as HTMLElement); if (parentContainer) { // Find the third position in the container - after the first two divs // Looking for the flex-row div to insert before it const flexRowDiv = parentContainer.querySelector('.flex.flex-row.items-center.gap-2.min-w-0'); // Find the tools div that we want to position after const toolsDiv = inputToolsMenuButton.closest('div > div > div > div')?.parentNode?.parentNode; // Make sure our button is the third div in the container if (flexRowDiv && toolsDiv) { // Insert right after the tools div and before the flex-row div parentContainer.insertBefore(buttonContainer, flexRowDiv); } else { // Fallback to just append to the parent parentContainer.appendChild(buttonContainer); } } else { // Fallback to original behavior if parent not found inputToolsMenuButton.parentNode?.insertBefore( buttonContainer, inputToolsMenuButton.nextSibling ); } // Update notification dot updateNotificationDot(); } else if ( window.location.href.includes('claude.ai/new') && screenshotButton && !hasAnyMem0Button ) { const buttonContainer = document.createElement('div'); buttonContainer.style.position = 'relative'; buttonContainer.style.display = 'inline-block'; const mem0Button = document.createElement('button'); mem0Button.id = 'mem0-button'; mem0Button.className = screenshotButton.className; mem0Button.style.marginLeft = '0px'; mem0Button.setAttribute('aria-label', 'Add memories to your prompt'); const mem0Icon = document.createElement('img'); mem0Icon.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); mem0Icon.style.width = '16px'; mem0Icon.style.height = '16px'; mem0Icon.style.borderRadius = '50%'; const popup = createPopup(buttonContainer, 'right'); mem0Button.appendChild(mem0Icon); mem0Button.addEventListener('click', () => { if (memoryEnabled) { // Hide the tooltip if it's showing const tooltip = document.querySelector('#mem0-tooltip'); if (tooltip) { tooltip.style.display = 'none'; } handleMem0Modal(popup); } }); // Create notification dot const notificationDot = document.createElement('div'); notificationDot.id = 'mem0-notification-dot'; notificationDot.style.cssText = ` position: absolute; top: -3px; right: -3px; width: 10px; height: 10px; background-color: rgb(128, 221, 162); border-radius: 50%; border: 2px solid #1C1C1E; display: none; z-index: 1001; pointer-events: none; `; mem0Button.appendChild(notificationDot); buttonContainer.appendChild(mem0Button); const tooltip = document.createElement('div'); tooltip.id = 'mem0-tooltip'; tooltip.textContent = 'Add memories to your prompt'; tooltip.style.cssText = ` display: none; position: fixed; background-color: black; color: white; padding: 3px 7px; border-radius: 6px; font-size: 12px; z-index: 10000; pointer-events: none; white-space: nowrap; transform: translateX(-50%); `; document.body.appendChild(tooltip); mem0Button.addEventListener('mouseenter', () => { // Hide any existing popup first const existingMem0Popup = document.querySelector('.mem0-popup[style*="display: block"]'); if (existingMem0Popup && existingMem0Popup !== popup) { existingMem0Popup.style.display = 'none'; } const rect = mem0Button.getBoundingClientRect(); const buttonCenterX = rect.left + rect.width / 2; // Set initial tooltip properties tooltip.style.display = 'block'; // Once displayed, we can get its height and set proper positioning const tooltipHeight = tooltip.offsetHeight || 24; // Default height if not yet rendered tooltip.style.left = `${buttonCenterX}px`; tooltip.style.top = `${rect.top - tooltipHeight - 10}px`; // Position 10px above button }); mem0Button.addEventListener('mouseleave', () => { tooltip.style.display = 'none'; }); screenshotButton.parentNode?.insertBefore(buttonContainer, screenshotButton.nextSibling); // Update notification dot updateNotificationDot(); } else if ((sendButton || sendUpButton) && !hasAnyMem0Button) { const targetButton = sendButton || sendUpButton; if (targetButton) { // Find the parent container of the send button const buttonParent = targetButton.parentNode; if (buttonParent) { const buttonContainer = document.createElement('div'); buttonContainer.style.position = 'relative'; buttonContainer.style.display = 'inline-block'; buttonContainer.style.marginRight = '12px'; const mem0Button = document.createElement('button'); mem0Button.id = 'mem0-button'; mem0Button.style.cssText = ` display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; background: transparent; border: none; cursor: pointer; border-radius: 8px; position: relative; transition: background-color 0.3s ease; `; mem0Button.setAttribute('aria-label', 'Add memories to your prompt'); const mem0Icon = document.createElement('img'); mem0Icon.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); mem0Icon.style.width = '20px'; mem0Icon.style.height = '20px'; mem0Icon.style.borderRadius = '50%'; // Create notification dot const notificationDot = document.createElement('div'); notificationDot.id = 'mem0-notification-dot'; notificationDot.style.cssText = ` position: absolute; top: 0px; right: 0px; width: 10px; height: 10px; background-color: rgb(128, 221, 162); border-radius: 50%; border: 2px solid #1C1C1E; display: none; z-index: 1001; pointer-events: none; `; const popup = createPopup(buttonContainer, 'top'); mem0Button.appendChild(mem0Icon); mem0Button.appendChild(notificationDot); mem0Button.addEventListener('click', () => { if (memoryEnabled) { // Hide the tooltip if it's showing const tooltip = document.querySelector('#mem0-tooltip'); if (tooltip) { tooltip.style.display = 'none'; } handleMem0Modal(popup); } }); const tooltip = document.createElement('div'); tooltip.id = 'mem0-tooltip'; tooltip.textContent = 'Add memories to your prompt'; tooltip.style.cssText = ` display: none; position: fixed; background-color: black; color: white; padding: 3px 7px; border-radius: 6px; font-size: 12px; z-index: 10000; pointer-events: none; white-space: nowrap; transform: translateX(-50%); `; document.body.appendChild(tooltip); mem0Button.addEventListener('mouseenter', () => { // Hide any existing popup first const existingMem0Popup = document.querySelector('.mem0-popup[style*="display: block"]'); if (existingMem0Popup && existingMem0Popup !== popup) { existingMem0Popup.style.display = 'none'; } const rect = mem0Button.getBoundingClientRect(); const buttonCenterX = rect.left + rect.width / 2; // Set initial tooltip properties tooltip.style.display = 'block'; // Once displayed, we can get its height and set proper positioning const tooltipHeight = tooltip.offsetHeight || 24; // Default height if not yet rendered tooltip.style.left = `${buttonCenterX}px`; tooltip.style.top = `${rect.top - tooltipHeight - 10}px`; // Position 10px above button }); mem0Button.addEventListener('mouseleave', () => { mem0Button.style.backgroundColor = 'transparent'; popup.style.display = 'none'; }); // Set popover text popup.textContent = 'Add memories to your prompt'; buttonContainer.appendChild(mem0Button); // Insert the button before the send button if (buttonParent.querySelector('button[aria-label="Send message"]')) { buttonParent.insertBefore( buttonContainer, buttonParent.querySelector('button[aria-label="Send message"]') ); } else { buttonParent.insertBefore(buttonContainer, targetButton); } // Update notification dot updateNotificationDot(); } } } // Send button listeners are now handled in initializeMem0Integration for better reliability // Also handle Enter key press for sending messages const inputElement = document.querySelector('div[contenteditable="true"]') || document.querySelector('textarea') || document.querySelector('p[data-placeholder="How can I help you today?"]') || document.querySelector('p[data-placeholder="Reply to Claude..."]'); if (inputElement && !inputElement.dataset.mem0KeyListener) { inputElement.dataset.mem0KeyListener = 'true'; inputElement.addEventListener('keydown', function (event: Event) { const keyboardEvent = event as KeyboardEvent; // Check if Enter was pressed without Shift (standard send behavior) if ( keyboardEvent.key === 'Enter' && !keyboardEvent.shiftKey && !keyboardEvent.ctrlKey && !keyboardEvent.metaKey ) { // Don't process for textarea which may want newlines if (inputElement.tagName.toLowerCase() !== 'textarea') { // Snapshot before send const current = getInputValue(); if (current && current.trim() !== '') { lastTyped = current; } lastSendInitiatedAt = Date.now(); // Capture and save memory asynchronously captureAndStoreMemory(lastTyped); // Clear all memories after sending setTimeout(() => { allMemories = []; allMemoriesById.clear(); }, 100); } } }); // Keep a live cache during typing to improve reliability if (!inputElement.dataset.mem0CacheListener) { inputElement.dataset.mem0CacheListener = 'true'; const updateCache = () => { const val = getInputValue(); if (val && val.trim() !== '') { lastTyped = val; } }; inputElement.addEventListener('input', updateCache, true); inputElement.addEventListener('compositionend', updateCache, true); } } // Update notification dot state updateNotificationDot(); // Using MemoryItem from src/types/content-scripts.ts function createMemoryModal( memoryItems: MemoryItem[], isLoading: boolean = false, sourceButtonId: string | null = null ): void { console.log('🎯 createMemoryModal called', { memoryItems, isLoading, sourceButtonId, memoryModalShown, }); // Close existing modal if it exists if (memoryModalShown) { console.log('🗂️ Closing existing modal'); try { if (typeof currentModalUnplace === 'function') { currentModalUnplace(); } } catch { // Ignore error } if (currentModalContainer && currentModalContainer.isConnected) { try { currentModalContainer.remove(); } catch { // Ignore error } } if (currentModalOverlay && document.body.contains(currentModalOverlay)) { document.body.removeChild(currentModalOverlay); } } memoryModalShown = true; console.log('✅ Modal state set to shown'); let currentMemoryIndex = 0; // Calculate modal dimensions (estimated) const modalWidth = 447; let modalHeight = 400; // Default height let memoriesPerPage = 3; // Default number of memories per page // Resolve anchor element function resolveAnchor() { if (sourceButtonId) { const byId = document.getElementById(sourceButtonId); if (byId) { return byId; } } const iconBtn = document.querySelector('#mem0-icon-button'); if (iconBtn) { return iconBtn; } return ( document.querySelector('div[contenteditable="true"]') || document.querySelector('textarea') || document.querySelector('p[data-placeholder="How can I help you today?"]') || document.querySelector('p[data-placeholder="Reply to Claude..."]') ); } const anchorEl = resolveAnchor(); // Choose placement side and adjust height/page density let placementSide = 'bottom'; // let placementAlign = 'end'; if (anchorEl) { const r = anchorEl.getBoundingClientRect(); const viewportHeight = window.innerHeight; if (r.top >= modalHeight + 10) { placementSide = 'top'; memoriesPerPage = 3; } else { placementSide = 'bottom'; if (r.bottom > viewportHeight / 2) { modalHeight = 300; memoriesPerPage = 2; } } } // Create modal overlay const modalOverlay = document.createElement('div'); modalOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: transparent; display: flex; z-index: 10000; pointer-events: auto; `; // Save reference to current modal overlay currentModalOverlay = modalOverlay; // Add event listener to close modal when clicking outside modalOverlay.addEventListener('click', event => { // Only close if clicking directly on the overlay, not its children if (event.target === modalOverlay) { closeModal(); } }); // Create modal container (placement handled by OPENMEMORY_UI) const modalContainer = document.createElement('div'); modalContainer.style.cssText = ` background-color: #1C1C1E; border-radius: 12px; width: ${modalWidth}px; height: ${modalHeight}px; display: flex; flex-direction: column; color: white; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); pointer-events: auto; border: 1px solid #27272A; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; overflow: hidden; `; // Place near the anchor using the new positioning util let unplace = () => { // Ignore error }; if (OPENMEMORY_UI && anchorEl) { // Prefer SITE_CONFIG for Claude to dock within the composer box let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.claude ? SITE_CONFIG.claude : null; let placementCfg = (cfg && cfg.placement) || { strategy: 'dock' as const, container: 'form', side: (placementSide === 'top' ? 'top' : 'bottom') as 'top' | 'bottom', align: 'start' as const, gap: 8, }; unplace = OPENMEMORY_UI.applyPlacement({ container: modalContainer, anchor: anchorEl, placement: placementCfg, }); } else { // Fallback placement document.body.appendChild(modalContainer); Object.assign(modalContainer.style, { position: 'fixed', left: '50%', top: '20%', transform: 'translateX(-50%)', zIndex: '2147483647', }); } // Track current container/unplace for cleanup on next open currentModalContainer = modalContainer; currentModalUnplace = unplace; // Create modal header const modalHeader = document.createElement('div'); modalHeader.style.cssText = ` display: flex; align-items: center; padding: 10px 16px; justify-content: space-between; background-color: #232325; flex-shrink: 0; cursor: move; user-select: none; `; // Add drag functionality let isDragging = false; let dragOffset = { x: 0, y: 0 }; modalHeader.addEventListener('mousedown', e => { // Only start drag if clicking on the header itself, not buttons if ( e.target === modalHeader || e.target === title || e.target === logoImg || e.target === headerLeft ) { isDragging = true; const rect = modalContainer.getBoundingClientRect(); dragOffset.x = e.clientX - rect.left; dragOffset.y = e.clientY - rect.top; // Switch to fixed positioning for dragging modalContainer.style.position = 'fixed'; modalContainer.style.left = rect.left + 'px'; modalContainer.style.top = rect.top + 'px'; modalContainer.style.transform = 'none'; modalContainer.style.zIndex = '2147483647'; e.preventDefault(); } }); document.addEventListener('mousemove', e => { if (isDragging && modalContainer) { const newX = e.clientX - dragOffset.x; const newY = e.clientY - dragOffset.y; // Keep modal within viewport bounds const maxX = window.innerWidth - modalContainer.offsetWidth; const maxY = window.innerHeight - modalContainer.offsetHeight; modalContainer.style.left = Math.max(0, Math.min(newX, maxX)) + 'px'; modalContainer.style.top = Math.max(0, Math.min(newY, maxY)) + 'px'; } }); document.addEventListener('mouseup', () => { isDragging = false; }); // Create header left section with just the logo const headerLeft = document.createElement('div'); headerLeft.style.cssText = ` display: flex; flex-direction: row; align-items: center; `; // Add Mem0 logo const logoImg = document.createElement('img'); logoImg.src = chrome.runtime.getURL('icons/mem0-claude-icon.png'); logoImg.style.cssText = ` width: 26px; height: 26px; border-radius: 50%; margin-right: 10px; `; // Add "OpenMemory" title const title = document.createElement('div'); title.textContent = 'OpenMemory'; title.style.cssText = ` font-size: 16px; font-weight: 600; color: white; `; // Create header right section const headerRight = document.createElement('div'); headerRight.style.cssText = ` display: flex; flex-direction: row; align-items: center; gap: 8px; `; // Create Add to Prompt button with arrow const addToPromptBtn = document.createElement('button'); addToPromptBtn.style.cssText = ` display: flex; flex-direction: row; align-items: center; padding: 5px 16px; gap: 8px; background-color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 12px; font-weight: 600; color: black; `; addToPromptBtn.textContent = 'Add to Prompt'; // Add arrow icon to button const arrowIcon = document.createElement('span'); arrowIcon.innerHTML = ` `; addToPromptBtn.appendChild(arrowIcon); // Create settings button const settingsBtn = document.createElement('button'); settingsBtn.style.cssText = ` background: none; border: none; cursor: pointer; padding: 8px; opacity: 0.6; transition: opacity 0.2s; `; settingsBtn.innerHTML = ` `; // Add click event to open app.mem0.ai in a new tab settingsBtn.addEventListener('click', () => { if (currentModalOverlay && document.body.contains(currentModalOverlay)) { document.body.removeChild(currentModalOverlay); memoryModalShown = false; currentModalOverlay = null; } chrome.runtime.sendMessage({ action: SidebarAction.SIDEBAR_SETTINGS }); }); // Add hover effect for the settings button settingsBtn.addEventListener('mouseenter', () => { settingsBtn.style.opacity = '1'; }); settingsBtn.addEventListener('mouseleave', () => { settingsBtn.style.opacity = '0.6'; }); // Content section const contentSection = document.createElement('div'); const contentSectionHeight = modalHeight - 130; // Account for header and navigation contentSection.style.cssText = ` display: flex; flex-direction: column; padding: 0 16px; gap: 12px; overflow: hidden; flex: 1; height: ${contentSectionHeight}px; `; // Create memories counter const memoriesCounter = document.createElement('div'); memoriesCounter.style.cssText = ` font-size: 16px; font-weight: 600; color: #FFFFFF; margin-top: 16px; flex-shrink: 0; `; // Update counter text based on loading state and number of memories if (isLoading) { memoriesCounter.textContent = `Loading Relevant Memories...`; } else if (memoryItems.length === 0) { memoriesCounter.textContent = `No Relevant Memories`; } else { memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`; } // Calculate max height for memories content based on modal height const memoriesContentMaxHeight = contentSectionHeight - 40; // Account for memories counter // Create memories content container with adjusted height const memoriesContent = document.createElement('div'); memoriesContent.style.cssText = ` display: flex; flex-direction: column; gap: 8px; overflow-y: auto; flex: 1; max-height: ${memoriesContentMaxHeight}px; padding-right: 8px; margin-right: -8px; scrollbar-width: none; -ms-overflow-style: none; `; memoriesContent.style.cssText += '::-webkit-scrollbar { display: none; }'; // Track currently expanded memory let currentlyExpandedMemory: HTMLElement | null = null; // Function to create skeleton loading items function createSkeletonItems() { memoriesContent.innerHTML = ''; for (let i = 0; i < memoriesPerPage; i++) { const skeletonItem = document.createElement('div'); skeletonItem.style.cssText = ` display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; padding: 12px; background-color: #27272A; border-radius: 8px; height: 72px; flex-shrink: 0; animation: pulse 1.5s infinite ease-in-out; `; const skeletonText = document.createElement('div'); skeletonText.style.cssText = ` background-color: #383838; border-radius: 4px; height: 14px; width: 85%; margin-bottom: 8px; `; const skeletonText2 = document.createElement('div'); skeletonText2.style.cssText = ` background-color: #383838; border-radius: 4px; height: 14px; width: 65%; `; const skeletonActions = document.createElement('div'); skeletonActions.style.cssText = ` display: flex; gap: 4px; margin-left: 10px; `; const skeletonButton1 = document.createElement('div'); skeletonButton1.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; background-color: #383838; `; const skeletonButton2 = document.createElement('div'); skeletonButton2.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; background-color: #383838; `; skeletonActions.appendChild(skeletonButton1); skeletonActions.appendChild(skeletonButton2); const textContainer = document.createElement('div'); textContainer.style.cssText = ` display: flex; flex-direction: column; flex-grow: 1; `; textContainer.appendChild(skeletonText); textContainer.appendChild(skeletonText2); skeletonItem.appendChild(textContainer); skeletonItem.appendChild(skeletonActions); memoriesContent.appendChild(skeletonItem); } // Add keyframe animation to document if not exists if (!document.getElementById('skeleton-animation')) { const style = document.createElement('style'); style.id = 'skeleton-animation'; style.innerHTML = ` @keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 0.8; } 100% { opacity: 0.6; } } `; document.head.appendChild(style); } } // Function to expand memory function expandMemory( memoryContainer: HTMLDivElement, memoryText: HTMLDivElement, contentWrapper: HTMLDivElement, removeButton: HTMLButtonElement, isExpanded: { value: boolean } ) { if (currentlyExpandedMemory && currentlyExpandedMemory !== memoryContainer) { currentlyExpandedMemory.dispatchEvent(new Event('collapse')); } isExpanded.value = true; memoryText.style.webkitLineClamp = 'unset'; memoryText.style.height = 'auto'; contentWrapper.style.overflowY = 'auto'; contentWrapper.style.maxHeight = '240px'; // Limit height to prevent overflow contentWrapper.style.scrollbarWidth = 'none'; contentWrapper.style.msOverflowStyle = 'none'; contentWrapper.style.cssText += '::-webkit-scrollbar { display: none; }'; memoryContainer.style.backgroundColor = '#1C1C1E'; memoryContainer.style.maxHeight = '300px'; // Allow expansion but within container memoryContainer.style.overflow = 'hidden'; removeButton.style.display = 'flex'; currentlyExpandedMemory = memoryContainer; // Scroll to make expanded memory visible if needed memoriesContent.scrollTop = memoryContainer.offsetTop - memoriesContent.offsetTop; } // Function to collapse memory function collapseMemory( memoryContainer: HTMLDivElement, memoryText: HTMLDivElement, contentWrapper: HTMLDivElement, removeButton: HTMLButtonElement, isExpanded: { value: boolean } ) { isExpanded.value = false; memoryText.style.webkitLineClamp = '2'; memoryText.style.height = '42px'; contentWrapper.style.overflowY = 'visible'; memoryContainer.style.backgroundColor = '#27272A'; memoryContainer.style.maxHeight = '72px'; memoryContainer.style.overflow = 'hidden'; removeButton.style.display = 'none'; currentlyExpandedMemory = null; } // Function to show memories with adjusted count based on modal position function showMemories() { memoriesContent.innerHTML = ''; if (isLoading) { createSkeletonItems(); return; } if (memoryItems.length === 0) { showEmptyState(); updateNavigationState(0, 0); return; } // Reset Add to Prompt button state if (addToPromptBtn) { addToPromptBtn.disabled = false; addToPromptBtn.style.opacity = '1'; addToPromptBtn.style.cursor = 'pointer'; } // Use the dynamically set memoriesPerPage value const memoriesToShow = Math.min(memoriesPerPage, memoryItems.length); // Calculate total pages and current page const totalPages = Math.ceil(memoryItems.length / memoriesToShow); const currentPage = Math.floor(currentMemoryIndex / memoriesToShow) + 1; // Update navigation buttons state updateNavigationState(currentPage, totalPages); for (let i = 0; i < memoriesToShow; i++) { const memoryIndex = currentMemoryIndex + i; if (memoryIndex >= memoryItems.length) { break; } // Stop if we've reached the end const memory = memoryItems[memoryIndex]; if (!memory) { continue; } // Skip memories that have been added already if (allMemoriesById.has(String(memory.id))) { continue; } // Ensure memory has an ID if (!memory.id) { memory.id = `memory-${Date.now()}-${memoryIndex}`; } const memoryContainer = document.createElement('div'); memoryContainer.style.cssText = ` display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; padding: 12px; background-color: #27272A; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; min-height: 72px; max-height: 72px; overflow: hidden; flex-shrink: 0; `; const memoryText = document.createElement('div'); memoryText.style.cssText = ` font-size: 14px; line-height: 1.5; color: #D4D4D8; flex-grow: 1; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; transition: all 0.2s ease; height: 42px; /* Height for 2 lines of text */ `; memoryText.textContent = memory.text || ''; const actionsContainer = document.createElement('div'); actionsContainer.style.cssText = ` display: flex; gap: 4px; margin-left: 10px; flex-shrink: 0; `; // Add button const addButton = document.createElement('button'); addButton.style.cssText = ` border: none; cursor: pointer; padding: 4px; background:rgb(66, 66, 69); color:rgb(199, 199, 201); border-radius: 100%; transition: all 0.2s ease; `; addButton.innerHTML = ` `; // Add click handler for add button addButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); sendExtensionEvent('memory_injection', { provider: 'claude', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), injected_all: false, memory_id: memory.id, }); // Mark this memory as added allMemoriesById.add(String(memory.id)); // Add this memory to existing ones instead of replacing allMemories.push(String(memory.text || '')); // Update the input with all memories updateInputWithMemories(); // Remove this memory from the list const index = memoryItems.findIndex((m: MemoryItem) => m.id === memory.id); if (index !== -1) { memoryItems.splice(index, 1); // Recalculate pagination after removing an item // If we're on a page that's now empty, go to previous page if (currentMemoryIndex > 0 && currentMemoryIndex >= memoryItems.length) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); } memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`; showMemories(); } // Don't close the modal, allow adding more memories }); // Menu button const menuButton = document.createElement('button'); menuButton.style.cssText = ` background: none; border: none; cursor: pointer; padding: 4px; color: #A1A1AA; `; menuButton.innerHTML = ` `; // Track expanded state using object to maintain reference const isExpanded = { value: false }; // Create remove button (hidden by default) const removeButton = document.createElement('button'); removeButton.style.cssText = ` display: none; align-items: center; gap: 6px; background:rgb(66, 66, 69); color:rgb(199, 199, 201); border-radius: 8px; padding: 2px 4px; border: none; cursor: pointer; font-size: 13px; margin-top: 12px; width: fit-content; `; removeButton.innerHTML = ` Remove `; // Create content wrapper for text and remove button const contentWrapper = document.createElement('div'); contentWrapper.style.cssText = ` display: flex; flex-direction: column; flex-grow: 1; `; contentWrapper.appendChild(memoryText); contentWrapper.appendChild(removeButton); memoryContainer.addEventListener('collapse', () => { collapseMemory(memoryContainer, memoryText, contentWrapper, removeButton, isExpanded); }); menuButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); if (isExpanded.value) { collapseMemory(memoryContainer, memoryText, contentWrapper, removeButton, isExpanded); } else { expandMemory(memoryContainer, memoryText, contentWrapper, removeButton, isExpanded); } }); // Add click handler for remove button removeButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); // Remove from memoryItems const index = memoryItems.findIndex((m: MemoryItem) => m.id === memory.id); if (index !== -1) { memoryItems.splice(index, 1); // Recalculate pagination after removing an item // If we're on the last page and it's now empty, go to previous page if (currentMemoryIndex > 0 && currentMemoryIndex >= memoryItems.length) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); } memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`; showMemories(); } }); actionsContainer.appendChild(addButton); actionsContainer.appendChild(menuButton); memoryContainer.appendChild(contentWrapper); memoryContainer.appendChild(actionsContainer); memoriesContent.appendChild(memoryContainer); // Add hover effect memoryContainer.addEventListener('mouseenter', () => { memoryContainer.style.backgroundColor = isExpanded.value ? '#18181B' : '#323232'; }); memoryContainer.addEventListener('mouseleave', () => { memoryContainer.style.backgroundColor = isExpanded.value ? '#1C1C1E' : '#27272A'; }); } // If after filtering for already added memories, there are no items to show, // check if we need to go to previous page if (memoriesContent.children.length === 0 && memoryItems.length > 0) { if (currentMemoryIndex > 0) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); showMemories(); } else { updateNavigationState(0, 0); showEmptyState(); } } } // Function to show empty state function showEmptyState() { memoriesContent.innerHTML = ''; memoriesCounter.textContent = 'No Relevant Memories'; const emptyContainer = document.createElement('div'); emptyContainer.style.cssText = ` display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 32px 16px; text-align: center; flex: 1; min-height: 200px; `; const emptyIcon = document.createElement('div'); emptyIcon.innerHTML = ` `; emptyIcon.style.marginBottom = '16px'; const emptyText = document.createElement('div'); emptyText.textContent = 'No relevant memories found'; emptyText.style.cssText = ` color: #71717A; font-size: 14px; font-weight: 500; `; emptyContainer.appendChild(emptyIcon); emptyContainer.appendChild(emptyText); memoriesContent.appendChild(emptyContainer); // Disable the Add to Prompt button when there are no memories if (addToPromptBtn) { addToPromptBtn.disabled = true; addToPromptBtn.style.opacity = '0.5'; addToPromptBtn.style.cursor = 'not-allowed'; } } // Update navigation button states function updateNavigationState(currentPage: number, totalPages: number): void { if (memoryItems.length === 0 || totalPages === 0) { prevButton.disabled = true; prevButton.style.opacity = '0.5'; prevButton.style.cursor = 'not-allowed'; nextButton.disabled = true; nextButton.style.opacity = '0.5'; nextButton.style.cursor = 'not-allowed'; return; } if (currentPage <= 1) { prevButton.disabled = true; prevButton.style.opacity = '0.5'; prevButton.style.cursor = 'not-allowed'; } else { prevButton.disabled = false; prevButton.style.opacity = '1'; prevButton.style.cursor = 'pointer'; } if (currentPage >= totalPages) { nextButton.disabled = true; nextButton.style.opacity = '0.5'; nextButton.style.cursor = 'not-allowed'; } else { nextButton.disabled = false; nextButton.style.opacity = '1'; nextButton.style.cursor = 'pointer'; } } // Navigation section at bottom const navigationSection = document.createElement('div'); navigationSection.style.cssText = ` display: flex; justify-content: center; gap: 12px; padding: 10px; border-top: none; flex-shrink: 0; `; // Navigation buttons const prevButton = document.createElement('button'); prevButton.innerHTML = ` `; prevButton.style.cssText = ` background: #27272A; border: none; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.2s; `; const nextButton = document.createElement('button'); nextButton.innerHTML = ` `; nextButton.style.cssText = prevButton.style.cssText; // Add navigation button handlers prevButton.addEventListener('click', () => { if (currentMemoryIndex >= memoriesPerPage) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); showMemories(); } }); nextButton.addEventListener('click', () => { if (currentMemoryIndex + memoriesPerPage < memoryItems.length) { currentMemoryIndex = currentMemoryIndex + memoriesPerPage; showMemories(); } }); // Add hover effects [prevButton, nextButton].forEach(button => { button.addEventListener('mouseenter', () => { if (!button.disabled) { button.style.backgroundColor = '#323232'; } }); button.addEventListener('mouseleave', () => { if (!button.disabled) { button.style.backgroundColor = '#27272A'; } }); }); // Assemble modal headerLeft.appendChild(logoImg); headerLeft.appendChild(title); headerRight.appendChild(addToPromptBtn); headerRight.appendChild(settingsBtn); modalHeader.appendChild(headerLeft); modalHeader.appendChild(headerRight); // Drag behavior removed in favor of utility-based placement contentSection.appendChild(memoriesCounter); contentSection.appendChild(memoriesContent); navigationSection.appendChild(prevButton); navigationSection.appendChild(nextButton); modalContainer.appendChild(modalHeader); modalContainer.appendChild(contentSection); modalContainer.appendChild(navigationSection); // When using floating placement, modalContainer is already in the DOM. // Always append overlay last so outside-click works and layering is correct. document.body.appendChild(modalOverlay); // Show initial memories or loading state if (isLoading) { createSkeletonItems(); } else if (memoryItems.length === 0) { showEmptyState(); } else { showMemories(); } // Function to close the modal function closeModal(): void { console.log('🚪 Closing modal'); try { if (typeof unplace === 'function') { unplace(); } } catch { // Ignore error } try { if (typeof currentModalUnplace === 'function') { currentModalUnplace(); } } catch { // Ignore error } if (currentModalContainer && currentModalContainer.isConnected) { try { currentModalContainer.remove(); } catch { // Ignore error } } if (currentModalOverlay && document.body.contains(currentModalOverlay)) { // Clean up drag event listeners document.body.removeChild(currentModalOverlay); } if (modalOverlay && document.body.contains(modalOverlay)) { document.body.removeChild(modalOverlay); } currentModalOverlay = null; currentModalContainer = null; currentModalUnplace = null; memoryModalShown = false; isProcessingMem0 = false; } // Update Add to Prompt button click handler addToPromptBtn.addEventListener('click', () => { // Only add memories that are not already added const newMemories = memoryItems .filter(memory => !allMemoriesById.has(String(memory.id)) && !memory.removed) .map(memory => { allMemoriesById.add(String(memory.id)); return String(memory.text || ''); }); sendExtensionEvent('memory_injection', { provider: 'claude', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), injected_all: true, memory_count: newMemories.length, }); // Add new memories to allMemories (don't replace existing ones) if (newMemories.length > 0) { // Add new memories to the existing array allMemories = [...allMemories, ...newMemories]; // Update the input with all memories updateInputWithMemories(); } // Close the modal closeModal(); // Remove all added memories from the memoryItems list for (let i = memoryItems.length - 1; i >= 0; i--) { if (allMemoriesById.has(String(memoryItems[i]?.id))) { memoryItems.splice(i, 1); } } }); } // Shared function to update the input field with all collected memories function updateInputWithMemories() { // Find the input element (prioritizing the ProseMirror div with contenteditable="true") let inputElement = document.querySelector('div[contenteditable="true"].ProseMirror'); // If ProseMirror not found, try other input elements if (!inputElement) { inputElement = document.querySelector('div[contenteditable="true"]') || document.querySelector('textarea') || document.querySelector('p[data-placeholder="How can I help you today?"]') || document.querySelector('p[data-placeholder="Reply to Claude..."]'); } if (inputElement && allMemories.length > 0) { // Define the header text const headerText = OPENMEMORY_PROMPTS.memory_header_text; // Check if ProseMirror editor if (inputElement.classList.contains('ProseMirror')) { // First check if the header already exists const headerExists = Array.from(inputElement.querySelectorAll('p strong')).some(el => (el.textContent || '').includes('Here is some of my memories') ); if (headerExists) { // Get all existing memory paragraphs const paragraphs = Array.from(inputElement.querySelectorAll('p')) as HTMLElement[]; let headerIndex = -1; const existingMemories = []; // Find the index of the header paragraph for (let i = 0; i < paragraphs.length; i++) { const strongEl = paragraphs[i]?.querySelector('strong'); if (strongEl && (strongEl.textContent || '').includes('Here is some of my memories')) { headerIndex = i; break; } } // Collect all existing memories after the header if (headerIndex >= 0) { for (let i = headerIndex + 1; i < paragraphs.length; i++) { const para = paragraphs[i]; if (!para) { continue; } const text = (para.textContent || '').trim(); if (text.startsWith('-')) { existingMemories.push(text.substring(1).trim()); } } // Keep everything up to and including the header paragraph const newHTML = Array.from(paragraphs) .slice(0, headerIndex + 1) .map(p => p.outerHTML) .join(''); // Combine existing and new memories, avoiding duplicates const combinedMemories = [...existingMemories]; // Add new memories if they don't already exist allMemories.forEach(mem => { if (!combinedMemories.includes(mem)) { combinedMemories.push(mem); } }); // Add the memories after the header const memoriesHTML = combinedMemories.map(mem => `

- ${mem}

`).join(''); // Set the new HTML content inputElement.innerHTML = newHTML + memoriesHTML; } } else { // Header doesn't exist, get the content without any existing memory wrappers const baseContent = getContentWithoutMemories(undefined); // Create the memory section let memoriesContent = `

${headerText}

`; // Add all memories to the content with proper paragraph tags memoriesContent += allMemories.map(mem => `

- ${mem}

`).join(''); // If empty, replace the entire content if ( !baseContent || baseContent.trim() === '' || (inputElement.querySelectorAll('p').length === 1 && inputElement.querySelector('p.is-empty') !== null) ) { inputElement.innerHTML = memoriesContent; } else { // Otherwise append after a line break inputElement.innerHTML = `${baseContent}


${memoriesContent}`; } } // Dispatch proper events for ProseMirror const inputEvent = new InputEvent('input', { bubbles: true, cancelable: true, inputType: 'insertText', }); inputElement.dispatchEvent(inputEvent); // Also dispatch a change event const changeEvent = new Event('change', { bubbles: true }); inputElement.dispatchEvent(changeEvent); } else if (inputElement.tagName.toLowerCase() === 'div') { // For normal contenteditable divs // Check if the header already exists if (inputElement.innerHTML.includes(headerText)) { // Find the header position and extract existing memories const htmlParts = inputElement.innerHTML.split(headerText); if (htmlParts.length > 1) { const beforeHeader = htmlParts[0]; const afterHeader = htmlParts[1]; // Extract existing memories from the content after the header const tempDiv = document.createElement('div'); tempDiv.innerHTML = afterHeader || ''; const existingMemories: string[] = []; // Find all paragraphs that start with a dash Array.from(tempDiv.querySelectorAll('p')).forEach(p => { const text = (p.textContent || '').trim(); if (text.startsWith('-')) { existingMemories.push(text.substring(1).trim()); } }); // Combine existing and new memories, avoiding duplicates const combinedMemories = [...existingMemories]; // Add new memories if they don't already exist allMemories.forEach(mem => { if (!combinedMemories.includes(mem)) { combinedMemories.push(mem); } }); // Create HTML with header and all memories let newHTML = beforeHeader + `

${headerText}

`; combinedMemories.forEach(mem => { newHTML += `

- ${mem}

`; }); inputElement.innerHTML = newHTML; } } else { // Header doesn't exist const baseContent = getContentWithoutMemories(undefined); let memoriesContent = `

${headerText}

`; allMemories.forEach(mem => { memoriesContent += `

- ${mem}

`; }); inputElement.innerHTML = `${baseContent}${baseContent ? '


' : ''}${memoriesContent}`; } // Dispatch input event inputElement.dispatchEvent(new Event('input', { bubbles: true })); } else if ( inputElement.tagName.toLowerCase() === 'p' && (inputElement.getAttribute('data-placeholder') === 'How can I help you today?' || inputElement.getAttribute('data-placeholder') === 'Reply to Claude...') ) { // For p element placeholders // Check if the header already exists if ((inputElement.textContent || '').includes(headerText)) { // Find the header position and extract existing memories const textParts = (inputElement.textContent || '').split(headerText); if (textParts.length > 1) { const beforeHeader = textParts[0]; const afterHeader = textParts[1]; // Extract existing memories const existingMemories: string[] = []; const memoryLines = (afterHeader || '').split('\n'); memoryLines.forEach(line => { const trimmed = line.trim(); if (trimmed.startsWith('-')) { existingMemories.push(trimmed.substring(1).trim()); } }); // Combine existing and new memories, avoiding duplicates const combinedMemories = [...existingMemories]; // Add new memories if they don't already exist allMemories.forEach(mem => { if (!combinedMemories.includes(mem)) { combinedMemories.push(mem); } }); // Create text with header and all memories const newText = beforeHeader + headerText + '\n\n' + combinedMemories.map(mem => `- ${mem}`).join('\n'); inputElement.textContent = newText; } } else { // Header doesn't exist const baseContent = getContentWithoutMemories(undefined); inputElement.textContent = `${baseContent}${baseContent ? '\n\n' : ''}${headerText}\n\n${allMemories.map(mem => `- ${mem}`).join('\n')}`; } // Dispatch various events inputElement.dispatchEvent(new Event('input', { bubbles: true })); inputElement.dispatchEvent(new Event('focus', { bubbles: true })); inputElement.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true })); inputElement.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true })); inputElement.dispatchEvent(new Event('change', { bubbles: true })); } else { // For textarea // Check if the header already exists if (((inputElement as HTMLTextAreaElement).value || '').includes(headerText)) { // Find the header position and extract existing memories const valueParts = ((inputElement as HTMLTextAreaElement).value || '').split(headerText); if (valueParts.length > 1) { const beforeHeader = valueParts[0]; const afterHeader = valueParts[1]; // Extract existing memories const existingMemories: string[] = []; const memoryLines = (afterHeader || '').split('\n'); memoryLines.forEach(line => { const trimmed = line.trim(); if (trimmed.startsWith('-')) { existingMemories.push(trimmed.substring(1).trim()); } }); // Combine existing and new memories, avoiding duplicates const combinedMemories = [...existingMemories]; // Add new memories if they don't already exist allMemories.forEach(mem => { if (!combinedMemories.includes(mem)) { combinedMemories.push(mem); } }); // Create text with header and all memories const newValue = beforeHeader + headerText + '\n\n' + combinedMemories.map(mem => `- ${mem}`).join('\n'); inputElement.value = newValue; } } else { // Header doesn't exist const baseContent = getContentWithoutMemories(undefined); (inputElement as HTMLTextAreaElement).value = `${baseContent}${baseContent ? '\n\n' : ''}${headerText}\n\n${allMemories.map(mem => `- ${mem}`).join('\n')}`; } // Dispatch input event inputElement.dispatchEvent(new Event('input', { bubbles: true })); } // Focus the input element to ensure the user can continue typing (inputElement as HTMLElement).focus(); } } // Function to get the content without any memory wrappers function getContentWithoutMemories(providedMessage: string | undefined) { // Find the input element (prioritizing the ProseMirror div with contenteditable="true") let inputElement = document.querySelector('div[contenteditable="true"].ProseMirror'); // If ProseMirror not found, try other input elements if (!inputElement) { inputElement = document.querySelector('div[contenteditable="true"]') || document.querySelector('textarea') || document.querySelector('p[data-placeholder="How can I help you today?"]') || document.querySelector('p[data-placeholder="Reply to Claude..."]'); } // If a message is provided, operate on it; otherwise read from DOM let content = ''; if (typeof providedMessage === 'string') { content = providedMessage; } else { if (!inputElement) { return ''; } if (inputElement.classList.contains('ProseMirror')) { // For ProseMirror, get the innerHTML for proper structure handling content = inputElement.innerHTML; } else if (inputElement.tagName.toLowerCase() === 'div') { // For normal contenteditable divs content = inputElement.innerHTML; } else if ( inputElement.tagName.toLowerCase() === 'p' && (inputElement.getAttribute('data-placeholder') === 'How can I help you today?' || inputElement.getAttribute('data-placeholder') === 'Reply to Claude...') ) { // For p element placeholders content = inputElement.innerHTML || inputElement.textContent || ''; } else { // For textarea content = (inputElement as HTMLTextAreaElement).value || ''; } } // Remove any memory headers and content // Match both HTML and plain text variants // HTML variant try { const MEM0_HTML = OPENMEMORY_PROMPTS.memory_header_html_regex; const MEM0_PLAIN = OPENMEMORY_PROMPTS.memory_header_plain_regex; content = content.replace(MEM0_HTML, ''); content = content.replace(MEM0_PLAIN, ''); } catch { // Ignore errors when processing memory content } // Also clean up any empty paragraphs at the end content = content.replace(/


<\/p>$/g, ''); content = content.replace( /


<\/p>$/g, '' ); return content.trim(); } // New function to handle the memory modal async function handleMem0Modal( popup: HTMLElement | null, clickSendButton: boolean = false, sourceButtonId: string | null = null ): Promise { console.log('🚀 handleMem0Modal called', { popup, clickSendButton, sourceButtonId, isProcessingMem0, memoryModalShown, }); if (isProcessingMem0) { console.log('⏸️ Already processing, returning early'); return; } if (memoryModalShown) { console.log('📱 Modal already shown, returning early'); return; } // First check if memory is enabled const enabled = await getMemoryEnabledState(); console.log('🧠 Memory enabled state:', enabled); if (enabled === false) { console.log('❌ Memory disabled, not showing modal'); return; // Don't show modal or login popup if memory is disabled } isProcessingMem0 = true; // Set loading state for button setButtonLoadingState(); // Hide any tooltip that might be showing const tooltip = document.querySelector('#mem0-tooltip'); if (tooltip) { tooltip.style.display = 'none'; } try { const data = await new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, StorageKey.SIMILARITY_THRESHOLD, StorageKey.TOP_K, ], function (items) { resolve(items); } ); }); const apiKey = data.apiKey; const userId = data.userId || data.user_id || 'chrome-extension-user'; const accessToken = data.access_token; if (!apiKey && !accessToken) { // Show login popup instead of error message isProcessingMem0 = false; setButtonLoadingState(); showLoginPopup(); return; } let message = getInputValue(); console.log('📝 Input message:', message); if (!message || message.trim() === '') { console.log('❌ No input message, showing popup'); if (popup) { // Hide any existing tooltip first const tooltip = document.querySelector('#mem0-tooltip'); if (tooltip) { tooltip.style.display = 'none'; } showPopup(popup, 'Please enter some text first'); } isProcessingMem0 = false; setButtonLoadingState(); return; } console.log('✅ All checks passed, creating memory modal'); // Now we can show the loading modal since we have text input createMemoryModal([], true, sourceButtonId); // Clean the message by removing any existing memory wrappers message = getContentWithoutMemories(undefined); // Strip HTML tags to ensure clean text for search (fix for

tag issue) const tempDiv = document.createElement('div'); tempDiv.innerHTML = message; message = tempDiv.textContent || tempDiv.innerText || message; message = message.trim(); sendExtensionEvent('modal_clicked', { provider: 'claude', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), }); const authHeader = accessToken ? `Bearer ${accessToken}` : `Token ${apiKey}`; const messages = getConversationContext(false); // Use sliding window context messages.push({ role: MessageRole.User, content: message }); // If clickSendButton is true, click the send button if (clickSendButton) { const sendButton = (document.querySelector('button[aria-label="Send Message"]') as HTMLElement) || (document.querySelector('button[aria-label="Send message"]') as HTMLElement); if (sendButton) { setTimeout(() => { (sendButton as HTMLElement).click(); }, 100); } } const optionalParams: OptionalApiParams = {}; if (data.selected_org) { optionalParams.org_id = data.selected_org; } if (data.selected_project) { optionalParams.project_id = data.selected_project; } // Use raw input for search key to match typing cache; cleaning happens in fetch currentModalSourceButtonId = sourceButtonId; try { const rawInput = (function () { const el = document.querySelector('div[contenteditable="true"]') || document.querySelector('textarea') || document.querySelector('p[data-placeholder="How can I help you today?"]') || document.querySelector('p[data-placeholder="Reply to Claude..."]'); if (!el) { return message; } const val = (el.textContent || el.value || '').trim(); return val || message; })(); console.log("Claude modal search for:", rawInput); console.log("Cache state:", claudeSearch.getState()); claudeSearch.runImmediate(rawInput); } catch (_) { claudeSearch.runImmediate(message); } // New add memory API call (non-blocking) fetch('https://api.mem0.ai/v1/memories/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify({ messages: messages, user_id: userId, infer: true, metadata: { provider: 'Claude', }, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }), }) .then(response => { if (!response.ok) { // Silent failure for background memory addition } }) .catch(() => { // Silent failure for background memory addition }); } catch { if (popup) { showPopup(popup, 'Failed to send message to Mem0'); } } finally { isProcessingMem0 = false; setButtonLoadingState(); } } function setButtonLoadingState(): void { // Legacy mem0-button state removed; no-op } function showPopup(popup: HTMLElement, message: string): void { // Legacy tooltip suppressed; keep popup container-only usage if (!popup) { return; } Array.from(document.querySelectorAll('#mem0-tooltip')).forEach(p => { try { p.remove(); } catch { // Ignore error } }); popup.textContent = message; popup.style.display = 'block'; setTimeout(() => { popup.style.display = 'none'; }, 2000); } function getInputValue(): string | null { const inputElement = document.querySelector('div[contenteditable="true"]') || document.querySelector('textarea') || document.querySelector('p[data-placeholder="How can I help you today?"]') || document.querySelector('p[data-placeholder="Reply to Claude..."]'); if (!inputElement) { return null; } // For the p element placeholders specifically if ( inputElement.tagName.toLowerCase() === 'p' && (inputElement.getAttribute('data-placeholder') === 'How can I help you today?' || inputElement.getAttribute('data-placeholder') === 'Reply to Claude...') ) { return inputElement.textContent || ''; } return inputElement.textContent || (inputElement as HTMLTextAreaElement)?.value || null; } let claudeBackgroundSearchHandler: (() => void) | null = null; function hookClaudeBackgroundSearchTyping() { const inputElement = document.querySelector('div[contenteditable="true"]') || document.querySelector('textarea') || document.querySelector('p[data-placeholder="How can I help you today?"]') || document.querySelector('p[data-placeholder="Reply to Claude..."]'); if (!inputElement) { return; } if (inputElement.dataset.claudeBackgroundHooked) { return; } inputElement.dataset.claudeBackgroundHooked = 'true'; if (!claudeBackgroundSearchHandler) { claudeBackgroundSearchHandler = function () { let text = getInputValue() || ''; try { const MEM0_PLAIN = OPENMEMORY_PROMPTS.memory_header_plain_regex; text = text.replace(MEM0_PLAIN, '').trim(); } catch {} console.log("Claude background search triggered:", text); claudeSearch.setText(text); }; } inputElement.addEventListener('input', claudeBackgroundSearchHandler); inputElement.addEventListener('keyup', claudeBackgroundSearchHandler); } // Auto-inject support: simple debounce and config async function updateMemoryEnabled() { memoryEnabled = Boolean(await getMemoryEnabledState()); // If memory is disabled, remove the button completely if (memoryEnabled === false) { removeMemButton(); } else { // If memory is enabled, ensure the button is added // addMem0Button(); // Removed: OPENMEMORY_UI handles icon mounting } } function initializeMem0Integration(): void { console.log('🚀 initializeMem0Integration started'); updateMemoryEnabled(); hookClaudeBackgroundSearchTyping(); if (!(OPENMEMORY_UI && OPENMEMORY_UI.mountOnEditorFocus)) { // addMem0Button(); // Removed: OPENMEMORY_UI handles icon mounting } // Prime the cache so the very first send is captured const _initVal = getInputValue(); if (_initVal && _initVal.trim()) { lastTyped = _initVal; } // Ensure send button listeners are attached early and repeatedly const ensureSendButtonListeners = () => { const allSendButtons = [ document.querySelector('button[aria-label="Send Message"]'), document.querySelector('button[aria-label="Send message"]'), ].filter(Boolean); allSendButtons.forEach(sendBtn => { if (sendBtn && !sendBtn.dataset.mem0Listener) { sendBtn.dataset.mem0Listener = 'true'; // Snapshot current input as early as possible (before Claude clears it) sendBtn.addEventListener( 'pointerdown', function () { const current = getInputValue(); if (current && current.trim() !== '') { lastTyped = current; } lastSendInitiatedAt = Date.now(); }, true ); // Use capture-phase click so we run before Claude's handler sendBtn.addEventListener( 'click', function () { // Capture and save memory with snapshot fallback captureAndStoreMemory(lastTyped); // Clear all memories after sending setTimeout(() => { allMemories = []; allMemoriesById.clear(); }, 100); }, true ); } }); }; // Attach listeners immediately ensureSendButtonListeners(); // Also attach them repeatedly during early page load const earlyAttachInterval = setInterval(() => { ensureSendButtonListeners(); }, 100); // Stop the aggressive checking after page is more stable setTimeout(() => { clearInterval(earlyAttachInterval); }, 5000); // Refresh cache whenever the editor gains focus if (!(document as ExtendedDocument).__mem0FocusPrimed) { (document as ExtendedDocument).__mem0FocusPrimed = true; document.addEventListener( 'focusin', e => { const target = e.target as Element | null; const el = target && (target as ExtendedElement).closest && (target as ExtendedElement).closest( 'div[contenteditable="true"], textarea, p[data-placeholder="How can I help you today?"], p[data-placeholder="Reply to Claude..."]' ); if (el) { const v = getInputValue(); if (v && v.trim()) { lastTyped = v; } } }, true ); } document.addEventListener('keydown', function (event) { if (event.ctrlKey && event.key === 'm') { event.preventDefault(); console.log('⌨️ Ctrl+M pressed', { memoryEnabled, memoryModalShown, isProcessingMem0 }); // If modal is already shown, close it instead of creating duplicate if (memoryModalShown) { console.log('🔄 Modal already shown, closing it'); if (currentModalOverlay && document.body.contains(currentModalOverlay)) { document.body.removeChild(currentModalOverlay); } memoryModalShown = false; currentModalOverlay = null; return; } if (memoryEnabled && !isProcessingMem0) { const popup = document.querySelector('.mem0-popup'); if (popup) { (async () => { await handleMem0Modal(popup as HTMLElement, false); })(); } else { // If no popup is available, use the mem0-icon-button as source (async () => { await handleMem0Modal(null, false, 'mem0-icon-button'); })(); } } } }); // Cache-first mount for Claude (left/bottom placement relative to textarea) console.log('🎯 Starting cache-first mount for Claude'); function renderMem0Icon(shadow: ShadowRoot, host: HTMLElement) { console.log('🎨 renderMem0Icon called', { shadow, host }); let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { console.log('🖱️ Mem0 button clicked!'); handleMem0Modal(null, false, 'mem0-icon-button'); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } } try { console.log('🔍 Checking for existing button and OPENMEMORY_UI', { existingButton: !!document.getElementById('mem0-icon-button'), hasOpenMemoryUI: !!OPENMEMORY_UI, }); // Only create button if none exists (including shadow DOM buttons) const existingButton = document.getElementById('mem0-icon-button') || document.querySelector('[id*="mem0"]') || document.querySelector('.mem0-btn'); if (!existingButton && OPENMEMORY_UI) { console.log('🚀 Starting OPENMEMORY_UI.resolveCachedAnchor'); OPENMEMORY_UI.resolveCachedAnchor( { learnKey: location.host + ':' + location.pathname }, null, 24 * 60 * 60 * 1000 ) .then(function (hit: { el: Element; placement: Placement | null } | null) { console.log('🎯 resolveCachedAnchor result:', hit); if (!hit || !hit.el) { console.log('❌ No anchor found, cannot create button'); return; } console.log('✅ Anchor found, creating button'); let hs = OPENMEMORY_UI.createShadowRootHost('mem0-root'); let host = hs.host, shadow = hs.shadow; host.id = 'mem0-icon-button'; let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.claude ? SITE_CONFIG.claude : null; let placement = hit.placement || (cfg && cfg.placement) || { strategy: 'dock', container: 'form', side: 'bottom', align: 'start', gap: 8, }; OPENMEMORY_UI.applyPlacement({ container: host, anchor: hit.el, placement: placement }); renderMem0Icon(shadow, host); }) .catch(function (error: Error) { console.error('❌ Error in resolveCachedAnchor:', error); }); } else { console.log('❌ Cannot create button:', { buttonExists: !!document.getElementById('mem0-icon-button'), hasOpenMemoryUI: !!OPENMEMORY_UI, }); } } catch (error) { // Ignore errors during re-initialization console.error('❌ Error in cache-first mount:', error); } // Focus-driven mount for Claude console.log('🎯 Checking focus-driven mount'); if ( OPENMEMORY_UI && OPENMEMORY_UI.mountOnEditorFocus && !document.querySelector('[id*="mem0"], .mem0-btn') ) { console.log('🚀 Starting focus-driven mount'); OPENMEMORY_UI.mountOnEditorFocus({ existingHostSelector: '#mem0-icon-button, [id*="mem0"], .mem0-btn', editorSelector: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.claude && SITE_CONFIG.claude.editorSelector ? SITE_CONFIG.claude.editorSelector : 'div[contenteditable="true"], textarea, p[data-placeholder], [contenteditable="true"]', deriveAnchor: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.claude && typeof SITE_CONFIG.claude.deriveAnchor === 'function' ? SITE_CONFIG.claude.deriveAnchor : function (editor: Element) { return editor.closest('form') || editor.parentElement; }, placement: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.claude && SITE_CONFIG.claude.placement ? SITE_CONFIG.claude.placement : { strategy: 'dock', container: 'form', side: 'bottom', align: 'start', gap: 8 }, render: function (shadow: ShadowRoot, host: HTMLElement) { host.id = 'mem0-icon-button'; return renderMem0Icon(shadow, host); }, fallback: function () { try { let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.claude ? SITE_CONFIG.claude : null; if (!cfg || !OPENMEMORY_UI || !OPENMEMORY_UI.mountResilient) { return; } OPENMEMORY_UI.mountResilient({ anchors: cfg.fallbackAnchors || [], placement: cfg.placement, enableFloatingFallback: true, render: function (shadow: ShadowRoot, host: HTMLElement) { host.id = 'mem0-icon-button'; return renderMem0Icon(shadow, host); }, }); } catch { // Ignore error } }, }); } // Global early keydown capture for Enter to snapshot the very first send if (!(document as ExtendedDocument).__mem0EnterCapture) { (document as ExtendedDocument).__mem0EnterCapture = true; document.addEventListener( 'keydown', e => { if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) { const v = getInputValue(); if (v && v.trim()) { lastTyped = v; lastSendInitiatedAt = Date.now(); } } }, true ); } // Global submit capture to catch earliest send even if buttons/forms change if (!(document as ExtendedDocument).__mem0SubmitCapture) { (document as ExtendedDocument).__mem0SubmitCapture = true; document.addEventListener( 'submit', () => { const v = getInputValue(); if (v && v.trim()) { lastTyped = v; } lastSendInitiatedAt = Date.now(); captureAndStoreMemory(lastTyped); }, true ); } // Find the input element and observe it // function observeInput() { // const inputElement = // document.querySelector('div[contenteditable="true"]') || // document.querySelector('textarea') || // document.querySelector('p[data-placeholder="How can I help you today?"]') || // document.querySelector('p[data-placeholder="Reply to Claude..."]'); // if (inputElement) { // inputObserver = new MutationObserver(() => { // // Observer callback - can be empty for now // }); // inputObserver.observe(inputElement, { // childList: true, // characterData: true, // subtree: true, // }); // } else { // // If no input element found, try again later // setTimeout(observeInput, 1000); // } // } chrome.storage.onChanged.addListener((changes, namespace) => { if (namespace === 'sync' && changes.memory_enabled) { updateMemoryEnabled(); } }); // Fallback: observe chat thread for newly added user bubbles and post if we missed send const ensureThreadObserver = () => { const thread = document.querySelector( '.flex-1.flex.flex-col.gap-3.px-4.max-w-3xl.mx-auto.w-full' ); if (!thread) { setTimeout(ensureThreadObserver, 1000); return; } if ((thread as ExtendedElement).__mem0Observed) { return; } (thread as ExtendedElement).__mem0Observed = true; // Track processed messages to avoid duplicates const processedMessages = new Set(); const observer = new MutationObserver(mutations => { for (let i = 0; i < mutations.length; i++) { const m = mutations[i]; if (!m) { continue; } const added = m.addedNodes; for (let j = 0; j < added.length; j++) { const n = added[j]; if (!n) { continue; } const node = (n as ExtendedElement).nodeType === 1 ? (n as Element) : null; if (!node) { continue; } const userEl = (node as ExtendedElement).matches?.('.font-user-message') ? node : (node as ExtendedElement).querySelector?.('.font-user-message'); if (userEl) { const text = (userEl.textContent || '').trim(); if (text) { // Create a simple hash of the message to avoid duplicates const messageHash = text.length + '_' + text.substring(0, 50); // Skip if we've already processed this exact message recently if (processedMessages.has(messageHash)) { return; } // Add to processed set and clean up old entries periodically processedMessages.add(messageHash); if (processedMessages.size > 10) { const entries = Array.from(processedMessages); processedMessages.clear(); // Keep the last 5 entries entries.slice(-5).forEach(entry => processedMessages.add(entry)); } // If we just initiated a send, give primary handlers a brief head start const justInitiated = Date.now() - lastSendInitiatedAt < 500; // Reduced from 1200ms to 500ms if (justInitiated) { // For very recent sends, delay slightly to let primary handlers run first setTimeout(() => { // Double-check if we still need to process this message if (!processedMessages.has(messageHash + '_processed')) { processedMessages.add(messageHash + '_processed'); captureAndStoreMemory(text); } }, 200); } else { // For older messages or when no recent send detected, process immediately processedMessages.add(messageHash + '_processed'); captureAndStoreMemory(text); } // Update lastSendInitiatedAt to help coordinate with other handlers lastSendInitiatedAt = Date.now(); return; } } } } }); observer.observe(thread, { childList: true, subtree: true }); }; ensureThreadObserver(); } // Function to show login popup function showLoginPopup() { // First remove any existing popups const existingPopup = document.querySelector('#mem0-login-popup'); if (existingPopup) { existingPopup.remove(); } // Create popup container const popupOverlay = document.createElement('div'); popupOverlay.id = 'mem0-login-popup'; popupOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10001; `; const popupContainer = document.createElement('div'); popupContainer.style.cssText = ` background-color: #1C1C1E; border-radius: 12px; width: 320px; padding: 24px; color: white; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; // Close button const closeButton = document.createElement('button'); closeButton.style.cssText = ` position: absolute; top: 16px; right: 16px; background: none; border: none; color: #A1A1AA; font-size: 16px; cursor: pointer; `; closeButton.innerHTML = '×'; closeButton.addEventListener('click', () => { document.body.removeChild(popupOverlay); }); // Logo and heading const logoContainer = document.createElement('div'); logoContainer.style.cssText = ` display: flex; align-items: center; justify-content: center; margin-bottom: 16px; `; const logo = document.createElement('img'); logo.src = chrome.runtime.getURL('icons/mem0-claude-icon.png'); logo.style.cssText = ` width: 24px; height: 24px; border-radius: 50%; margin-right: 12px; `; const logoDark = document.createElement('img'); logoDark.src = chrome.runtime.getURL('icons/mem0-icon-black.png'); logoDark.style.cssText = ` width: 24px; height: 24px; border-radius: 50%; margin-right: 12px; `; const heading = document.createElement('h2'); heading.textContent = 'Sign in to OpenMemory'; heading.style.cssText = ` margin: 0; font-size: 18px; font-weight: 600; `; logoContainer.appendChild(heading); // Message const message = document.createElement('p'); message.textContent = 'Please sign in to access your memories and personalize your conversations!'; message.style.cssText = ` margin-bottom: 24px; color: #D4D4D8; font-size: 14px; line-height: 1.5; text-align: center; `; // Sign in button const signInButton = document.createElement('button'); signInButton.style.cssText = ` display: flex; align-items: center; justify-content: center; width: 100%; padding: 10px; background-color: white; color: black; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: background-color 0.2s; `; // Add text in span for better centering const signInText = document.createElement('span'); signInText.textContent = 'Sign in with Mem0'; signInButton.appendChild(logoDark); signInButton.appendChild(signInText); signInButton.addEventListener('mouseenter', () => { signInButton.style.backgroundColor = '#f5f5f5'; }); signInButton.addEventListener('mouseleave', () => { signInButton.style.backgroundColor = 'white'; }); // Open sign-in page when clicked signInButton.addEventListener('click', () => { window.open('https://app.mem0.ai/login', '_blank'); document.body.removeChild(popupOverlay); }); // Assemble popup popupContainer.appendChild(logoContainer); popupContainer.appendChild(message); popupContainer.appendChild(signInButton); popupOverlay.appendChild(popupContainer); popupOverlay.appendChild(closeButton); // Add click event to close when clicking outside popupOverlay.addEventListener('click', e => { if (e.target === popupOverlay) { document.body.removeChild(popupOverlay); } }); // Add to body document.body.appendChild(popupOverlay); } // Function to capture and store memory asynchronously async function captureAndStoreMemory(snapshot: string) { // Check if extension context is valid if (!chrome || !chrome.storage) { return; } try { // Check if memory is enabled const memoryEnabled = await getMemoryEnabledState(); if (memoryEnabled === false) { return; // Don't process memories if disabled } } catch (_) { // Ignore error return; } // Use the provided snapshot directly if available, otherwise try to get from input let message = typeof snapshot === 'string' && snapshot.trim() !== '' ? snapshot : ''; if (!message) { // Find the input element (prioritizing the ProseMirror div with contenteditable="true") let inputElement = document.querySelector('div[contenteditable="true"].ProseMirror'); // If ProseMirror not found, try other input elements if (!inputElement) { inputElement = document.querySelector('div[contenteditable="true"]') || document.querySelector('textarea') || document.querySelector('p[data-placeholder="How can I help you today?"]') || document.querySelector('p[data-placeholder="Reply to Claude..."]'); } if (!inputElement) { return; } if (inputElement.classList.contains('ProseMirror')) { // For ProseMirror, get the textContent for plain text message = inputElement.textContent || ''; } else if (inputElement.tagName.toLowerCase() === 'div') { message = inputElement.textContent || ''; } else if (inputElement.tagName.toLowerCase() === 'p') { message = inputElement.textContent || ''; } else { message = inputElement.value || ''; } } if (!message || message.trim() === '') { return; } // For ProseMirror, the getContentWithoutMemories returns HTML, so we need to extract text if (typeof snapshot !== 'string' || !snapshot.trim()) { message = getContentWithoutMemories(message); } // Skip if message is empty after processing if (!message || message.trim() === '') { return; } // Asynchronously store the memory try { // Check extension context again before storage access if (!chrome || !chrome.storage || !chrome.storage.sync) { return; } chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.MEMORY_ENABLED, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, ], function (items) { // Check for chrome.runtime.lastError which indicates extension context issues try { // @ts-ignore if (chrome.runtime && (chrome.runtime as ChromeRuntime).lastError) { return; } } catch { // Ignore errors when checking chrome.runtime.lastError } // Skip if memory is disabled or no credentials if (items.memory_enabled === false || (!items.apiKey && !items.access_token)) { return; } const authHeader = items.access_token ? `Bearer ${items.access_token}` : `Token ${items.apiKey}`; const userId = items.userId || items.user_id || 'chrome-extension-user'; // Get recent messages for context using sliding window const contextMessages = getConversationContext(false); // Don't include current message yet contextMessages.push({ role: MessageRole.User, content: message }); // Add current message const optionalParams: OptionalApiParams = {}; if (items.selected_org) { optionalParams.org_id = items.selected_org; } if (items.selected_project) { optionalParams.project_id = items.selected_project; } // Send memory to mem0 API asynchronously without waiting for response fetch('https://api.mem0.ai/v1/memories/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify({ messages: contextMessages, user_id: userId, infer: true, metadata: { provider: 'Claude', }, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }), }) .then(response => { if (!response.ok) { // Silent failure for background memory addition } }) .catch(() => { // Silent failure for background memory addition }); } ); } catch { // Silent failure for background memory addition } } // Function to update the notification dot function updateNotificationDot() { // Find all Mem0 notification dots const notificationDots = document.querySelectorAll('#mem0-notification-dot'); if (!notificationDots.length) { return; } // Find the input element (prioritizing the ProseMirror div with contenteditable="true") let inputElement = document.querySelector('div[contenteditable="true"].ProseMirror'); // If ProseMirror not found, try other input elements if (!inputElement) { inputElement = document.querySelector('div[contenteditable="true"]') || document.querySelector('textarea') || document.querySelector('p[data-placeholder="How can I help you today?"]') || document.querySelector('p[data-placeholder="Reply to Claude..."]'); } if (!inputElement) { return; } // Function to check if input has text const checkForText = () => { let hasText = false; // Check for text based on the input type if (inputElement.classList.contains('ProseMirror')) { // For ProseMirror, check if it has any content other than just a placeholder

const paragraphs = inputElement.querySelectorAll('p'); // Check if there's text content or if there are multiple paragraphs (not just empty placeholder) const textContent = (inputElement.textContent || '').trim(); hasText = textContent !== '' || paragraphs.length > 1 || (paragraphs.length === 1 && !(paragraphs[0] as HTMLElement | undefined)?.classList?.contains('is-empty')); } else if (inputElement.tagName.toLowerCase() === 'p') { // For p elements with placeholder hasText = (inputElement.textContent || '').trim() !== ''; } else if (inputElement.tagName.toLowerCase() === 'div') { // For normal contenteditable divs hasText = (inputElement.textContent || '').trim() !== ''; } else { // For textareas hasText = (inputElement.value || '').trim() !== ''; } // Update all notification dots notificationDots.forEach(notificationDot => { if (hasText) { notificationDot.classList.add('active'); notificationDot.style.display = 'block'; } else { notificationDot.classList.remove('active'); notificationDot.style.display = 'none'; } }); }; // Setup mutation observer for the input element to detect changes const observer = new MutationObserver(checkForText); observer.observe(inputElement, { childList: true, characterData: true, subtree: true, attributes: true, attributeFilter: ['class'], }); // Also listen for direct input events inputElement.addEventListener('input', checkForText); inputElement.addEventListener('keyup', checkForText); inputElement.addEventListener('focus', checkForText); // Initial check checkForText(); // Force another check after a small delay to ensure DOM is fully loaded setTimeout(checkForText, 500); } // Enhanced DOM-based message detection since CSP blocks network interception let domMonitoringActive = false; function setupEnhancedDOMMonitoring() { if (domMonitoringActive) { return; } domMonitoringActive = true; // Enhanced real-time message monitoring with multiple strategies function setupRealTimeMessageMonitoring() { const threadSelector = '.flex-1.flex.flex-col.gap-3.px-4.max-w-3xl.mx-auto.w-full'; // Strategy 1: Monitor for new user message elements const messageObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { const el = node as Element; // Look for user messages const userMessage = el.querySelector('.font-user-message'); if (userMessage) { const text = (userMessage.textContent || '').trim(); if (text) { // Add to conversation history addToConversationHistory(MessageRole.User, text); setTimeout(() => { captureAndStoreMemory(text); setTimeout(() => { allMemories = []; allMemoriesById.clear(); }, 100); }, 50); } } // Also check if the node itself is a user message if ( (el as ExtendedElement).classList && (el as ExtendedElement).classList.contains('font-user-message') ) { const text = (el.textContent || '').trim(); if (text) { // Add to conversation history addToConversationHistory(MessageRole.User, text); setTimeout(() => { captureAndStoreMemory(text); setTimeout(() => { allMemories = []; allMemoriesById.clear(); }, 100); }, 50); } } // Also look for Claude's assistant messages const assistantMessage = el.querySelector('.font-claude-message'); if (assistantMessage) { const text = (assistantMessage.textContent || '').trim(); if (text) { // Add to conversation history addToConversationHistory(MessageRole.Assistant, text); } } // Check if the node itself is an assistant message if ( (el as ExtendedElement).classList && (el as ExtendedElement).classList.contains('font-claude-message') ) { const text = (el.textContent || '').trim(); if (text) { // Add to conversation history addToConversationHistory(MessageRole.Assistant, text); } } } }); }); }); // Find and observe the thread const thread = document.querySelector(threadSelector); if (thread) { messageObserver.observe(thread, { childList: true, subtree: true, attributes: false, characterData: false, }); } else { // Retry finding the thread setTimeout(setupRealTimeMessageMonitoring, 1000); } } // Strategy 2: Monitor input clearing as a signal that message was sent function setupInputClearingMonitor() { const inputSelectors = [ 'div[contenteditable="true"].ProseMirror', 'div[contenteditable="true"]', 'textarea', 'p[data-placeholder="How can I help you today?"]', 'p[data-placeholder="Reply to Claude..."]', ]; let lastInputValue = ''; let inputClearingObserver: MutationObserver | undefined; function findAndObserveInput() { for (const selector of inputSelectors) { const input = document.querySelector(selector); if (input) { // Disconnect any existing observer if (inputClearingObserver) { inputClearingObserver.disconnect(); } inputClearingObserver = new MutationObserver(() => { const currentValue = getInputValue() || ''; // Check if input was cleared (had content, now empty) if (lastInputValue.trim() && !currentValue.trim()) { // Add to conversation history addToConversationHistory(MessageRole.User, lastInputValue); setTimeout(() => { captureAndStoreMemory(lastInputValue); setTimeout(() => { allMemories = []; allMemoriesById.clear(); }, 100); }, 50); } lastInputValue = currentValue; }); inputClearingObserver.observe(input, { childList: true, subtree: true, characterData: true, attributes: true, }); // Also listen for input events input.addEventListener('input', () => { lastInputValue = getInputValue() || ''; }); break; } } } findAndObserveInput(); // Re-find input periodically in case DOM changes setInterval(findAndObserveInput, 5000); } // Start all monitoring strategies setupRealTimeMessageMonitoring(); setupInputClearingMonitor(); } // CSP blocks script injection, so focus on content script level approaches // Add extension context monitoring let extensionContextValid = true; let currentUrl = window.location.href; function checkExtensionContext() { // @ts-ignore const isValid = !!(chrome && chrome.runtime); if (extensionContextValid && !isValid) { extensionContextValid = false; } return isValid; } // Function to detect URL changes (SPA navigation) function detectNavigation() { const newUrl = window.location.href; if (newUrl !== currentUrl) { const wasNewChat = currentUrl.includes('/new') || currentUrl.includes('/chat/new'); const isNewChat = newUrl.includes('/new') || newUrl.includes('/chat/new'); const isDifferentChat = currentUrl.includes('/chat/') && newUrl.includes('/chat/') && currentUrl !== newUrl; // Clear conversation history when navigating to a new chat or different chat if (isNewChat || isDifferentChat || wasNewChat) { conversationHistory = []; } // Reset DOM monitoring flag so it can be re-setup for new page domMonitoringActive = false; currentUrl = newUrl; // Re-initialize everything after navigation setTimeout(() => { // Re-initialize conversation history from new DOM initializeConversationHistoryFromDOM(); hookClaudeBackgroundSearchTyping(); // Re-add buttons and listeners if (!(OPENMEMORY_UI && OPENMEMORY_UI.mountOnEditorFocus)) { // addMem0Button(); // Removed: OPENMEMORY_UI handles icon mounting } // Re-setup enhanced DOM monitoring for new page setupEnhancedDOMMonitoring(); // Update notification dot updateNotificationDot(); }, 500); // Small delay to let DOM update } } // Check for navigation every 1 second (more frequent than context check) setInterval(() => { checkExtensionContext(); detectNavigation(); }, 1000); // Also listen for browser navigation events for faster detection window.addEventListener('popstate', () => { setTimeout(detectNavigation, 100); }); // Override pushState to catch programmatic navigation const originalPushState = history.pushState; history.pushState = function (data: HistoryStateData, unused: string, url?: string | URL | null) { originalPushState.call(history, data, unused, url); setTimeout(detectNavigation, 100); }; // Override replaceState to catch programmatic navigation const originalReplaceState = history.replaceState; history.replaceState = function ( data: HistoryStateData, unused: string, url?: string | URL | null ) { originalReplaceState.call(history, data, unused, url); setTimeout(detectNavigation, 100); }; // Initialize conversation history from existing messages initializeConversationHistoryFromDOM(); // Set up enhanced DOM monitoring setupEnhancedDOMMonitoring(); // Main initialization console.log('🎯 Starting main initialization'); initializeMem0Integration(); ================================================ FILE: src/context-menu-memory.ts ================================================ import { type ApiMemoryRequest, DEFAULT_USER_ID, MessageRole, SOURCE } from './types/api'; import { MessageType, type SelectionContextResponse, type ToastMessage, ToastVariant, } from './types/messages'; import { Category, Provider } from './types/providers'; import type { Settings } from './types/settings'; import { StorageKey } from './types/storage'; export function initContextMenuMemory(): void { try { chrome.contextMenus.create( { id: 'mem0.saveSelection', title: 'Save to OpenMemory', contexts: ['selection'], }, () => { /* no-op */ } ); } catch { // ignore } chrome.contextMenus.onClicked.addListener( async (info: chrome.contextMenus.OnClickData, tab?: chrome.tabs.Tab) => { if ( !tab || tab.id === null || tab.id === undefined || info.menuItemId !== 'mem0.saveSelection' ) { return; } const tabId = tab.id; // Type narrowing - we know tab.id is not null or undefined here const selection = String(info.selectionText || '').trim(); if (!selection) { toast(tabId, 'Select text first', ToastVariant.ERROR); return; } const settings = await getSettings(); if (!settings.hasCreds) { toast(tabId, 'Sign in required', ToastVariant.ERROR); return; } if (settings.memoryEnabled === false) { toast(tabId, 'Memory is disabled in settings', ToastVariant.ERROR); return; } const title = tab.title || ''; const url = info.pageUrl || tab.url || ''; let ctx = await requestSelectionContext(tabId); if (ctx && ctx.error) { await tryInjectSelectionScript(tabId); ctx = await requestSelectionContext(tabId); } const content = composeBasic({ selection, title, url }); try { const ok = await addMemory(content, settings); toast( tabId, ok ? 'Saved to OpenMemory' : 'Failed to save', ok ? ToastVariant.SUCCESS : ToastVariant.ERROR ); } catch (err) { console.error('Failed to add memory:', err); toast(tabId, 'Failed to save', ToastVariant.ERROR); } } ); } function toast(tabId: number, message: string, variant: ToastVariant = ToastVariant.SUCCESS): void { try { const msg: ToastMessage = { type: MessageType.TOAST, payload: { message, variant } }; chrome.tabs.sendMessage(tabId, msg); } catch { // Best effort only } } function normalize(text: string): string { return (text || '').replace(/\s+/g, ' ').trim(); } function clamp(text: string, max: number): string { if (!text) { return text; } if (text.length <= max) { return text; } return text.slice(0, max - 1).trimEnd() + '…'; } function composeBasic({ selection }: { selection: string; title: string; url: string }): string { const s = clamp(normalize(selection), 700); // Return raw selection only (no prefixes). We keep title/url only in metadata. return s; } function requestSelectionContext(tabId: number): Promise { return new Promise(resolve => { try { chrome.tabs.sendMessage( tabId, { type: MessageType.GET_SELECTION_CONTEXT }, undefined, (resp?: SelectionContextResponse) => { resolve(resp || { error: 'no-response' }); } ); } catch (e) { resolve({ error: String(e) }); } }); } async function tryInjectSelectionScript(tabId: number): Promise { try { if (!chrome.scripting) { return false; } await chrome.scripting.executeScript({ target: { tabId }, files: ['selection_context.ts'], }); return true; } catch { return false; } } function getSettings(): Promise { return new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.ACCESS_TOKEN, StorageKey.USER_ID, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.MEMORY_ENABLED, ], d => { resolve({ hasCreds: Boolean(d[StorageKey.API_KEY] || d[StorageKey.ACCESS_TOKEN]), apiKey: d[StorageKey.API_KEY], accessToken: d[StorageKey.ACCESS_TOKEN], userId: d[StorageKey.USER_ID] || DEFAULT_USER_ID, orgId: d[StorageKey.SELECTED_ORG], projectId: d[StorageKey.SELECTED_PROJECT], memoryEnabled: d[StorageKey.MEMORY_ENABLED] !== false, }); } ); }); } async function addMemory(content: string, settings: Settings): Promise { const headers: Record = { 'Content-Type': 'application/json' }; if (settings.accessToken) { headers.Authorization = `Bearer ${settings.accessToken}`; } else if (settings.apiKey) { headers.Authorization = `Token ${settings.apiKey}`; } else { throw new Error('Missing credentials'); } const body: ApiMemoryRequest = { messages: [{ role: MessageRole.User, content }], user_id: settings.userId, metadata: { provider: Provider.ContextMenu, category: Category.BOOKMARK, }, source: SOURCE, }; if (settings.orgId) { body.org_id = settings.orgId; } if (settings.projectId) { body.project_id = settings.projectId; } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); try { const res = await fetch('https://api.mem0.ai/v1/memories/', { method: 'POST', headers, body: JSON.stringify(body), signal: controller.signal, }); return res.ok; } finally { clearTimeout(timeout); } } ================================================ FILE: src/deepseek/content.ts ================================================ import { MessageRole } from '../types/api'; import type { ExtendedHTMLElement } from '../types/dom'; import type { MemoryItem, MemorySearchItem, OptionalApiParams } from '../types/memory'; import { SidebarAction } from '../types/messages'; import { StorageKey } from '../types/storage'; import { createOrchestrator, type SearchStorage } from '../utils/background_search'; import { OPENMEMORY_PROMPTS } from '../utils/llm_prompts'; import { SITE_CONFIG } from '../utils/site_config'; import { getBrowser, sendExtensionEvent } from '../utils/util_functions'; import { OPENMEMORY_UI, type Placement } from '../utils/util_positioning'; export {}; const INPUT_SELECTOR = "#chat-input, textarea, [contenteditable='true']"; // Helper function to check if a node matches ignored selectors function isIgnoredNode(node: Element, ignoredSelectors: string[]): boolean { if (node.nodeType !== Node.ELEMENT_NODE) { return false; } // Check self for (const selector of ignoredSelectors) { if (node.matches && node.matches(selector)) { return true; } } // Check parents up to 3 levels let parent: HTMLElement | null = node.parentElement; let level = 0; while (parent && level < 3) { for (const selector of ignoredSelectors) { if (parent.matches && parent.matches(selector)) { return true; } } parent = parent.parentElement; level++; } return false; } // Function to expand memory function expandMemory( memoryContainer: HTMLElement, memoryText: HTMLElement, contentWrapper: HTMLElement, removeButton: HTMLElement, currentlyExpandedMemory: HTMLElement | null, memoriesContent: HTMLElement ) { if (currentlyExpandedMemory && currentlyExpandedMemory !== memoryContainer) { currentlyExpandedMemory.dispatchEvent(new Event('collapse')); } memoryText.style.webkitLineClamp = 'unset'; memoryText.style.height = 'auto'; contentWrapper.style.overflowY = 'auto'; contentWrapper.style.maxHeight = '240px'; // Limit height to prevent overflow contentWrapper.style.scrollbarWidth = 'none'; contentWrapper.style.msOverflowStyle = 'none'; contentWrapper.style.cssText += '::-webkit-scrollbar { display: none; }'; memoryContainer.style.backgroundColor = '#1C1C1E'; memoryContainer.style.maxHeight = '300px'; // Allow expansion but within container memoryContainer.style.overflow = 'hidden'; removeButton.style.display = 'flex'; currentlyExpandedMemory = memoryContainer; // Scroll to make expanded memory visible if needed memoriesContent.scrollTop = memoryContainer.offsetTop - memoriesContent.offsetTop; } // Function to collapse memory function collapseMemory( memoryContainer: HTMLElement, memoryText: HTMLElement, contentWrapper: HTMLElement, removeButton: HTMLElement ) { memoryText.style.webkitLineClamp = '2'; memoryText.style.height = '42px'; contentWrapper.style.overflowY = 'visible'; memoryContainer.style.backgroundColor = '#27272A'; memoryContainer.style.maxHeight = '52px'; memoryContainer.style.overflow = 'hidden'; removeButton.style.display = 'none'; } // Initialize memory tracking variables let isProcessingMem0: boolean = false; let observer: MutationObserver; let memoryModalShown: boolean = false; let allMemories: string[] = []; let allMemoriesById: Set = new Set(); let currentModalOverlay: HTMLDivElement | null = null; let mem0ButtonCheckInterval: ReturnType | null = null; // Add interval variable for button checks let modalDragPosition: { left: number; top: number } | null = null; // Store the dragged position of the modal // Using MemoryItem from src/types/content-scripts.ts (includes memory field for compatibility) // Function to remove the Mem0 icon button when memory is disabled function removeMem0IconButton() { const iconButton = document.querySelector('#mem0-icon-button'); if (iconButton) { const buttonContainer = iconButton.closest('div'); if (buttonContainer && buttonContainer.id !== 'mem0-custom-container') { // Only remove the button, not the container unless it's our custom one try { buttonContainer.removeChild(iconButton); } catch { // If removal fails, try removing just the button iconButton.remove(); } } else { // Remove the button directly iconButton.remove(); } } // Also remove custom container if it exists const customContainer = document.querySelector('#mem0-custom-container'); if (customContainer) { customContainer.remove(); } } function getInputElement() { // Try finding with the more specific selector first const inputElement = document.querySelector(INPUT_SELECTOR); if (inputElement) { return inputElement; } // If not found, try a more general approach // Try finding by common input attributes const textareas = document.querySelectorAll('textarea'); if (textareas.length > 0) { // Return the textarea that's visible and has the largest area (likely the main input) let bestMatch = null; let largestArea = 0; Array.from(textareas).forEach(textarea => { const rect = (textarea as HTMLTextAreaElement).getBoundingClientRect(); const isVisible = rect.width > 0 && rect.height > 0; const area = rect.width * rect.height; if (isVisible && area > largestArea) { largestArea = area; bestMatch = textarea as HTMLTextAreaElement; } }); if (bestMatch) { return bestMatch; } } // Try contenteditable divs const editableDivs = document.querySelectorAll('[contenteditable="true"]'); if (editableDivs.length > 0) { return editableDivs[0]; } // Try any element with role="textbox" const textboxes = document.querySelectorAll('[role="textbox"]'); if (textboxes.length > 0) { return textboxes[0]; } return null; } function getSendButtonElement(): HTMLElement | null { try { // Strategy 1: Look for buttons with send-like characteristics const buttons = document.querySelectorAll('div[role="button"]'); if (buttons.length === 0) { return null; } // Get the input element to help with positioning-based detection const inputElement = getInputElement(); const inputRect = inputElement ? (inputElement as HTMLElement).getBoundingClientRect() : null; // Find candidate buttons that might be send buttons let bestSendButton: HTMLElement | null = null; let bestScore = 0; Array.from(buttons).forEach(btn => { const button = btn as HTMLElement; // Skip if button is not visible or has no size const buttonRect = button.getBoundingClientRect(); if (buttonRect.width === 0 || buttonRect.height === 0) { return; } let score = 0; // 1. Check if it has an SVG (likely an icon button) const svg = button.querySelector('svg'); if (svg) { score += 2; } // 2. Check if it has no text content (icon-only buttons) const buttonText = (button.textContent || '').trim(); if (buttonText === '') { score += 2; } // 3. Check if it contains a paper airplane shape (common in send buttons) const paths = svg ? svg.querySelectorAll('path') : []; if (paths.length > 0) { score += 1; } // 4. Check positioning relative to input (send buttons are usually close to input) if (inputRect) { // Check if button is positioned to the right of input if (buttonRect.left > inputRect.left) { score += 1; } // Check if button is at similar height to input if (Math.abs(buttonRect.top - inputRect.top) < 100) { score += 2; } // Check if button is very close to input (right next to it) if (Math.abs(buttonRect.left - (inputRect.right + 20)) < 40) { score += 3; } } // 5. Check for DeepSeek specific classes if (button.classList.contains('ds-button--primary')) { score += 2; } // Update best match if this button has a higher score if (score > bestScore) { bestScore = score; bestSendButton = button; } }); // Return best match if score is reasonable if (bestScore >= 4) { return bestSendButton; } // Strategy 2: Look for buttons positioned at the right of the input if (inputElement && inputRect) { // Find buttons positioned to the right of the input const rightButtons = Array.from(buttons).filter(btn => { const buttonRect = (btn as HTMLElement).getBoundingClientRect(); return ( buttonRect.left > inputRect.right - 50 && // To the right Math.abs(buttonRect.top - inputRect.top) < 50 ); // Similar height }); // Sort by horizontal proximity to input rightButtons.sort((a, b) => { const aRect = (a as HTMLElement).getBoundingClientRect(); const bRect = (b as HTMLElement).getBoundingClientRect(); return aRect.left - inputRect.right - (bRect.left - inputRect.right); }); // Return the closest button if (rightButtons.length > 0) { return (rightButtons[0] as HTMLElement) || null; } } // Strategy 3: Last resort - take the last button with an SVG const svgButtons = Array.from(buttons).filter(btn => (btn as HTMLElement).querySelector('svg')); if (svgButtons.length > 0) { return svgButtons[svgButtons.length - 1] as HTMLElement; } return null; } catch { return null; // Return null on error instead of failing } } // Updated handleEnterKey with additional safety checks async function handleEnterKey(event: KeyboardEvent) { try { // Safety check - only proceed if we can identify an input element const inputElement = getInputElement(); if (!inputElement) { return; // Skip processing if no input found } // Only handle Enter without Shift and when target is the input element if (event.key === 'Enter' && !event.shiftKey && event.target === inputElement) { // Don't prevent default behavior yet until we've checked memory state // Check if memory is enabled let memoryEnabled = false; try { memoryEnabled = await getMemoryEnabledState(); } catch { return; // Don't interfere if we can't check memory state } if (!memoryEnabled) { return; // Let the default behavior proceed } // At this point, we know memory is enabled so let's handle the Enter key // Now prevent default since we'll handle the send ourselves event.preventDefault(); event.stopPropagation(); // Process memories and then send try { await handleMem0Processing(); } catch { triggerSendAction(); } } } catch { // Don't interfere with normal behavior if something goes wrong } } function initializeMem0Integration(): void { // Global flag to track initialization state window.mem0Initialized = window.mem0Initialized || false; // Reset initialization flag on navigation or visibility change document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { // Page likely navigated or became visible, reset initialization if (window.mem0Initialized) { setTimeout(() => { if (!document.querySelector('#mem0-icon-button')) { window.mem0Initialized = false; stageCriticalInit(); } }, 1000); } } }); // Avoid duplicating initialization if (window.mem0Initialized) { if (!document.querySelector('#mem0-icon-button')) { addMem0IconButton(); } return; } // Step 1: Wait for the page to be fully loaded before doing anything if (document.readyState !== 'complete') { window.addEventListener('load', function () { setTimeout(stageCriticalInit, 500); // Reduced wait time after load }); } else { // Page is already loaded, wait a moment and then initialize setTimeout(stageCriticalInit, 500); // Reduced wait time } // Stage 1: Initialize critical features (keyboard shortcuts, basic listeners) function stageCriticalInit() { try { // Early exit if already initialized if (window.mem0Initialized) { return; } // Add keyboard event listeners addKeyboardListeners(); // Add send button listener (non-blocking) setTimeout(() => { try { addSendButtonListener(); } catch { // Ignore errors } }, 2000); // Start background search typing hook once try { hookDeepseekBackgroundSearchTyping(); } catch { // Ignore errors } // Wait additional time for UI to stabilize setTimeout(stageUIInit, 1000); // Reduced time } catch { // Don't mark as initialized on error } } // Stage 2: Initialize UI components after the DOM has settled function stageUIInit() { try { // Early exit if already initialized if (window.mem0Initialized) { return; } // Set up the observer to detect UI changes setupObserver(); // Mark as initialized once we've completed both stages window.mem0Initialized = true; // Clear any existing interval if (mem0ButtonCheckInterval) { clearInterval(mem0ButtonCheckInterval); } // Set up periodic checks for button presence - check memory state first mem0ButtonCheckInterval = setInterval(async () => { try { const memoryEnabled = await getMemoryEnabledState(); if (memoryEnabled) { if (!document.querySelector('#mem0-icon-button')) { addMem0IconButton(); } } else { removeMem0IconButton(); } } catch { // On error, don't do anything } }, 5000); // Check every 5 seconds // Final check after more time setTimeout(async () => { try { const memoryEnabled = await getMemoryEnabledState(); if (memoryEnabled) { if (!document.querySelector('#mem0-icon-button')) { addMem0IconButton(); } } else { removeMem0IconButton(); } } catch { // On error, don't do anything } }, 5000); } catch { // Ignore errors } } // Add keyboard listeners with error handling function addKeyboardListeners() { try { // Skip if already added if (window.mem0KeyboardListenersAdded) { return; } // Listen for Enter key to handle memory processing document.addEventListener('keydown', handleEnterKey, true); // Listen for Ctrl+M to open the modal directly document.addEventListener('keydown', function (event) { if (event.ctrlKey && event.key === 'm') { event.preventDefault(); (async () => { try { await handleMem0Modal('mem0-icon-button'); } catch { // Ignore errors } })(); } }); window.mem0KeyboardListenersAdded = true; } catch { // Ignore errors } } // Set up mutation observer with throttling and filtering function setupObserver(): void { try { // Disconnect existing observer if any if (observer) { observer.disconnect(); } // Track when we last processed mutations let lastObserverRun = 0; const MIN_THROTTLE_MS = 3000; // Reduced from 10s to 3s const ignoredSelectors = [ '#mem0-icon-button', '.mem0-tooltip', '.mem0-tooltip-arrow', '#mem0-notification-dot', '#mem0-icon-button *', // Any children of the button ]; observer = new MutationObserver(mutations => { // Skip mutations on ignored elements const shouldIgnore = mutations.every(mutation => { // Check if the mutation target or parents match any ignored selectors const isIgnoredElement = (mutation.target as Node).nodeType === Node.ELEMENT_NODE ? isIgnoredNode(mutation.target as Element, ignoredSelectors) : false; // Check added nodes for tooltip/button related elements if (mutation.type === 'childList') { const addedIgnored = Array.from(mutation.addedNodes).some(node => { return ( node.nodeType === Node.ELEMENT_NODE && isIgnoredNode(node as Element, ignoredSelectors) ); }); if (addedIgnored) { return true; } } return isIgnoredElement; }); if (shouldIgnore) { return; // Skip these mutations } // Check if the button exists - no action needed if it does if (document.querySelector('#mem0-icon-button')) { return; } // Apply throttling const now = Date.now(); if (now - lastObserverRun < MIN_THROTTLE_MS) { return; // Too soon, skip } // Process mutations - just check and add button lastObserverRun = now; addMem0IconButton(); }); // Helper function to check if a node matches ignored selectors // isIgnoredNode function is defined above // Only observe high-level document changes to detect navigation observer.observe(document.body, { childList: true, subtree: true, attributes: false, attributeFilter: ['class', 'style'], // Only observe class/style changes }); } catch { // Ignore errors } } } async function getMemoryEnabledState(): Promise { return new Promise(resolve => { chrome.storage.sync.get( [StorageKey.MEMORY_ENABLED, StorageKey.API_KEY, StorageKey.ACCESS_TOKEN], data => { // Check if memory is enabled AND if we have auth credentials const hasAuth = !!data.apiKey || !!data.access_token; const memoryEnabled = !!data.memory_enabled; // Only consider logged in if both memory is enabled and auth credentials exist resolve(!!(memoryEnabled && hasAuth)); } ); }); } function getInputElementValue(): string | null { const inputElement = getInputElement(); const el = inputElement as HTMLTextAreaElement | HTMLDivElement | null; if (!el) { return null; } // Prefer textContent for contenteditable const text = (el as HTMLDivElement).textContent ?? (el as HTMLTextAreaElement).value ?? null; return text; } function getAuthDetails(): Promise<{ apiKey: string; accessToken: string; userId: string }> { return new Promise(resolve => { chrome.storage.sync.get( [StorageKey.API_KEY, StorageKey.ACCESS_TOKEN, StorageKey.USER_ID_CAMEL], items => { resolve({ apiKey: items.apiKey || null, accessToken: items.access_token || null, userId: items.userId || 'chrome-extension-user', }); } ); }); } const MEM0_API_BASE_URL = 'https://api.mem0.ai'; let currentModalSourceButtonId: string | null = null; const deepseekSearch = createOrchestrator({ fetch: async function (query: string, opts: { signal?: AbortSignal }) { const data = await new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, StorageKey.SIMILARITY_THRESHOLD, StorageKey.TOP_K, ], function (items) { resolve(items as SearchStorage); } ); }); const apiKey = data[StorageKey.API_KEY]; const accessToken = data[StorageKey.ACCESS_TOKEN]; if (!apiKey && !accessToken) { return []; } const authHeader = accessToken ? `Bearer ${accessToken}` : `Token ${apiKey}`; const userId = data[StorageKey.USER_ID_CAMEL] || data[StorageKey.USER_ID] || 'chrome-extension-user'; const threshold = data[StorageKey.SIMILARITY_THRESHOLD] !== undefined ? data[StorageKey.SIMILARITY_THRESHOLD] : 0.1; const topK = data[StorageKey.TOP_K] !== undefined ? data[StorageKey.TOP_K] : 10; const optionalParams: OptionalApiParams = {}; if (data[StorageKey.SELECTED_ORG]) { optionalParams.org_id = data[StorageKey.SELECTED_ORG]; } if (data[StorageKey.SELECTED_PROJECT]) { optionalParams.project_id = data[StorageKey.SELECTED_PROJECT]; } const payload = { query, filters: { user_id: userId }, rerank: true, threshold: threshold, top_k: topK, filter_memories: false, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }; const res = await fetch('https://api.mem0.ai/v2/memories/search/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify(payload), signal: opts && opts.signal, }); if (!res.ok) { throw new Error(`API request failed with status ${res.status}`); } return await res.json(); }, // Don’t render on prefetch. When modal is open, update it. onSuccess: function (normQuery: string, responseData: MemorySearchItem[]) { if (!memoryModalShown) { return; } const memoryItems = ((responseData as MemorySearchItem[]) || []).map( (item: MemorySearchItem) => ({ id: String(item.id), text: item.memory, categories: item.categories || [], }) ); createMemoryModal(memoryItems, false, currentModalSourceButtonId); }, onError: function () { if (memoryModalShown) { createMemoryModal([], false, currentModalSourceButtonId); } }, minLength: 3, debounceMs: 75, cacheTTL: 60000, }); let deepseekBackgroundSearchHandler: (() => void) | null = null; function hookDeepseekBackgroundSearchTyping() { const inputEl = getInputElement(); if (!inputEl) { return; } if (inputEl.dataset.deepseekBackgroundHooked) { return; } inputEl.dataset.deepseekBackgroundHooked = 'true'; if (!deepseekBackgroundSearchHandler) { deepseekBackgroundSearchHandler = function () { const text = getInputElementValue() || ''; deepseekSearch.setText(text); }; } inputEl.addEventListener('input', deepseekBackgroundSearchHandler); inputEl.addEventListener('keyup', deepseekBackgroundSearchHandler); } // async function searchMemories(query: string): Promise { // try { // const items = await chrome.storage.sync.get([ // StorageKey.API_KEY, // StorageKey.USER_ID_CAMEL, // StorageKey.ACCESS_TOKEN, // StorageKey.SELECTED_ORG, // StorageKey.SELECTED_PROJECT, // StorageKey.USER_ID, // StorageKey.SIMILARITY_THRESHOLD, // StorageKey.TOP_K, // ]); // const userId = items.userId || items.user_id || "chrome-extension-user"; // const threshold = items.similarity_threshold !== undefined ? items.similarity_threshold : 0.1; // const topK = items.top_k !== undefined ? items.top_k : 10; // if (!items.access_token && !items.apiKey) { // throw new Error("Authentication details missing"); // } // const optionalParams: OptionalApiParams = {}; // if (items.selected_org) { // optionalParams["org_id"] = items.selected_org; // } // if (items.selected_project) { // optionalParams["project_id"] = items.selected_project; // } // const headers: Record = { // "Content-Type": "application/json", // }; // if (items.access_token) { // headers["Authorization"] = `Bearer ${items.access_token}`; // } else { // headers["Authorization"] = `Api-Key ${items.apiKey}`; // } // const url = `${MEM0_API_BASE_URL}/v2/memories/search/`; // const body = JSON.stringify({ // query: query, // filters: { // user_id: userId, // }, // rerank: true, // threshold: threshold, // top_k: topK, // filter_memories: false, // // llm_rerank: true, // source: "OPENMEMORY_CHROME_EXTENSION", // ...optionalParams, // }); // const response = await fetch(url, { // method: "POST", // headers: headers, // body: body, // }); // if (!response.ok) { // throw new Error(`HTTP error! status: ${response.status}`); // } // const data = await response.json(); // const memoryItems: MemoryItem[] = (data as MemorySearchResponse).map(item => ({ // id: item.id, // text: item.text || item.memory, // created_at: item.created_at, // user_id: item.user_id, // memory: item.memory, // })); // return memoryItems; // } catch { // // Error preparing search request // return []; // } // } function addMemory(memoryText: string) { return new Promise((resolve, reject) => { (async () => { try { const items = await chrome.storage.sync.get([ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, ]); const userId = items.userId || items.user_id || 'chrome-extension-user'; if (!items.access_token && !items.apiKey) { // No API Key or Access Token found for adding memory return reject(new Error('Authentication details missing')); } const optionalParams: OptionalApiParams = {}; if (items.selected_org) { optionalParams['org_id'] = items.selected_org; } if (items.selected_project) { optionalParams['project_id'] = items.selected_project; } const headers: Record = { 'Content-Type': 'application/json', }; if (items.access_token) { headers['Authorization'] = `Bearer ${items.access_token}`; } else { headers['Authorization'] = `Api-Key ${items.apiKey}`; } const url = `${MEM0_API_BASE_URL}/v1/memories/`; const body = JSON.stringify({ messages: [ { role: MessageRole.User, content: memoryText, }, ], user_id: userId, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }); fetch(url, { method: 'POST', headers: headers, body: body, }) .then(response => { if (!response.ok) { return response .json() .then(errorData => { // Mem0 API Add Memory Error Response Body throw new Error(errorData.detail || `HTTP error! status: ${response.status}`); }) .catch(() => { // Failed to parse add memory error response body throw new Error(`HTTP error! status: ${response.status}`); }); } if (response.status === 204) { return null; } return response.json(); }) .then(data => { resolve(data); }) .catch(error => { // Error adding memory directly reject(error); }); } catch (error) { // Error preparing add memory request reject(error); } })(); }); } async function triggerSendAction(): Promise { try { // Get send button with multiple attempts if needed let sendButton = getSendButtonElement(); let attempts = 0; // If button not found, try again a few times with increasing delays while (!sendButton && attempts < 3) { attempts++; await new Promise(resolve => setTimeout(resolve, attempts * 300)); sendButton = getSendButtonElement(); } if (sendButton) { // Check if button is disabled const isDisabled = sendButton.getAttribute('aria-disabled') === 'true' || sendButton.classList.contains('disabled') || sendButton.classList.contains('ds-button--disabled') || sendButton.hasAttribute('disabled') || (sendButton as ExtendedHTMLElement).disabled; if (!isDisabled) { // Try multiple click strategies try { // Strategy 1: Native click() method sendButton.click(); // Strategy 2: After a short delay, try a MouseEvent if the first click didn't work setTimeout(() => { try { // Check if the input field is now empty (indicating message was sent) const inputElement = getInputElement() as HTMLTextAreaElement | HTMLDivElement; const inputValue = inputElement ? ( (inputElement as HTMLDivElement).textContent || (inputElement as HTMLTextAreaElement).value || '' ).trim() : null; // If input is still not empty, try alternative click method if (inputValue && inputValue.length > 0) { const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window, }); sendButton.dispatchEvent(clickEvent); } } catch { // Ignore errors } }, 200); // Strategy 3: As a last resort, try to focus and press Enter setTimeout(() => { try { const inputElement = getInputElement() as HTMLTextAreaElement | HTMLDivElement; const inputValue = inputElement ? ( (inputElement as HTMLDivElement).textContent || (inputElement as HTMLTextAreaElement).value || '' ).trim() : null; // If input is still not empty, try pressing Enter if (inputValue && inputValue.length > 0) { (inputElement as HTMLElement).focus(); const enterEvent = new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true, }); if (inputElement) { (inputElement as HTMLElement).dispatchEvent(enterEvent); } } } catch { // Ignore errors } }, 500); } catch { // Ignore errors } } else { // Button is disabled } } else { // No send button found } } catch { // Ignore errors } } async function handleMem0Processing(): Promise { try { // Check if we're already processing (prevent double processing) if (isProcessingMem0) { return; } isProcessingMem0 = true; // Get the current input value const originalPrompt = getInputElementValue(); if (!originalPrompt || originalPrompt.trim() === '') { isProcessingMem0 = false; triggerSendAction(); return; } // Trigger the send action await triggerSendAction(); // Add the user's input as a new memory try { if (originalPrompt.trim().length > 5) { // Only add non-trivial prompts await addMemory(originalPrompt); } } catch { // Continue regardless of error adding memory } // Reset state after a short delay setTimeout(() => { isProcessingMem0 = false; allMemories = []; // Clear loaded memories allMemoriesById = new Set(); }, 1000); } catch { // Reset processing state and trigger send as fallback isProcessingMem0 = false; triggerSendAction(); } } // Function to create a memory modal function createMemoryModal( memoryItems: MemoryItem[], isLoading: boolean = false, sourceButtonId: string | null = null ): void { // Close existing modal if it exists (but preserve drag position for updates) if (memoryModalShown && currentModalOverlay) { document.body.removeChild(currentModalOverlay); } memoryModalShown = true; let currentMemoryIndex = 0; // Calculate modal dimensions const modalWidth = 447; let modalHeight = 400; // Default height let memoriesPerPage = 3; // Default number of memories per page let topPosition: number | undefined; let leftPosition: number | undefined; // Check if we have a stored drag position and use it if (modalDragPosition) { topPosition = modalDragPosition.top; leftPosition = modalDragPosition.left; } else { // Different positioning based on which button triggered the modal if (sourceButtonId === 'mem0-icon-button') { // Position relative to the mem0-icon-button const iconButton = document.querySelector('#mem0-icon-button'); if (iconButton) { const buttonRect = iconButton.getBoundingClientRect(); // Determine if there's enough space above the button const spaceAbove = buttonRect.top; const viewportHeight = window.innerHeight; leftPosition = buttonRect.left - modalWidth + buttonRect.width; leftPosition = Math.max(leftPosition, 10); if (spaceAbove >= modalHeight + 10) { // Place above topPosition = buttonRect.top - modalHeight - 10; } else { // Not enough space above, place below topPosition = buttonRect.bottom + 10; if (buttonRect.bottom > viewportHeight / 2) { modalHeight = 300; // Reduced height memoriesPerPage = 2; // Show only 2 memories } } } else { // Fallback to input-based positioning positionRelativeToInput(); } } else { // Default positioning relative to the input field positionRelativeToInput(); } } // Helper function to position modal relative to input field function positionRelativeToInput() { const inputElement = getInputElement(); if (!inputElement) { return; } // Get the position and dimensions of the input field const inputRect = inputElement.getBoundingClientRect(); // Determine if there's enough space below the input field const viewportHeight = window.innerHeight; const spaceBelow = viewportHeight - inputRect.bottom; // Position the modal aligned to the right of the input leftPosition = Math.max(inputRect.right - 20 - modalWidth, 10); // 20px offset from right edge // Decide whether to place modal above or below based on available space if (spaceBelow >= modalHeight) { // Place below the input topPosition = inputRect.bottom + 10; // Check if it's in the lower half of the screen if (inputRect.bottom > viewportHeight / 2) { modalHeight = 300; // Reduced height memoriesPerPage = 2; // Show only 2 memories } } else { // Place above the input if not enough space below topPosition = inputRect.top - modalHeight - 10; } } // Create modal overlay const modalOverlay = document.createElement('div'); modalOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: transparent; display: flex; z-index: 10000; pointer-events: auto; `; // Save reference to current modal overlay currentModalOverlay = modalOverlay; // Add event listener to close modal when clicking outside modalOverlay.addEventListener('click', (event: MouseEvent) => { // Only close if clicking directly on the overlay, not its children if (event.target === modalOverlay) { closeModal(); } }); // Create modal container with positioning const modalContainer = document.createElement('div'); modalContainer.style.cssText = ` background-color: #1C1C1E; border-radius: 12px; width: ${modalWidth}px; height: ${modalHeight}px; display: flex; flex-direction: column; color: white; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); position: absolute; top: ${topPosition}px; left: ${leftPosition}px; pointer-events: auto; border: 1px solid #27272A; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; overflow: hidden; `; // Create modal header const modalHeader = document.createElement('div'); modalHeader.style.cssText = ` display: flex; align-items: center; padding: 10px 16px; justify-content: space-between; background-color: #232325; flex-shrink: 0; cursor: move; user-select: none; `; // Create header left section with logo const headerLeft = document.createElement('div'); headerLeft.style.cssText = ` display: flex; flex-direction: row; align-items: center; pointer-events: none; `; // Add Mem0 logo const logoImg = document.createElement('img'); logoImg.src = chrome.runtime.getURL('icons/mem0-claude-icon.png'); logoImg.style.cssText = ` width: 26px; height: 26px; border-radius: 50%; margin-right: 10px; `; // OpenMemory titel const title = document.createElement('div'); title.textContent = 'OpenMemory'; title.style.cssText = ` font-size: 16px; font-weight: 600; color: white; `; // Create header right section const headerRight = document.createElement('div'); headerRight.style.cssText = ` display: flex; flex-direction: row; align-items: center; gap: 8px; pointer-events: auto; `; // Create Add to Prompt button with arrow const addToPromptBtn = document.createElement('button'); addToPromptBtn.style.cssText = ` display: flex; flex-direction: row; align-items: center; padding: 5px 16px; gap: 8px; background-color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 12px; font-weight: 600; color: black; `; addToPromptBtn.textContent = 'Add to Prompt'; // Add arrow icon to button const arrowIcon = document.createElement('span'); arrowIcon.innerHTML = ` `; addToPromptBtn.appendChild(arrowIcon); // Create settings button const settingsBtn = document.createElement('button'); settingsBtn.style.cssText = ` background: none; border: none; cursor: pointer; padding: 8px; opacity: 0.6; transition: opacity 0.2s; `; settingsBtn.innerHTML = ` `; // Add click event to open app.mem0.ai in a new tab settingsBtn.addEventListener('click', () => { if (currentModalOverlay && document.body.contains(currentModalOverlay)) { document.body.removeChild(currentModalOverlay); memoryModalShown = false; currentModalOverlay = null; } chrome.runtime.sendMessage({ action: SidebarAction.SIDEBAR_SETTINGS }); }); // Add hover effect for the settings button settingsBtn.addEventListener('mouseenter', () => { settingsBtn.style.opacity = '1'; }); settingsBtn.addEventListener('mouseleave', () => { settingsBtn.style.opacity = '0.6'; }); // Content section const contentSection = document.createElement('div'); const contentSectionHeight = modalHeight - 130; // Account for header and navigation contentSection.style.cssText = ` display: flex; flex-direction: column; padding: 0 16px; gap: 12px; overflow: hidden; flex: 1; height: ${contentSectionHeight}px; `; // Create memories counter const memoriesCounter = document.createElement('div'); memoriesCounter.style.cssText = ` font-size: 16px; font-weight: 600; color: #FFFFFF; margin-top: 16px; flex-shrink: 0; `; // Update counter text based on loading state and number of memories if (isLoading) { memoriesCounter.textContent = `Loading Relevant Memories...`; } else { memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`; } // Calculate max height for memories content based on modal height const memoriesContentMaxHeight = contentSectionHeight - 40; // Account for memories counter // Create memories content container with adjusted height const memoriesContent = document.createElement('div'); memoriesContent.style.cssText = ` display: flex; flex-direction: column; gap: 8px; overflow-y: auto; flex: 1; max-height: ${memoriesContentMaxHeight}px; padding-right: 8px; margin-right: -8px; scrollbar-width: none; -ms-overflow-style: none; `; memoriesContent.style.cssText += '::-webkit-scrollbar { display: none; }'; // Track currently expanded memory let currentlyExpandedMemory: HTMLElement | null = null; // Function to create skeleton loading items function createSkeletonItems() { memoriesContent.innerHTML = ''; for (let i = 0; i < memoriesPerPage; i++) { const skeletonItem = document.createElement('div'); skeletonItem.style.cssText = ` display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; padding: 12px; background-color: #27272A; border-radius: 8px; height: 52px; flex-shrink: 0; animation: pulse 1.5s infinite ease-in-out; `; const skeletonText = document.createElement('div'); skeletonText.style.cssText = ` background-color: #383838; border-radius: 4px; height: 14px; width: 85%; margin-bottom: 8px; `; const skeletonText2 = document.createElement('div'); skeletonText2.style.cssText = ` background-color: #383838; border-radius: 4px; height: 14px; width: 65%; `; const skeletonActions = document.createElement('div'); skeletonActions.style.cssText = ` display: flex; gap: 4px; margin-left: 10px; `; const skeletonButton1 = document.createElement('div'); skeletonButton1.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; background-color: #383838; `; const skeletonButton2 = document.createElement('div'); skeletonButton2.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; background-color: #383838; `; skeletonActions.appendChild(skeletonButton1); skeletonActions.appendChild(skeletonButton2); const textContainer = document.createElement('div'); textContainer.style.cssText = ` display: flex; flex-direction: column; flex-grow: 1; `; textContainer.appendChild(skeletonText); textContainer.appendChild(skeletonText2); skeletonItem.appendChild(textContainer); skeletonItem.appendChild(skeletonActions); memoriesContent.appendChild(skeletonItem); } // Add keyframe animation to document if not exists if (!document.getElementById('skeleton-animation')) { const style = document.createElement('style'); style.id = 'skeleton-animation'; style.innerHTML = ` @keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 0.8; } 100% { opacity: 0.6; } } `; document.head.appendChild(style); } } // Navigation section at bottom const navigationSection = document.createElement('div'); navigationSection.style.cssText = ` display: flex; justify-content: center; gap: 12px; padding: 10px; border-top: none; flex-shrink: 0; `; // Navigation buttons const prevButton = document.createElement('button'); prevButton.innerHTML = ` `; prevButton.style.cssText = ` background: #27272A; border: none; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.2s; `; const nextButton = document.createElement('button'); nextButton.innerHTML = ` `; nextButton.style.cssText = prevButton.style.cssText; // Add click handlers for navigation buttons prevButton.addEventListener('click', () => { // Calculate current page information const memoriesToShow = Math.min(memoriesPerPage, memoryItems.length); // const totalPages = Math.ceil(memoryItems.length / memoriesToShow); const currentPage = Math.floor(currentMemoryIndex / memoriesToShow) + 1; if (currentPage > 1) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); showMemories(); } }); nextButton.addEventListener('click', () => { // Calculate current page information const memoriesToShow = Math.min(memoriesPerPage, memoryItems.length); const totalPages = Math.ceil(memoryItems.length / memoriesToShow); const currentPage = Math.floor(currentMemoryIndex / memoriesToShow) + 1; if (currentPage < totalPages) { currentMemoryIndex = currentMemoryIndex + memoriesPerPage; showMemories(); } }); // Assemble modal headerLeft.appendChild(logoImg); headerLeft.appendChild(title); headerRight.appendChild(addToPromptBtn); headerRight.appendChild(settingsBtn); modalHeader.appendChild(headerLeft); modalHeader.appendChild(headerRight); contentSection.appendChild(memoriesCounter); contentSection.appendChild(memoriesContent); navigationSection.appendChild(prevButton); navigationSection.appendChild(nextButton); modalContainer.appendChild(modalHeader); modalContainer.appendChild(contentSection); modalContainer.appendChild(navigationSection); modalOverlay.appendChild(modalContainer); // Add drag functionality let isDragging = false; const dragOffset = { x: 0, y: 0 }; modalHeader.addEventListener('mousedown', (e: MouseEvent) => { isDragging = true; const containerRect = modalContainer.getBoundingClientRect(); dragOffset.x = e.clientX - containerRect.left; dragOffset.y = e.clientY - containerRect.top; modalHeader.style.cursor = 'grabbing'; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); e.preventDefault(); }); function handleMouseMove(e: MouseEvent) { if (!isDragging) { return; } const newLeft = e.clientX - dragOffset.x; const newTop = e.clientY - dragOffset.y; // Keep modal within viewport bounds const maxLeft = window.innerWidth - modalWidth; const maxTop = window.innerHeight - modalHeight; const constrainedLeft = Math.max(0, Math.min(newLeft, maxLeft)); const constrainedTop = Math.max(0, Math.min(newTop, maxTop)); modalContainer.style.left = constrainedLeft + 'px'; modalContainer.style.top = constrainedTop + 'px'; // Store the position for future modal recreations modalDragPosition = { left: constrainedLeft, top: constrainedTop, }; } function handleMouseUp() { isDragging = false; modalHeader.style.cursor = 'move'; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); } // Append to body document.body.appendChild(modalOverlay); // Show initial memories or loading state if (isLoading) { createSkeletonItems(); } else { showMemories(); } // Function to close the modal function closeModal() { if (currentModalOverlay && document.body.contains(currentModalOverlay)) { document.body.removeChild(currentModalOverlay); } currentModalOverlay = null; memoryModalShown = false; // Reset drag position when modal is truly closed by user action modalDragPosition = null; } // Function to show memories function showMemories() { memoriesContent.innerHTML = ''; if (isLoading) { createSkeletonItems(); return; } if (memoryItems.length === 0) { showEmptyState(memoriesContent); updateNavigationState(prevButton, nextButton, 0, 0); return; } // Use the dynamically set memoriesPerPage value const memoriesToShow = Math.min(memoriesPerPage, memoryItems.length); // Calculate total pages and current page const totalPages = Math.ceil(memoryItems.length / memoriesToShow); const currentPage = Math.floor(currentMemoryIndex / memoriesToShow) + 1; // Update navigation buttons state updateNavigationState(prevButton, nextButton, currentPage, totalPages); for (let i = 0; i < memoriesToShow; i++) { const memoryIndex = currentMemoryIndex + i; if (memoryIndex >= memoryItems.length) { break; } const memory = memoryItems[memoryIndex]; if (!memory) { continue; } // Skip memories that have been added already if (allMemoriesById.has(String(memory.id))) { continue; } // Ensure memory has an ID if (!memory.id) { memory.id = `memory-${Date.now()}-${memoryIndex}`; } const memoryContainer = document.createElement('div'); memoryContainer.style.cssText = ` display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; padding: 12px; background-color: #27272A; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; min-height: 52px; max-height: 52px; overflow: hidden; flex-shrink: 0; `; const memoryText = document.createElement('div'); memoryText.style.cssText = ` font-size: 14px; line-height: 1.5; color: #D4D4D8; flex-grow: 1; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; transition: all 0.2s ease; height: 42px; `; memoryText.textContent = memory.memory || memory.text || ''; // Create remove button (hidden by default) const removeButton = document.createElement('button'); removeButton.style.cssText = ` display: none; align-items: center; gap: 6px; background:rgba(54, 54, 54, 0.71); color:rgb(199, 199, 201); border-radius: 8px; padding: 2px 4px; border: none; cursor: pointer; font-size: 13px; margin-top: 12px; width: fit-content; `; removeButton.innerHTML = ` Remove `; // Create content wrapper for text and remove button const contentWrapper = document.createElement('div'); contentWrapper.style.cssText = ` display: flex; flex-direction: column; flex-grow: 1; `; contentWrapper.appendChild(memoryText); contentWrapper.appendChild(removeButton); const actionsContainer = document.createElement('div'); actionsContainer.style.cssText = ` display: flex; gap: 4px; margin-left: 10px; flex-shrink: 0; `; // Add button const addButton = document.createElement('button'); addButton.style.cssText = ` border: none; cursor: pointer; padding: 4px; height: 28px; background:rgb(66, 66, 69); color:rgb(199, 199, 201); border-radius: 100%; transition: all 0.2s ease; `; addButton.innerHTML = ` `; // Add click handler for add button addButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); sendExtensionEvent('memory_injection', { provider: 'deepseek', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), injected_all: false, memory_id: memory.id, }); // Add this memory allMemoriesById.add(String(memory.id)); allMemories.push(String(memory.memory || memory.text || '')); updateInputWithMemories(); // Remove this memory from the list const index = memoryItems.findIndex((m: MemoryItem) => m.id === memory.id); if (index !== -1) { memoryItems.splice(index, 1); // Recalculate pagination after removing an item if (currentMemoryIndex > 0 && currentMemoryIndex >= memoryItems.length) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); } memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`; showMemories(); } }); // Menu button (more options) const menuButton = document.createElement('button'); menuButton.style.cssText = ` background: none; border: none; cursor: pointer; padding: 4px; color: #A1A1AA; `; menuButton.innerHTML = ` `; // Track expanded state let isExpanded = false; // Function to expand memory const expandMemoryHandler = () => { expandMemory( memoryContainer, memoryText, contentWrapper, removeButton, currentlyExpandedMemory, memoriesContent ); }; // Function to collapse memory const collapseMemoryHandler = () => { collapseMemory(memoryContainer, memoryText, contentWrapper, removeButton); }; // Add collapse event listener memoryContainer.addEventListener('collapse', collapseMemoryHandler); // Add click handler for the menu button menuButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); if (isExpanded) { collapseMemoryHandler(); } else { expandMemoryHandler(); } }); // Add click handler for remove button removeButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); // Remove from memoryItems const index = memoryItems.findIndex(m => m.id === memory.id); if (index !== -1) { memoryItems.splice(index, 1); // Recalculate pagination after removing an item // const newTotalPages = Math.ceil(memoryItems.length / memoriesPerPage); // If we're on the last page and it's now empty, go to previous page if (currentMemoryIndex > 0 && currentMemoryIndex >= memoryItems.length) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); } memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`; showMemories(); } }); actionsContainer.appendChild(addButton); actionsContainer.appendChild(menuButton); memoryContainer.appendChild(contentWrapper); memoryContainer.appendChild(actionsContainer); memoriesContent.appendChild(memoryContainer); // Add hover effect memoryContainer.addEventListener('mouseenter', () => { memoryContainer.style.backgroundColor = isExpanded ? '#1C1C1E' : '#323232'; }); memoryContainer.addEventListener('mouseleave', () => { memoryContainer.style.backgroundColor = isExpanded ? '#1C1C1E' : '#27272A'; }); // Add click handler to expand/collapse when clicking on memory memoryContainer.addEventListener('click', () => { if (isExpanded) { collapseMemoryHandler(); } else { expandMemoryHandler(); } }); } } // Update Add to Prompt button click handler addToPromptBtn.addEventListener('click', () => { // Only add memories that are not already added const newMemories = memoryItems .filter(memory => !allMemoriesById.has(String(memory.id))) .map(memory => { allMemoriesById.add(String(memory.id)); return String(memory.memory || memory.text || ''); }); sendExtensionEvent('memory_injection', { provider: 'deepseek', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), injected_all: true, memory_count: newMemories.length, }); // Add all new memories to allMemories allMemories.push(...newMemories); // Update the input with all memories if (allMemories.length > 0) { updateInputWithMemories(); closeModal(); } else { // If no new memories were added but we have existing ones, just close if (allMemoriesById.size > 0) { closeModal(); } } }); } // Function to show empty state with specific container function showEmptyState(container: HTMLElement) { if (!container) { return; } container.innerHTML = ''; const emptyContainer = document.createElement('div'); emptyContainer.style.cssText = ` display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 32px 16px; text-align: center; flex: 1; min-height: 200px; `; const emptyIcon = document.createElement('div'); emptyIcon.innerHTML = ` `; emptyIcon.style.marginBottom = '16px'; const emptyText = document.createElement('div'); emptyText.textContent = 'No relevant memories found'; emptyText.style.cssText = ` color: #71717A; font-size: 14px; font-weight: 500; `; emptyContainer.appendChild(emptyIcon); emptyContainer.appendChild(emptyText); container.appendChild(emptyContainer); } // Update navigation button states with specific buttons function updateNavigationState( prevButton: HTMLButtonElement, nextButton: HTMLButtonElement, currentPage: number, totalPages: number ) { if (!prevButton || !nextButton) { return; } if (totalPages === 0) { prevButton.disabled = true; prevButton.style.opacity = '0.5'; prevButton.style.cursor = 'not-allowed'; nextButton.disabled = true; nextButton.style.opacity = '0.5'; nextButton.style.cursor = 'not-allowed'; return; } if (currentPage <= 1) { prevButton.disabled = true; prevButton.style.opacity = '0.5'; prevButton.style.cursor = 'not-allowed'; } else { prevButton.disabled = false; prevButton.style.opacity = '1'; prevButton.style.cursor = 'pointer'; } if (currentPage >= totalPages) { nextButton.disabled = true; nextButton.style.opacity = '0.5'; nextButton.style.cursor = 'not-allowed'; } else { nextButton.disabled = false; nextButton.style.opacity = '1'; nextButton.style.cursor = 'pointer'; } } // Function to apply memories to the input field function updateInputWithMemories(): void { const inputElement = getInputElement(); if (inputElement && allMemories.length > 0) { // Get the content without any existing memory wrappers const baseContent = getContentWithoutMemories(); // Create the memory wrapper with all collected memories let memoriesContent = '\n\n' + OPENMEMORY_PROMPTS.memory_header_text + '\n'; // Add all memories to the content allMemories.forEach(mem => { memoriesContent += `- ${mem}\n`; }); // Add the final content to the input (inputElement as HTMLTextAreaElement).value = `${baseContent}${memoriesContent}`; (inputElement as HTMLElement).dispatchEvent(new Event('input', { bubbles: true })); (inputElement as HTMLElement).focus(); } } // Function to get the content without any memory wrappers function getContentWithoutMemories(): string { const inputElement = getInputElement(); if (!inputElement) { return ''; } let content = (inputElement as HTMLDivElement).textContent || (inputElement as HTMLTextAreaElement).value || ''; // Remove memories section content = content.replace(/\n\nHere is some of my memories[\s\S]*$/, ''); return content.trim(); } // Function to handle the Mem0 modal async function handleMem0Modal(sourceButtonId: string | null = null): Promise { try { // First check if memory is enabled (user is logged in) const memoryEnabled = await getMemoryEnabledState(); if (!memoryEnabled) { // User is not logged in, show login modal showLoginModal(); return; } // Get current input text const message = getInputElementValue(); // If no message, show a guidance popover and return if (!message || message.trim() === '') { showGuidancePopover(); return; } if (isProcessingMem0) { return; } isProcessingMem0 = true; // Show the loading modal immediately createMemoryModal([], true, sourceButtonId); try { const auth = await getAuthDetails(); if (!auth.apiKey && !auth.accessToken) { isProcessingMem0 = false; showLoginModal(); return; } sendExtensionEvent('modal_clicked', { provider: 'deepseek', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), }); currentModalSourceButtonId = sourceButtonId; deepseekSearch.runImmediate(message); addMemory(message).catch(() => { // Ignore errors }); } catch { // Error in handleMem0Modal createMemoryModal([], false, sourceButtonId); } finally { isProcessingMem0 = false; } } catch { isProcessingMem0 = false; } } // Function to show a guidance popover when input is empty function showGuidancePopover(): void { // First remove any existing popovers const existingPopover = document.getElementById('mem0-guidance-popover'); if (existingPopover) { document.body.removeChild(existingPopover); } // Get the Mem0 button to position relative to it const mem0Button = document.getElementById('mem0-icon-button'); if (!mem0Button) { return; } const buttonRect = mem0Button.getBoundingClientRect(); // Create the popover const popover = document.createElement('div'); popover.id = 'mem0-guidance-popover'; popover.style.cssText = ` position: fixed; background-color: #1C1C1E; color: white; padding: 12px 16px; border-radius: 8px; font-size: 14px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); z-index: 10002; max-width: 250px; border: 1px solid #383838; top: ${buttonRect.bottom + 10}px; left: ${buttonRect.left - 110}px; `; // Add content to the popover popover.innerHTML = `

No Input Detected
Please type your message in the input field first to add or search memories.
`; // Add close button const closeButton = document.createElement('button'); closeButton.style.cssText = ` position: absolute; top: 8px; right: 8px; background: none; border: none; color: #A1A1AA; cursor: pointer; padding: 4px; line-height: 1; `; closeButton.innerHTML = ` `; closeButton.addEventListener('click', () => { if (document.body.contains(popover)) { document.body.removeChild(popover); } }); // Add arrow const arrow = document.createElement('div'); arrow.style.cssText = ` position: absolute; top: -6px; left: 120px; width: 12px; height: 12px; background: #1C1C1E; transform: rotate(45deg); border-left: 1px solid #383838; border-top: 1px solid #383838; `; popover.appendChild(closeButton); popover.appendChild(arrow); document.body.appendChild(popover); // Auto-close after 5 seconds setTimeout(() => { if (document.body.contains(popover)) { document.body.removeChild(popover); } }, 5000); } // Function to show login modal function showLoginModal(): void { // First check if modal already exists if (document.getElementById('mem0-login-popup')) { return; } // Create popup overlay const popupOverlay = document.createElement('div'); popupOverlay.id = 'mem0-login-popup'; popupOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 100000; `; // Create popup container const popupContainer = document.createElement('div'); popupContainer.style.cssText = ` background-color: #1C1C1E; border-radius: 12px; width: 320px; padding: 24px; color: white; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; // Close button const closeButton = document.createElement('button'); closeButton.style.cssText = ` position: absolute; top: 16px; right: 16px; background: none; border: none; color: #A1A1AA; font-size: 16px; cursor: pointer; `; closeButton.innerHTML = '×'; closeButton.addEventListener('click', () => { document.body.removeChild(popupOverlay); }); // Logo and heading const logoContainer = document.createElement('div'); logoContainer.style.cssText = ` display: flex; align-items: center; justify-content: center; margin-bottom: 16px; `; const logo = document.createElement('img'); logo.src = chrome.runtime.getURL('icons/mem0-claude-icon.png'); logo.style.cssText = ` width: 24px; height: 24px; border-radius: 50%; margin-right: 12px; `; const logoDark = document.createElement('img'); logoDark.src = chrome.runtime.getURL('icons/mem0-icon-black.png'); logoDark.style.cssText = ` width: 24px; height: 24px; border-radius: 50%; margin-right: 12px; `; const heading = document.createElement('h2'); heading.textContent = 'Sign in to OpenMemory'; heading.style.cssText = ` margin: 0; font-size: 18px; font-weight: 500; `; logoContainer.appendChild(heading); // Message const message = document.createElement('p'); message.textContent = 'Please sign in to access your memories and personalize your conversations!'; message.style.cssText = ` margin-bottom: 24px; color: #D4D4D8; font-size: 14px; line-height: 1.5; text-align: center; `; // Sign in button const signInButton = document.createElement('button'); signInButton.style.cssText = ` display: flex; align-items: center; justify-content: center; width: 100%; padding: 10px; background-color: white; color: black; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: background-color 0.2s; `; // Add text in span for better centering const signInText = document.createElement('span'); signInText.textContent = 'Sign in with Mem0'; signInButton.appendChild(logoDark); signInButton.appendChild(signInText); signInButton.addEventListener('mouseenter', () => { signInButton.style.backgroundColor = '#f5f5f5'; }); signInButton.addEventListener('mouseleave', () => { signInButton.style.backgroundColor = 'white'; }); // Open sign-in page when clicked signInButton.addEventListener('click', () => { // Send message to background script to handle authentication try { chrome.runtime.sendMessage({ action: SidebarAction.SHOW_LOGIN_POPUP }); } catch { // Ignore errors } // Fallback: open the login page directly window.open('https://app.mem0.ai/login', '_blank'); // Close the modal document.body.removeChild(popupOverlay); }); // Assemble popup popupContainer.appendChild(logoContainer); popupContainer.appendChild(message); popupContainer.appendChild(signInButton); popupOverlay.appendChild(popupContainer); popupOverlay.appendChild(closeButton); // Add click event to close when clicking outside popupOverlay.addEventListener('click', (e: MouseEvent) => { if (e.target === popupOverlay) { document.body.removeChild(popupOverlay); } }); // Add to body document.body.appendChild(popupOverlay); } // Function to add the Mem0 icon button - enhanced with error handling and return status function addMem0IconButton() { try { // Prefer OPENMEMORY_UI mounts; fall back to legacy injection only if unavailable if (OPENMEMORY_UI && OPENMEMORY_UI.mountOnEditorFocus) { try { if (!document.getElementById('mem0-icon-button')) { OPENMEMORY_UI.resolveCachedAnchor( { learnKey: location.host + ':' + location.pathname }, null, 24 * 60 * 60 * 1000 ).then(function (hit: { el: Element; placement: Placement | null } | null) { if (!hit || !hit.el) { return; } let hs = OPENMEMORY_UI.createShadowRootHost('mem0-root'); let host = hs.host, shadow = hs.shadow; host.id = 'mem0-icon-button'; let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.deepseek ? SITE_CONFIG.deepseek : null; let placement = hit.placement || (cfg && cfg.placement) || { strategy: 'float', placement: 'right-center', gap: 12, }; OPENMEMORY_UI.applyPlacement({ container: host, anchor: hit.el, placement: placement }); let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal('mem0-icon-button'); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } }); } } catch { // Ignore errors during re-initialization } OPENMEMORY_UI.mountOnEditorFocus({ existingHostSelector: '#mem0-icon-button', editorSelector: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.deepseek && SITE_CONFIG.deepseek.editorSelector ? SITE_CONFIG.deepseek.editorSelector : 'textarea, [contenteditable="true"], input[type="text"]', deriveAnchor: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.deepseek && typeof SITE_CONFIG.deepseek.deriveAnchor === 'function' ? SITE_CONFIG.deepseek.deriveAnchor : function (editor: Element) { return editor.closest('form') || editor.parentElement; }, placement: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.deepseek && SITE_CONFIG.deepseek.placement ? SITE_CONFIG.deepseek.placement : { strategy: 'float', placement: 'right-center', gap: 12 }, render: function (shadow: ShadowRoot, host: HTMLElement) { host.id = 'mem0-icon-button'; let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal('mem0-icon-button'); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } }, fallback: function () { let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.deepseek ? SITE_CONFIG.deepseek : null; return OPENMEMORY_UI.mountResilient({ anchors: [ { find: function () { let sel = (cfg && cfg.editorSelector) || 'textarea, [contenteditable="true"], input[type="text"]'; let ed = document.querySelector(sel); if (!ed) { return null; } try { return cfg && typeof cfg.deriveAnchor === 'function' ? cfg.deriveAnchor(ed) : ed.closest('form') || ed.parentElement; } catch (_) { return ed.closest('form') || ed.parentElement; } }, }, ], placement: (cfg && cfg.placement) || { strategy: 'float', placement: 'right-center', gap: 12, }, enableFloatingFallback: true, render: function (shadow: ShadowRoot, host: HTMLElement) { host.id = 'mem0-icon-button'; let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal('mem0-icon-button'); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } }, }); }, }); return { success: true, status: 'openmemory_ui_mount' }; } // Check if memory is enabled before adding the button getMemoryEnabledState() .then(memoryEnabled => { if (!memoryEnabled) { removeMem0IconButton(); return; } // Continue with button creation if memory is enabled createAndAddButton(); }) .catch(() => { // If we can't check memory state, don't add the button }); return { success: true, status: 'checking_memory_state' }; } catch { return { success: false, status: 'unexpected_error', error: 'Unknown error' }; } // Helper function to create and add the button function createAndAddButton() { // Check if the button already exists if (document.querySelector('#mem0-icon-button')) { return { success: true, status: 'already_exists' }; } // Wait for input element to be available before trying to add the button const inputElement = getInputElement(); if (!inputElement) { // Retry in 1 second setTimeout(addMem0IconButton, 1000); return { success: false, status: 'no_input_element' }; } // Try multiple approaches to find placement locations let searchButton = null; let buttonContainer = null; let status = 'searching'; // Approach 1: Look for the search button by class and specific selectors searchButton = document.querySelector('div[role="button"] .ds-button__icon + span'); if (searchButton && (searchButton.textContent || '').trim().toLowerCase() === 'search') { const parentBtn = searchButton.closest('div[role="button"]'); buttonContainer = parentBtn ? (parentBtn.parentElement as HTMLElement) : null; if (buttonContainer) { status = 'found_search_button'; } } else { // Try alternative selector const allButtons = document.querySelectorAll('div[role="button"]'); Array.from(allButtons).some(btn => { if ((btn.textContent || '').trim().toLowerCase() === 'search') { searchButton = btn.querySelector('span'); buttonContainer = (btn as HTMLElement).parentElement as HTMLElement; status = 'found_search_button_alt'; return true; } return false; }); } // Approach 2: Look for any toolbar or button container if (!buttonContainer) { const toolbars = document.querySelectorAll('.toolbar, .button-container, .controls'); if (toolbars.length > 0) { buttonContainer = toolbars[0]; status = 'found_toolbar'; } } // Approach 3: Try to find the input field and place it near there if (!buttonContainer) { if (inputElement && inputElement.parentElement) { // Try going up a few levels to find a good container let parent: HTMLElement = inputElement.parentElement as HTMLElement; let level = 0; while (parent && level < 3) { const buttons = parent.querySelectorAll('div[role="button"]'); if (buttons.length > 0) { buttonContainer = parent; status = 'found_input_parent_with_buttons'; break; } parent = parent.parentElement as HTMLElement; level++; } // If still not found, use direct parent if (!buttonContainer) { buttonContainer = inputElement.parentElement; status = 'found_input_parent'; } } } // Approach 4: Look for a div with role="toolbar" if (!buttonContainer) { const toolbars = document.querySelectorAll('div[role="toolbar"]'); if (toolbars.length > 0) { buttonContainer = toolbars[0]; status = 'found_role_toolbar'; } } // If we couldn't find a suitable container, create one near the input if (!buttonContainer && inputElement) { buttonContainer = document.createElement('div'); buttonContainer.id = 'mem0-custom-container'; buttonContainer.style.cssText = ` display: flex; position: absolute; top: ${inputElement.getBoundingClientRect().top - 40}px; left: ${inputElement.getBoundingClientRect().right - 100}px; z-index: 1000; `; document.body.appendChild(buttonContainer); status = 'created_custom_container'; } // If we couldn't find a suitable container, bail out if (!buttonContainer) { return { success: false, status: 'no_container' }; } // Remove existing button if any const existingButton = document.querySelector('#mem0-icon-button'); if (existingButton) { try { if (existingButton.parentElement) { existingButton.parentElement.removeChild(existingButton); } } catch { // Ignore errors } } // Create button container const mem0ButtonContainer = document.createElement('div'); mem0ButtonContainer.style.cssText = ` display: inline-flex; position: relative; margin: 0 4px; align-items: center; `; // Create notification dot const notificationDot = document.createElement('div'); notificationDot.id = 'mem0-notification-dot'; notificationDot.style.cssText = ` position: absolute; top: -3px; right: -3px; width: 8px; height: 8px; background-color: rgb(128, 221, 162); border-radius: 50%; border: 1px solid #1C1C1E; display: none; z-index: 1001; pointer-events: none; `; // Add keyframe animation for the dot if (!document.getElementById('notification-dot-animation')) { try { const style = document.createElement('style'); style.id = 'notification-dot-animation'; style.innerHTML = ` @keyframes popIn { 0% { transform: scale(0); } 50% { transform: scale(1.2); } 100% { transform: scale(1); } } #mem0-notification-dot.active { display: block !important; animation: popIn 0.3s ease-out forwards; } `; document.head.appendChild(style); } catch { // Ignore errors } } // Create the button to match DeepSeek style const mem0Button = document.createElement('div'); mem0Button.id = 'mem0-icon-button'; mem0Button.setAttribute('role', 'button'); mem0Button.className = 'ds-button ds-button--rect ds-button--m'; mem0Button.tabIndex = 0 as number; mem0Button.style.cssText = ` cursor: pointer; height: 30px; display: inline-flex; margin-left: -2px; align-items: center; padding: 0px 6px; border: 1px solid rgb(95, 95, 95); border-radius: 16px; background-color: rgba(255, 255, 255, 0.0); transition: background-color 0.2s; `; // Rest of the existing button creation code... // Create the icon container const iconContainer = document.createElement('div'); iconContainer.className = 'ds-button__icon'; iconContainer.style.cssText = ` display: flex; align-items: center; justify-content: center; margin-right: 2px; `; // Create the icon const icon = document.createElement('img'); icon.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); icon.style.cssText = ` width: 14px; height: 14px; border-radius: 100%; `; // Create button text const buttonText = document.createElement('span'); buttonText.style.cssText = ` color: #F8FAFF; font-size: 12px; `; buttonText.textContent = 'Memory'; // Create tooltip with improved stability const tooltip = document.createElement('div'); tooltip.className = 'mem0-tooltip'; tooltip.style.cssText = ` position: absolute; bottom: 40px; left: 50%; transform: translateX(-50%); background-color: #1C1C1E; color: white; padding: 6px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; z-index: 10001; display: none; transition: opacity 0.2s; opacity: 0; pointer-events: none; `; tooltip.textContent = 'Add memories to your prompt'; // Add arrow to tooltip const arrow = document.createElement('div'); arrow.className = 'mem0-tooltip-arrow'; arrow.style.cssText = ` position: absolute; top: 100%; left: 50%; transform: translateX(-50%) rotate(45deg); width: 8px; height: 8px; background-color: #1C1C1E; pointer-events: none; `; tooltip.appendChild(arrow); // Show/hide tooltip using more stable approach with a data attribute let tooltipVisible = false; mem0Button.addEventListener('mouseenter', () => { if (tooltipVisible) { return; } tooltipVisible = true; tooltip.style.display = 'block'; requestAnimationFrame(() => { tooltip.style.opacity = '1'; mem0Button.style.backgroundColor = '#424451'; }); }); mem0Button.addEventListener('mouseleave', () => { if (!tooltipVisible) { return; } tooltipVisible = false; tooltip.style.opacity = '0'; setTimeout(() => { if (!tooltipVisible) { tooltip.style.display = 'none'; mem0Button.style.backgroundColor = 'rgba(41, 41, 46, 0.5)'; } }, 200); }); // Add click event to open memories modal - also check memory state again mem0Button.addEventListener('click', async () => { try { const memoryEnabled = await getMemoryEnabledState(); if (memoryEnabled) { await handleMem0Modal('mem0-icon-button'); } else { // Show login modal for non-logged in users showLoginModal(); // Remove the button since memory is disabled removeMem0IconButton(); } } catch { showLoginModal(); } }); // Assemble the button iconContainer.appendChild(icon); mem0Button.appendChild(iconContainer); mem0Button.appendChild(buttonText); mem0Button.appendChild(notificationDot); mem0ButtonContainer.appendChild(mem0Button); mem0ButtonContainer.appendChild(tooltip); // Insert the button in the appropriate position try { if (status === 'found_search_button' || status === 'found_search_button_alt') { // Position after the search button (to the right) const searchButtonParent = searchButton ? searchButton.closest('div[role="button"]') : null; if (searchButtonParent && searchButtonParent.nextSibling) { buttonContainer.insertBefore(mem0ButtonContainer, searchButtonParent.nextSibling); } else { buttonContainer.appendChild(mem0ButtonContainer); } } else if (status === 'found_toolbar' || status === 'found_role_toolbar') { // Find an appropriate position in the toolbar - prefer the right side const lastChild = buttonContainer.lastChild; if (lastChild) { buttonContainer.insertBefore(mem0ButtonContainer, null); // append to end } else { buttonContainer.appendChild(mem0ButtonContainer); } } else if (status === 'created_custom_container') { // Custom container - just append buttonContainer.appendChild(mem0ButtonContainer); } else { // Other cases - try to position after any buttons in the container const buttons = buttonContainer.querySelectorAll('div[role="button"]'); if (buttons.length > 0) { const lastButton = buttons[buttons.length - 1]; buttonContainer.insertBefore( mem0ButtonContainer, lastButton && lastButton.nextSibling ? lastButton.nextSibling : null ); } else { buttonContainer.appendChild(mem0ButtonContainer); } } // Only log the first time, not on subsequent calls if (!window.mem0ButtonAdded) { window.mem0ButtonAdded = true; } } catch { return { success: false, status: 'insert_failed', error: 'Insert failed' }; } // Update notification dot based on input content try { updateNotificationDot(); } catch { // Ignore errors } return { success: true, status: status }; } } // Function to update the notification dot function updateNotificationDot() { const inputElement = getInputElement(); const notificationDot = document.querySelector('#mem0-notification-dot'); if (inputElement && notificationDot) { // Function to check if input has text const checkForText = () => { const inputText = inputElement.value || ''; const hasText = inputText.trim() !== ''; if (hasText) { notificationDot.classList.add('active'); notificationDot.style.display = 'block'; } else { notificationDot.classList.remove('active'); notificationDot.style.display = 'none'; } }; // Set up an observer to watch for changes to the input field const inputObserver = new MutationObserver(checkForText); // Start observing the input element inputObserver.observe(inputElement, { attributes: true, attributeFilter: ['value'], }); if (!inputElement.dataset.deepseekNotificationHooked) { inputElement.dataset.deepseekNotificationHooked = 'true'; inputElement.addEventListener('input', checkForText); } // Initial check checkForText(); } } // Add a function to clear memories after sending a message function addSendButtonListener(): void { // Get all potential buttons const allButtons = document.querySelectorAll('div[role="button"]'); // Log details of each button for debugging Array.from(allButtons).forEach(() => { // Debug logging }); // Try to get the send button const sendButton = getSendButtonElement(); if (sendButton) { if (!sendButton.dataset.mem0Listener) { sendButton.dataset.mem0Listener = 'true'; sendButton.addEventListener('click', function () { // Clear all memories after sending setTimeout(() => { allMemories = []; allMemoriesById.clear(); }, 100); }); } else { // Button already has listener } } else { // No send button found } } // Call the initialization function initializeMem0Integration(); ================================================ FILE: src/direct-url-tracker.ts ================================================ import { type ApiMemoryRequest, DEFAULT_USER_ID, MessageRole, SOURCE } from './types/api'; import type { OnCommittedDetails } from './types/browser'; import { Category, Provider } from './types/providers'; import type { Settings } from './types/settings'; import { StorageKey } from './types/storage'; function getSettings(): Promise { return new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.ACCESS_TOKEN, StorageKey.USER_ID, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.MEMORY_ENABLED, ], d => { resolve({ hasCreds: Boolean(d[StorageKey.API_KEY] || d[StorageKey.ACCESS_TOKEN]), apiKey: d[StorageKey.API_KEY], accessToken: d[StorageKey.ACCESS_TOKEN], userId: d[StorageKey.USER_ID] || DEFAULT_USER_ID, orgId: d[StorageKey.SELECTED_ORG], projectId: d[StorageKey.SELECTED_PROJECT], memoryEnabled: d[StorageKey.MEMORY_ENABLED] !== false, }); } ); }); } async function addMemory(content: string, settings: Settings, pageUrl: string): Promise { const headers: Record = { 'Content-Type': 'application/json' }; if (settings.accessToken) { headers.Authorization = `Bearer ${settings.accessToken}`; } else if (settings.apiKey) { headers.Authorization = `Token ${settings.apiKey}`; } else { throw new Error('Missing credentials'); } const body: ApiMemoryRequest = { messages: [{ role: MessageRole.User, content }], user_id: settings.userId, metadata: { provider: Provider.DirectURL, category: Category.NAVIGATION, page_url: pageUrl || '', }, source: SOURCE, }; if (settings.orgId) { body.org_id = settings.orgId; } if (settings.projectId) { body.project_id = settings.projectId; } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); try { const res = await fetch('https://api.mem0.ai/v1/memories/', { method: 'POST', headers, body: JSON.stringify(body), signal: controller.signal, }); return res.ok; } finally { clearTimeout(timeout); } } function shouldTrackTyped(details: OnCommittedDetails): boolean { if (!details || details.frameId !== 0) { return false; } const url = details.url || ''; if (!/^https?:\/\//i.test(url)) { return false; } const type = details.transitionType || ''; const qualifiers = details.transitionQualifiers || []; if (type === 'typed') { return true; } if (qualifiers && qualifiers.includes('from_address_bar')) { return true; } return false; } export function initDirectUrlTracking(): void { try { chrome.webNavigation.onCommitted.addListener(async (details: OnCommittedDetails) => { try { if (!shouldTrackTyped(details)) { return; } const url = details.url; if (!url) { return; } if (isSearchResultsUrl(url)) { return; } const settings = await getSettings(); if (!settings.hasCreds || settings.memoryEnabled === false) { return; } // Gate by track_searches toggle (default OFF if undefined). We treat typed URL as part of tracking searches/history. const allow = await new Promise(resolve => { try { chrome.storage.sync.get([StorageKey.TRACK_SEARCHES], d => { resolve(d[StorageKey.TRACK_SEARCHES] === true); }); } catch { resolve(false); } }); if (!allow) { return; } const hostname = (() => { try { return new URL(url).hostname; } catch { return ''; } })(); const ts = formatTimestamp(); const content = `User visited ${url}${hostname ? ` (${hostname})` : ''} on ${ts.date} at ${ts.time}`; await addMemory(content, settings, url); } catch { // no-op } }); } catch { // no-op } } function isSearchResultsUrl(urlString: string): boolean { try { const u = new URL(urlString); const host = u.hostname || ''; const path = u.pathname || ''; const params = u.searchParams || new URLSearchParams(); if ( (/^.*\.google\.[^\\/]+$/.test(host) || host === 'google.com' || host.endsWith('.google.com')) && path.startsWith('/search') ) { if (params.get('q')) { return true; } } if (host.endsWith('bing.com') && (path === '/search' || path === '/')) { if (params.get('q')) { return true; } } if (host === 'search.brave.com' && (path === '/search' || path === '/images')) { if (params.get('q')) { return true; } } if (host === 'search.arc.net' && path.startsWith('/search')) { if (params.get('q') || params.get('query')) { return true; } } return false; } catch { return false; } } function formatTimestamp(): { date: string; time: string } { try { const now = new Date(); const date = now.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', }); const time = now.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit', }); return { date, time }; } catch { return { date: new Date().toISOString().slice(0, 10), time: new Date().toISOString().slice(11, 16), }; } } ================================================ FILE: src/gemini/content.ts ================================================ import { MessageRole } from '../types/api'; import type { ExtendedElement, MutableMutationObserver } from '../types/dom'; import type { MemorySearchItem, OptionalApiParams } from '../types/memory'; import { SidebarAction } from '../types/messages'; import { type StorageData, StorageKey } from '../types/storage'; import { createOrchestrator, type SearchStorage } from '../utils/background_search'; import { OPENMEMORY_PROMPTS } from '../utils/llm_prompts'; import { SITE_CONFIG } from '../utils/site_config'; import { getBrowser, sendExtensionEvent } from '../utils/util_functions'; import { OPENMEMORY_UI, type Placement } from '../utils/util_positioning'; export {}; let isProcessingMem0 = false; let memoryModalShown: boolean = false; // Global variable to store all memories let allMemories: string[] = []; // Track added memories by ID const allMemoriesById: Set = new Set(); // Reference to the modal overlay for updates let currentModalOverlay: HTMLDivElement | null = null; let inputObserver: MutationObserver | null = null; // **PERFORMANCE FIX: Add initialization flags and cleanup variables** let isInitialized: boolean = false; let sendListenerAdded: boolean = false; let mainObserver: MutableMutationObserver | null = null; let notificationObserver: MutationObserver | null = null; let setupRetryCount: number = 0; const MAX_SETUP_RETRIES: number = 10; // **TIMING FIX: Add periodic element detection** let elementDetectionInterval: number | null = null; let lastFoundTextarea: HTMLElement | null = null; let lastFoundSendButton: HTMLButtonElement | null = null; const geminiSearch = createOrchestrator({ fetch: async function (query: string, opts: { signal?: AbortSignal }) { const data = await new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, StorageKey.SIMILARITY_THRESHOLD, StorageKey.TOP_K, ], function (items) { resolve(items as SearchStorage); } ); }); const apiKey = data[StorageKey.API_KEY]; const accessToken = data[StorageKey.ACCESS_TOKEN]; if (!apiKey && !accessToken) { return []; } const authHeader = accessToken ? `Bearer ${accessToken}` : `Token ${apiKey}`; const userId = data[StorageKey.USER_ID_CAMEL] || data[StorageKey.USER_ID] || 'chrome-extension-user'; const threshold = data[StorageKey.SIMILARITY_THRESHOLD] !== undefined ? data[StorageKey.SIMILARITY_THRESHOLD] : 0.1; const topK = data[StorageKey.TOP_K] !== undefined ? data[StorageKey.TOP_K] : 10; const optionalParams: OptionalApiParams = {}; if (data[StorageKey.SELECTED_ORG]) { optionalParams.org_id = data[StorageKey.SELECTED_ORG]; } if (data[StorageKey.SELECTED_PROJECT]) { optionalParams.project_id = data[StorageKey.SELECTED_PROJECT]; } const payload = { query, filters: { user_id: userId }, rerank: true, threshold: threshold, top_k: topK, filter_memories: false, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }; const res = await fetch('https://api.mem0.ai/v2/memories/search/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify(payload), signal: opts && opts.signal, }); if (!res.ok) { throw new Error(`API request failed with status ${res.status}`); } return await res.json(); }, // Don’t render on prefetch. When modal is open, update it. onSuccess: function (normQuery: string, responseData: MemorySearchItem[]) { if (!memoryModalShown) { return; } const memoryItems = ((responseData as MemorySearchItem[]) || []).map( (item: MemorySearchItem) => ({ id: String(item.id), text: item.memory, categories: item.categories || [], }) ); createMemoryModal(memoryItems, false); }, onError: function () { if (memoryModalShown) { createMemoryModal([], false); } }, minLength: 3, debounceMs: 75, cacheTTL: 60000, }); let geminiBackgroundSearchHandler: (() => void) | null = null; function hookGeminiBackgroundSearchTyping() { const textarea = getTextarea(); if (!textarea) { return; } if (textarea.dataset.geminiBackgroundHooked) { return; } textarea.dataset.geminiBackgroundHooked = 'true'; if (!geminiBackgroundSearchHandler) { geminiBackgroundSearchHandler = function () { const text = (textarea.textContent || textarea.innerText || '').trim(); (geminiSearch as { setText: (text: string) => void }).setText(text); }; } textarea.addEventListener('input', geminiBackgroundSearchHandler); textarea.addEventListener('keyup', geminiBackgroundSearchHandler); } function getTextarea(): HTMLElement | null { const selectors = [ 'rich-textarea .ql-editor[contenteditable="true"]', 'rich-textarea .ql-editor.textarea', '.ql-editor[aria-label="Enter a prompt here"]', '.ql-editor.textarea.new-input-ui', '.text-input-field_textarea .ql-editor', 'div[contenteditable="true"][role="textbox"][aria-label="Enter a prompt here"]', ]; for (const selector of selectors) { const textarea = document.querySelector(selector) as HTMLElement | null; if (textarea) { // **TIMING FIX: Store reference for comparison** if (lastFoundTextarea !== textarea) { lastFoundTextarea = textarea; // Reset listener flag when new textarea is found if (textarea.dataset.mem0KeyListener !== 'true') { sendListenerAdded = false; } } // **PERFORMANCE FIX: Trigger listener setup if not done yet** if (!sendListenerAdded) { setTimeout(() => { if (!sendListenerAdded) { addSendButtonListener(); } }, 500); } return textarea; } } return null; } // **TIMING FIX: Add function to detect send button** function getSendButton(): HTMLButtonElement | null { const selectors = [ 'button[aria-label="Send message"]', 'button[data-testid="send-button"]', 'button[type="submit"]:not([aria-label*="attachment"])', '.send-button', 'button[aria-label*="Send"]', 'button[title*="Send"]', ]; for (const selector of selectors) { const button = document.querySelector(selector) as HTMLButtonElement | null; if (button) { // **TIMING FIX: Store reference for comparison** if (lastFoundSendButton !== button) { lastFoundSendButton = button; // Reset listener flag when new button is found if (button.dataset.mem0Listener !== 'true') { sendListenerAdded = false; } } return button; } } return null; } // **TIMING FIX: Add periodic element detection** function startElementDetection(): void { if (elementDetectionInterval) { clearInterval(elementDetectionInterval); } elementDetectionInterval = window.setInterval(() => { const textarea = getTextarea(); const sendButton = getSendButton(); // If we found elements and listeners aren't set up, try to set them up if ((textarea || sendButton) && !sendListenerAdded) { addSendButtonListener(); } // If both elements are found and listeners are set up, we can reduce frequency if (textarea && sendButton && sendListenerAdded) { if (elementDetectionInterval) { clearInterval(elementDetectionInterval); } // Check less frequently once everything is set up elementDetectionInterval = window.setInterval(() => { const currentTextarea = getTextarea(); const currentSendButton = getSendButton(); if ((!currentTextarea || !currentSendButton) && sendListenerAdded) { sendListenerAdded = false; lastFoundTextarea = null; lastFoundSendButton = null; } }, 5000); // Check every 5 seconds for maintenance } }, 1000); // Check every second initially } function setupInputObserver(): void { // **PERFORMANCE FIX: Prevent multiple observers and add retry limit** if (inputObserver) { return; } const textarea = getTextarea(); if (!textarea) { if (setupRetryCount < MAX_SETUP_RETRIES) { setupRetryCount++; setTimeout(setupInputObserver, 500); } return; } // **PERFORMANCE FIX: Reset retry count on success** setupRetryCount = 0; } function setInputValue(inputElement: HTMLElement, value: string): void { if (inputElement) { // For contenteditable divs, we need to set innerHTML or textContent if (inputElement.contentEditable === 'true') { // Clear existing content inputElement.innerHTML = ''; // Split the value by newlines and create paragraph elements const lines = value.split('\n'); lines.forEach(line => { const p = document.createElement('p'); if (line.trim() === '') { p.innerHTML = '
'; } else { p.textContent = line; } inputElement.appendChild(p); }); // Trigger input event inputElement.dispatchEvent(new Event('input', { bubbles: true })); // Focus and set cursor to end inputElement.focus(); // Set cursor to end of content const range = document.createRange(); const selection = window.getSelection(); range.selectNodeContents(inputElement); range.collapse(false); if (selection) { selection.removeAllRanges(); selection.addRange(range); } } else { // Fallback for regular input/textarea elements (inputElement as HTMLTextAreaElement).value = value; inputElement.dispatchEvent(new Event('input', { bubbles: true })); } } } // Function to get the content without any memory wrappers function getContentWithoutMemories(): string { const inputElement = getTextarea(); if (!inputElement) { return ''; } let content = inputElement.textContent || ''; // Remove any memory headers and content const memoryPrefix = OPENMEMORY_PROMPTS.memory_header_text; const prefixIndex = content.indexOf(memoryPrefix); if (prefixIndex !== -1) { content = content.substring(0, prefixIndex).trim(); } // Also try with regex pattern try { const MEM0_PLAIN = OPENMEMORY_PROMPTS.memory_header_plain_regex; content = content.replace(MEM0_PLAIN, '').trim(); } catch { // Ignore regex errors } return content; } // Function to check if memory is enabled function getMemoryEnabledState(): Promise { return new Promise(resolve => { chrome.storage.sync.get([StorageKey.MEMORY_ENABLED], function (result) { resolve(result.memory_enabled !== false); // Default to true if not set }); }); } // Track if memory has been captured for this session to prevent duplicates let memoryCaptured = false; let lastCapturedMessage = ''; // Add a function to handle send button actions and clear memories after sending function addSendButtonListener(): void { // **PERFORMANCE FIX: Prevent duplicate listener registration** if (sendListenerAdded) { return; } // Handle capturing and storing the current message function captureAndStoreMemory(): void { const textarea = getTextarea(); if (!textarea) { return; } const message = ((textarea as HTMLElement).textContent || '').trim(); if (!message) { return; } // Clean message from any existing memory content const cleanMessage = getContentWithoutMemories(); // Prevent duplicate captures for the same message if (memoryCaptured && lastCapturedMessage === cleanMessage) { return; } memoryCaptured = true; lastCapturedMessage = cleanMessage; // Reset the capture flag after a short delay setTimeout(() => { memoryCaptured = false; lastCapturedMessage = ''; }, 1000); // Asynchronously store the memory chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.MEMORY_ENABLED, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, ], function (items) { // Skip if memory is disabled or no credentials if (items.memory_enabled === false || (!items.apiKey && !items.access_token)) { return; } const authHeader = items.access_token ? `Bearer ${items.access_token}` : `Token ${items.apiKey}`; const userId = items.userId || items.user_id || 'chrome-extension-user'; const optionalParams: OptionalApiParams = {}; if (items.selected_org) { optionalParams.org_id = items.selected_org; } if (items.selected_project) { optionalParams.project_id = items.selected_project; } // Send memory to mem0 API asynchronously without waiting for response fetch('https://api.mem0.ai/v1/memories/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify({ messages: [{ role: MessageRole.User, content: cleanMessage }], user_id: userId, infer: true, metadata: { provider: 'Gemini', }, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }), }).catch(() => { // Ignore errors }); } ); // Clear all memories after sending setTimeout(() => { allMemories = []; allMemoriesById.clear(); }, 100); } // **TIMING FIX: Use the new getSendButton function** const sendButton = getSendButton(); if (sendButton && !sendButton.dataset.mem0Listener) { sendButton.dataset.mem0Listener = 'true'; sendButton.addEventListener('click', function () { captureAndStoreMemory(); }); } // Handle textarea for Enter key press separately const textarea = getTextarea(); if (textarea) { if (!textarea.dataset.mem0KeyListener) { textarea.dataset.mem0KeyListener = 'true'; textarea.addEventListener('keydown', function (event) { // Check if Enter was pressed without Shift (standard send behavior) if (event.key === 'Enter' && !event.shiftKey) { // Don't capture here if send button will also trigger // The send button click will handle the capture return; } }); } } // **TIMING FIX: Only mark as added if we actually found and set up both elements** if (textarea && sendButton) { sendListenerAdded = true; } } function injectMem0Button(): void { // Prefer OPENMEMORY_UI mounts; fall back to legacy injection only if unavailable if (OPENMEMORY_UI && OPENMEMORY_UI.mountOnEditorFocus) { try { if (!document.getElementById('mem0-icon-button')) { OPENMEMORY_UI.resolveCachedAnchor( { learnKey: location.host + ':' + location.pathname }, null, 24 * 60 * 60 * 1000 ).then(function (hit: { el: Element; placement: Placement | null } | null) { if (!hit || !hit.el) { return; } let hs = OPENMEMORY_UI.createShadowRootHost('mem0-root'); let host = hs.host, shadow = hs.shadow; host.id = 'mem0-icon-button'; let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.gemini ? SITE_CONFIG.gemini : null; let placement = hit.placement || (cfg && cfg.placement) || { strategy: 'inline' as const, where: 'beforeend' as const, inlineAlign: 'end' as const, }; OPENMEMORY_UI.applyPlacement({ container: host, anchor: hit.el, placement: placement }); let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal(); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } }); } } catch { // Ignore errors during re-initialization } OPENMEMORY_UI.mountOnEditorFocus({ existingHostSelector: '#mem0-icon-button', editorSelector: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.gemini && SITE_CONFIG.gemini.editorSelector ? SITE_CONFIG.gemini.editorSelector : 'textarea, [contenteditable="true"], input[type="text"]', deriveAnchor: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.gemini && typeof SITE_CONFIG.gemini.deriveAnchor === 'function' ? SITE_CONFIG.gemini.deriveAnchor : function (editor: Element) { return editor.closest('form') || editor.parentElement; }, placement: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.gemini && SITE_CONFIG.gemini.placement ? SITE_CONFIG.gemini.placement : { strategy: 'inline', where: 'beforeend', inlineAlign: 'end' }, render: function (shadow: ShadowRoot, host: HTMLElement) { host.id = 'mem0-icon-button'; let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal(); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } }, fallback: function () { let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.gemini ? SITE_CONFIG.gemini : null; return OPENMEMORY_UI.mountResilient({ anchors: [ { find: function () { let sel = (cfg && cfg.editorSelector) || 'textarea, [contenteditable="true"], input[type="text"]'; let ed = document.querySelector(sel); if (!ed) { return null; } try { return cfg && typeof cfg.deriveAnchor === 'function' ? cfg.deriveAnchor(ed) : ed.closest('form') || ed.parentElement; } catch (_) { return ed.closest('form') || ed.parentElement; } }, }, ], placement: (cfg && cfg.placement) || { strategy: 'inline', where: 'beforeend', inlineAlign: 'end', }, enableFloatingFallback: true, render: function (shadow: ShadowRoot, host: HTMLElement) { host.id = 'mem0-icon-button'; let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal(); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } }, }); }, }); return; } let buttonRetryCount = 0; const maxButtonRetries = 10; // Function to periodically check and add the button if the parent element exists async function tryAddButton() { // **PERFORMANCE FIX: Add retry limit** if (buttonRetryCount >= maxButtonRetries) { return; } buttonRetryCount++; // First check if memory is enabled const memoryEnabled = await getMemoryEnabledState(); // Remove existing button if memory is disabled if (!memoryEnabled) { const existingButton = document.querySelector('#mem0-icon-button'); if (existingButton && existingButton.parentElement) { existingButton.parentElement.remove(); } // **PERFORMANCE FIX: Reset retry count and set longer timeout** buttonRetryCount = 0; setTimeout(tryAddButton, 10000); return; } // Look for the toolbox-drawer container const toolboxDrawer = document.querySelector('toolbox-drawer .toolbox-drawer-container'); if (!toolboxDrawer) { setTimeout(tryAddButton, 1000); return; } // Check if our button already exists if (document.querySelector('#mem0-icon-button')) { // **PERFORMANCE FIX: Reset retry count on success** buttonRetryCount = 0; return; } // **PERFORMANCE FIX: Reset retry count when successfully creating button** buttonRetryCount = 0; // Create mem0 button container to match toolbox-drawer-item structure const mem0ButtonContainer = document.createElement('toolbox-drawer-item'); mem0ButtonContainer.className = 'mat-mdc-tooltip-trigger toolbox-drawer-item-button ng-tns-c1279795495-8 mat-mdc-tooltip-disabled ng-star-inserted'; mem0ButtonContainer.style.position = 'relative'; // For popover positioning mem0ButtonContainer.style.backgroundColor = 'transparent'; mem0ButtonContainer.style.border = 'none'; // Create mem0 button to match the toolbox drawer button style const mem0Button = document.createElement('button'); mem0Button.className = 'mat-ripple mat-mdc-tooltip-trigger toolbox-drawer-item-button gds-label-l is-mobile ng-star-inserted'; mem0Button.setAttribute('matripple', ''); mem0Button.setAttribute('aria-pressed', 'false'); mem0Button.setAttribute('aria-label', 'Mem0'); mem0Button.id = 'mem0-icon-button'; mem0Button.style.backgroundColor = 'transparent'; mem0Button.style.border = 'none'; // Create the button label div to match other toolbox items const buttonLabel = document.createElement('div'); buttonLabel.className = 'toolbox-drawer-button-label label'; buttonLabel.style.cssText = ` display: flex; align-items: center; gap: 6px; font-family: 'Google Sans', Roboto, sans-serif; font-size: 14px; font-weight: 500; background-color: transparent; `; // Create notification dot const notificationDot = document.createElement('div'); notificationDot.id = 'mem0-notification-dot'; notificationDot.style.cssText = ` position: absolute; top: -2px; right: -2px; width: 8px; height: 8px; background-color: #34a853; border-radius: 50%; border: 1px solid #fff; display: none; z-index: 1001; pointer-events: none; `; // Add keyframe animation for the dot if (!document.getElementById('notification-dot-animation')) { const style = document.createElement('style'); style.id = 'notification-dot-animation'; style.innerHTML = ` @keyframes popIn { 0% { transform: scale(0); } 50% { transform: scale(1.2); } 100% { transform: scale(1); } } #mem0-notification-dot.active { display: block !important; animation: popIn 0.3s ease-out forwards; } `; document.head.appendChild(style); } // Add icon and text to button label const iconImg = document.createElement('img'); iconImg.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); iconImg.style.cssText = ` width: 18px; height: 18px; border-radius: 50%; `; const labelText = document.createElement('span'); labelText.textContent = 'Mem0'; buttonLabel.appendChild(iconImg); buttonLabel.appendChild(labelText); mem0Button.appendChild(buttonLabel); // Create popover element (hidden by default) const popover = document.createElement('div'); popover.className = 'mem0-button-popover'; popover.style.cssText = ` position: absolute; bottom: 48px; left: 50%; transform: translateX(-50%); background-color: #2d2e30; border: 1px solid #5f6368; color: white; padding: 8px 12px; border-radius: 6px; font-size: 12px; white-space: nowrap; z-index: 10001; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); display: none; transition: opacity 0.2s; font-family: 'Google Sans', Roboto, sans-serif; `; popover.textContent = 'Add memories to your prompt'; // Add arrow const arrow = document.createElement('div'); arrow.style.cssText = ` position: absolute; top: 100%; left: 50%; transform: translateX(-50%) rotate(45deg); width: 8px; height: 8px; background-color: #2d2e30; border-right: 1px solid #5f6368; border-bottom: 1px solid #5f6368; `; popover.appendChild(arrow); // Add hover event for popover mem0ButtonContainer.addEventListener('mouseenter', () => { popover.style.display = 'block'; setTimeout(() => (popover.style.opacity = '1'), 10); }); mem0ButtonContainer.addEventListener('mouseleave', () => { popover.style.opacity = '0'; setTimeout(() => (popover.style.display = 'none'), 200); }); // Add click event to the mem0 button to show memory modal mem0Button.addEventListener('click', function () { // Check if the memories are enabled getMemoryEnabledState().then(memoryEnabled => { if (memoryEnabled) { handleMem0Modal(); } else { // If memories are disabled, open options chrome.runtime.sendMessage({ action: SidebarAction.OPEN_OPTIONS }); } }); }); // Assemble button components mem0ButtonContainer.appendChild(mem0Button); mem0ButtonContainer.appendChild(notificationDot); mem0ButtonContainer.appendChild(popover); // Insert the button into the toolbox drawer toolboxDrawer.appendChild(mem0ButtonContainer); // Update notification dot based on input content updateNotificationDot(); // Ensure notification dot is updated after DOM is fully loaded setTimeout(updateNotificationDot, 500); } // Start trying to add the button tryAddButton(); } // Function to update notification dot visibility based on text in the input function updateNotificationDot(): void { const textarea = getTextarea(); const notificationDot = document.querySelector('#mem0-notification-dot'); if (textarea && notificationDot) { // Function to check if input has text const checkForText = () => { const inputText = (textarea as HTMLElement).textContent || ''; const hasText = inputText.trim() !== ''; if (hasText) { notificationDot.classList.add('active'); // Force display style notificationDot.style.display = 'block'; } else { notificationDot.classList.remove('active'); notificationDot.style.display = 'none'; } }; // **PERFORMANCE FIX: Clean up existing observer first** if (notificationObserver) { notificationObserver.disconnect(); } // Set up an observer to watch for changes to the input field notificationObserver = new MutationObserver(checkForText); // Start observing the input element notificationObserver.observe(textarea as Node, { characterData: true, subtree: true, childList: true, }); if (!textarea.dataset.geminiNotificationHooked) { textarea.dataset.geminiNotificationHooked = 'true'; textarea.addEventListener('input', checkForText); textarea.addEventListener('keyup', checkForText); textarea.addEventListener('focus', checkForText); } // Initial check checkForText(); // Force check after a small delay to ensure DOM is fully loaded setTimeout(checkForText, 500); } else { // **PERFORMANCE FIX: Add retry limit for notification dot setup** let notificationRetryCount = 0; const maxNotificationRetries = 5; const retryNotificationSetup = () => { if (notificationRetryCount < maxNotificationRetries) { notificationRetryCount++; setTimeout(updateNotificationDot, 1000); } }; retryNotificationSetup(); } } // Shared function to update the input field with all collected memories function updateInputWithMemories(): void { const inputElement = getTextarea(); if (inputElement && allMemories.length > 0) { // Get the content without any existing memory wrappers const baseContent = getContentWithoutMemories(); // Create the memory string with all collected memories let memoriesContent = '\n\n' + OPENMEMORY_PROMPTS.memory_header_text + '\n'; // Add all memories to the content allMemories.forEach((mem, index) => { memoriesContent += `- ${mem}`; if (index < allMemories.length - 1) { memoriesContent += '\n'; } }); // Add the final content to the input setInputValue(inputElement, baseContent + memoriesContent); } } // Function to show a small popup message near the button function showButtonPopup(button: HTMLElement, message: string): void { // Remove any existing popups const existingPopup = document.querySelector('.mem0-button-popup'); if (existingPopup) { existingPopup.remove(); } const popup = document.createElement('div'); popup.className = 'mem0-button-popup'; popup.style.cssText = ` position: absolute; top: -40px; left: 50%; transform: translateX(-50%); background-color: #2d2e30; border: 1px solid #5f6368; color: white; padding: 8px 12px; border-radius: 6px; font-size: 12px; white-space: nowrap; z-index: 10001; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); font-family: 'Google Sans', Roboto, sans-serif; `; popup.textContent = message; // Create arrow const arrow = document.createElement('div'); arrow.style.cssText = ` position: absolute; bottom: -5px; left: 50%; transform: translateX(-50%) rotate(45deg); width: 8px; height: 8px; background-color: #2d2e30; border-right: 1px solid #5f6368; border-bottom: 1px solid #5f6368; `; popup.appendChild(arrow); // Position relative to button button.style.position = 'relative'; button.appendChild(popup); // Auto-remove after 3 seconds setTimeout(() => { if (popup && popup.parentElement) { popup.remove(); } }, 3000); } // Function to show login popup function showLoginPopup(): void { // First remove any existing popups const existingPopup = document.querySelector('#mem0-login-popup'); if (existingPopup) { existingPopup.remove(); } // Create popup container const popupOverlay = document.createElement('div'); popupOverlay.id = 'mem0-login-popup'; popupOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10001; `; const popupContainer = document.createElement('div'); popupContainer.style.cssText = ` background-color: #2d2e30; border-radius: 12px; width: 320px; padding: 24px; color: white; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); font-family: 'Google Sans', Roboto, sans-serif; `; // Close button const closeButton = document.createElement('button'); closeButton.style.cssText = ` position: absolute; top: 16px; right: 16px; background: none; border: none; color: #9aa0a6; font-size: 16px; cursor: pointer; `; closeButton.innerHTML = '×'; closeButton.addEventListener('click', () => { document.body.removeChild(popupOverlay); }); // Logo and heading const logoContainer = document.createElement('div'); logoContainer.style.cssText = ` display: flex; align-items: center; justify-content: center; margin-bottom: 16px; `; const heading = document.createElement('h2'); heading.textContent = 'Sign in to OpenMemory'; heading.style.cssText = ` margin: 0; font-size: 18px; font-weight: 600; `; logoContainer.appendChild(heading); // Message const message = document.createElement('p'); message.textContent = 'Please sign in to access your memories and enhance your conversations!'; message.style.cssText = ` margin-bottom: 24px; color: #e8eaed; font-size: 14px; line-height: 1.5; text-align: center; `; // Sign in button const signInButton = document.createElement('button'); signInButton.style.cssText = ` display: flex; align-items: center; justify-content: center; width: 100%; padding: 10px; background-color: #1a73e8; color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: background-color 0.2s; font-family: 'Google Sans', Roboto, sans-serif; gap: 8px; `; // Add logo and text const logoDark = document.createElement('img'); logoDark.src = chrome.runtime.getURL('icons/mem0-claude-icon.png'); logoDark.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; `; const signInText = document.createElement('span'); signInText.textContent = 'Sign in with OpenMemory'; signInButton.appendChild(logoDark); signInButton.appendChild(signInText); signInButton.addEventListener('mouseenter', () => { signInButton.style.backgroundColor = '#1557b0'; }); signInButton.addEventListener('mouseleave', () => { signInButton.style.backgroundColor = '#1a73e8'; }); // Open sign-in page when clicked signInButton.addEventListener('click', () => { window.open('https://app.mem0.ai/login', '_blank'); document.body.removeChild(popupOverlay); }); // Assemble popup popupContainer.appendChild(logoContainer); popupContainer.appendChild(message); popupContainer.appendChild(signInButton); popupOverlay.appendChild(popupContainer); popupOverlay.appendChild(closeButton); // Add click event to close when clicking outside popupOverlay.addEventListener('click', (e: MouseEvent) => { if (e.target === popupOverlay) { document.body.removeChild(popupOverlay); } }); // Add to body document.body.appendChild(popupOverlay); } function createMemoryModal( memoryItems: Array<{ id?: string; text: string; categories?: string[] }>, isLoading: boolean = false ): void { // Close existing modal if it exists if (memoryModalShown && currentModalOverlay) { document.body.removeChild(currentModalOverlay); } memoryModalShown = true; let currentMemoryIndex = 0; // Calculate modal dimensions (estimated) const modalWidth = 447; let modalHeight = 400; // Default height let memoriesPerPage = 3; // Default number of memories per page let topPosition: number; let leftPosition: number; // Position relative to the Mem0 button const mem0Button = (document.querySelector('#mem0-icon-button') as HTMLElement) || (document.querySelector('button[aria-label="Mem0"]') as HTMLElement); if (mem0Button) { const buttonRect = mem0Button.getBoundingClientRect(); // Determine if there's enough space below the button const viewportHeight = window.innerHeight; const spaceBelow = viewportHeight - buttonRect.bottom; // Position the modal centered under the button leftPosition = Math.max(buttonRect.left + buttonRect.width / 2 - modalWidth / 2, 10); // Ensure the modal doesn't go off the right edge of the screen const rightEdgePosition = leftPosition + modalWidth; if (rightEdgePosition > window.innerWidth - 10) { leftPosition = window.innerWidth - modalWidth - 10; } if (spaceBelow >= modalHeight) { // Place below the button topPosition = buttonRect.bottom + 10; } else { // Place above the button if not enough space below topPosition = buttonRect.top - modalHeight - 10; // Check if it's in the upper half of the screen if (buttonRect.top < viewportHeight / 2) { modalHeight = 300; // Reduced height memoriesPerPage = 2; // Show only 2 memories } } } else { // Fallback positioning topPosition = 100; leftPosition = window.innerWidth / 2 - modalWidth / 2; } // Create modal overlay const modalOverlay = document.createElement('div'); modalOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: transparent; display: flex; z-index: 10000; pointer-events: auto; `; // Save reference to current modal overlay currentModalOverlay = modalOverlay; // Add event listener to close modal when clicking outside modalOverlay.addEventListener('click', (event: MouseEvent) => { // Only close if clicking directly on the overlay, not its children if (event.target === modalOverlay) { closeModal(); } }); // Create modal container with positioning const modalContainer = document.createElement('div'); modalContainer.style.cssText = ` background-color: #2d2e30; border-radius: 12px; width: ${modalWidth}px; height: ${modalHeight}px; display: flex; flex-direction: column; color: white; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); position: absolute; top: ${topPosition}px; left: ${leftPosition}px; pointer-events: auto; border: 1px solid #5f6368; font-family: 'Google Sans', Roboto, sans-serif; overflow: hidden; `; // Create modal header const modalHeader = document.createElement('div'); modalHeader.style.cssText = ` display: flex; align-items: center; padding: 10px 16px; justify-content: space-between; background-color: #35373a; flex-shrink: 0; `; // Create header left section with just the logo const headerLeft = document.createElement('div'); headerLeft.style.cssText = ` display: flex; flex-direction: row; align-items: center; `; // Add Mem0 logo const logoImg = document.createElement('img'); logoImg.src = chrome.runtime.getURL('icons/mem0-claude-icon.png'); logoImg.style.cssText = ` width: 26px; height: 26px; border-radius: 50%; `; // Add "OpenMemory" title const title = document.createElement('div'); title.textContent = 'OpenMemory'; title.style.cssText = ` font-size: 16px; font-weight: 600; color: white; margin-left: 8px; `; // Create header right section const headerRight = document.createElement('div'); headerRight.style.cssText = ` display: flex; flex-direction: row; align-items: center; gap: 8px; `; // Create Add to Prompt button with arrow const addToPromptBtn = document.createElement('button'); addToPromptBtn.style.cssText = ` display: flex; flex-direction: row; align-items: center; padding: 5px 16px; gap: 8px; background-color:rgb(27, 27, 27); border: none; border-radius: 8px; cursor: pointer; font-size: 12px; font-weight: 600; color: white; transition: background-color 0.2s; `; addToPromptBtn.textContent = 'Add to Prompt'; // Add arrow icon to button const arrowIcon = document.createElement('span'); arrowIcon.innerHTML = ` `; arrowIcon.style.position = 'relative'; arrowIcon.style.top = '2px'; addToPromptBtn.appendChild(arrowIcon); // Add hover effect for Add to Prompt button addToPromptBtn.addEventListener('mouseenter', () => { addToPromptBtn.style.backgroundColor = 'rgb(36, 36, 36)'; }); addToPromptBtn.addEventListener('mouseleave', () => { addToPromptBtn.style.backgroundColor = 'rgb(27, 27, 27)'; }); // Create settings button const settingsBtn = document.createElement('button'); settingsBtn.style.cssText = ` background: none; border: none; cursor: pointer; padding: 8px; opacity: 0.6; transition: opacity 0.2s; `; settingsBtn.innerHTML = ` `; // Add click event to open app.mem0.ai in a new tab settingsBtn.addEventListener('click', () => { if (currentModalOverlay && document.body.contains(currentModalOverlay)) { document.body.removeChild(currentModalOverlay); memoryModalShown = false; currentModalOverlay = null; } chrome.runtime.sendMessage({ action: SidebarAction.SIDEBAR_SETTINGS }); }); // Add hover effect for the settings button settingsBtn.addEventListener('mouseenter', () => { settingsBtn.style.opacity = '1'; }); settingsBtn.addEventListener('mouseleave', () => { settingsBtn.style.opacity = '0.6'; }); // Content section const contentSection = document.createElement('div'); const contentSectionHeight = modalHeight - 130; // Account for header and navigation contentSection.style.cssText = ` display: flex; flex-direction: column; padding: 0 16px; gap: 12px; overflow: hidden; flex: 1; height: ${contentSectionHeight}px; `; // Create memories counter const memoriesCounter = document.createElement('div'); memoriesCounter.style.cssText = ` font-size: 16px; font-weight: 600; color: #FFFFFF; margin-top: 16px; flex-shrink: 0; `; // Update counter text based on loading state and number of memories if (isLoading) { memoriesCounter.textContent = `Loading Relevant Memories...`; } else { // Filter out memories that have already been added for accurate count const availableMemoriesCount = memoryItems.filter( memory => memory && memory.id && !allMemoriesById.has(memory.id) ).length; memoriesCounter.textContent = `${availableMemoriesCount} Relevant Memories`; } // Calculate max height for memories content based on modal height const memoriesContentMaxHeight = contentSectionHeight - 40; // Account for memories counter // Create memories content container with adjusted height const memoriesContent = document.createElement('div'); memoriesContent.style.cssText = ` display: flex; flex-direction: column; gap: 8px; overflow-y: auto; flex: 1; max-height: ${memoriesContentMaxHeight}px; padding-right: 8px; margin-right: -8px; scrollbar-width: thin; scrollbar-color: #5f6368 transparent; `; // Track currently expanded memory let currentlyExpandedMemory: HTMLElement | null = null; // Function to create skeleton loading items (adjusted for different heights) function createSkeletonItems() { memoriesContent.innerHTML = ''; for (let i = 0; i < memoriesPerPage; i++) { const skeletonItem = document.createElement('div'); skeletonItem.style.cssText = ` display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; padding: 12px; background-color: #3c4043; border-radius: 8px; height: 72px; flex-shrink: 0; animation: pulse 1.5s infinite ease-in-out; `; const skeletonText = document.createElement('div'); skeletonText.style.cssText = ` background-color: #5f6368; border-radius: 4px; height: 14px; width: 85%; margin-bottom: 8px; `; const skeletonText2 = document.createElement('div'); skeletonText2.style.cssText = ` background-color: #5f6368; border-radius: 4px; height: 14px; width: 65%; `; const skeletonActions = document.createElement('div'); skeletonActions.style.cssText = ` display: flex; gap: 4px; margin-left: 10px; `; const skeletonButton1 = document.createElement('div'); skeletonButton1.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; background-color: #5f6368; `; const skeletonButton2 = document.createElement('div'); skeletonButton2.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; background-color: #5f6368; `; skeletonActions.appendChild(skeletonButton1); skeletonActions.appendChild(skeletonButton2); const textContainer = document.createElement('div'); textContainer.style.cssText = ` display: flex; flex-direction: column; flex-grow: 1; `; textContainer.appendChild(skeletonText); textContainer.appendChild(skeletonText2); skeletonItem.appendChild(textContainer); skeletonItem.appendChild(skeletonActions); memoriesContent.appendChild(skeletonItem); } // Add keyframe animation to document if not exists if (!document.getElementById('skeleton-animation')) { const style = document.createElement('style'); style.id = 'skeleton-animation'; style.innerHTML = ` @keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 0.8; } 100% { opacity: 0.6; } } `; document.head.appendChild(style); } } // Function to show memories with adjusted count based on modal position function showMemories() { memoriesContent.innerHTML = ''; if (isLoading) { createSkeletonItems(); return; } // Filter out memories that have already been added const availableMemories = memoryItems.filter(memory => { const hasId = memory && memory.id; const isAlreadyAdded = hasId && allMemoriesById.has(String(memory.id)); return hasId && !isAlreadyAdded; }); // Update counter with actual available memories count memoriesCounter.textContent = isLoading ? 'Loading Relevant Memories...' : `${availableMemories.length} Relevant Memories`; if (availableMemories.length === 0) { showEmptyState(); updateNavigationState(0, 0); return; } // Use the dynamically set memoriesPerPage value const memoriesToShow = Math.min(memoriesPerPage, availableMemories.length); // Calculate total pages and current page based on available memories const totalPages = Math.ceil(availableMemories.length / memoriesToShow); const currentPage = Math.floor(currentMemoryIndex / memoriesToShow) + 1; // Adjust currentMemoryIndex if it exceeds available memories if (currentMemoryIndex >= availableMemories.length) { currentMemoryIndex = Math.max(0, availableMemories.length - memoriesToShow); } // Update navigation buttons state updateNavigationState(currentPage, totalPages); for (let i = 0; i < memoriesToShow; i++) { const memoryIndex = currentMemoryIndex + i; if (memoryIndex >= availableMemories.length) { break; } // Stop if we've reached the end const memory = availableMemories[memoryIndex]; if (!memory) { continue; } // Ensure memory has an ID if (!memory.id) { memory.id = `memory-${Date.now()}-${memoryIndex}`; } const memoryContainer = document.createElement('div'); memoryContainer.style.cssText = ` display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; padding: 12px; background-color: #3c4043; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; min-height: 56px; max-height: 56px; overflow: hidden; flex-shrink: 0; `; const memoryText = document.createElement('div'); memoryText.style.cssText = ` font-size: 14px; line-height: 1.5; color: #e8eaed; flex-grow: 1; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; transition: all 0.2s ease; height: 42px; /* Height for 2 lines of text */ `; memoryText.textContent = memory.text; const actionsContainer = document.createElement('div'); actionsContainer.style.cssText = ` display: flex; gap: 4px; margin-left: 10px; flex-shrink: 0; `; // Add button const addButton = document.createElement('button'); addButton.style.cssText = ` border: none; cursor: pointer; padding: 4px; background: #5f6368; color: #e8eaed; border-radius: 100%; width: 28px; height: 28px; transition: all 0.2s ease; `; addButton.innerHTML = ` `; // Add hover effect for add button addButton.addEventListener('mouseenter', () => { addButton.style.backgroundColor = 'rgb(36, 36, 36)'; }); addButton.addEventListener('mouseleave', () => { addButton.style.backgroundColor = '#5f6368'; }); // Add click handler for add button addButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); sendExtensionEvent('memory_injection', { provider: 'gemini', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), injected_all: false, memory_id: memory.id, }); // Add this memory allMemoriesById.add(String(memory.id)); allMemories.push(String(memory.text || '')); updateInputWithMemories(); // Refresh the memories display (no need to remove from memoryItems) showMemories(); }); // Menu button const menuButton = document.createElement('button'); menuButton.style.cssText = ` background: none; border: none; cursor: pointer; padding: 4px; color: #9aa0a6; `; menuButton.innerHTML = ` `; // Track expanded state let isExpanded = false; // Create remove button (hidden by default) const removeButton = document.createElement('button'); removeButton.style.cssText = ` display: none; align-items: center; gap: 6px; background: #5f6368; color: #e8eaed; border-radius: 8px; padding: 2px 4px; border: none; cursor: pointer; font-size: 13px; margin-top: 12px; width: fit-content; transition: background-color 0.2s; `; removeButton.innerHTML = ` Remove `; // Add hover effect for remove button removeButton.addEventListener('mouseenter', () => { removeButton.style.backgroundColor = '#ea4335'; }); removeButton.addEventListener('mouseleave', () => { removeButton.style.backgroundColor = '#5f6368'; }); // Create content wrapper for text and remove button const contentWrapper = document.createElement('div'); contentWrapper.style.cssText = ` display: flex; flex-direction: column; flex-grow: 1; `; contentWrapper.appendChild(memoryText); contentWrapper.appendChild(removeButton); // Function to expand memory const expandMemory = () => { if (currentlyExpandedMemory && currentlyExpandedMemory !== memoryContainer) { currentlyExpandedMemory.dispatchEvent(new Event('collapse')); } isExpanded = true; memoryText.style.webkitLineClamp = 'unset'; memoryText.style.height = 'auto'; contentWrapper.style.overflowY = 'auto'; contentWrapper.style.maxHeight = '240px'; // Limit height to prevent overflow contentWrapper.style.scrollbarWidth = 'thin'; contentWrapper.style.scrollbarColor = '#5f6368 transparent'; memoryContainer.style.backgroundColor = '#2d2e30'; memoryContainer.style.maxHeight = '300px'; // Allow expansion but within container memoryContainer.style.overflow = 'hidden'; removeButton.style.display = 'flex'; currentlyExpandedMemory = memoryContainer; // Scroll to make expanded memory visible if needed memoriesContent.scrollTop = memoryContainer.offsetTop - memoriesContent.offsetTop; }; // Function to collapse memory const collapseMemory = () => { isExpanded = false; memoryText.style.webkitLineClamp = '2'; memoryText.style.height = '42px'; contentWrapper.style.overflowY = 'visible'; memoryContainer.style.backgroundColor = '#3c4043'; memoryContainer.style.maxHeight = '72px'; memoryContainer.style.overflow = 'hidden'; removeButton.style.display = 'none'; currentlyExpandedMemory = null; }; memoryContainer.addEventListener('collapse', collapseMemory); menuButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); if (isExpanded) { collapseMemory(); } else { expandMemory(); } }); // Add click handler for remove button removeButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); // Remove from memoryItems const index = memoryItems.findIndex(m => m.id === memory.id); if (index !== -1) { memoryItems.splice(index, 1); // Refresh the memories display showMemories(); } }); actionsContainer.appendChild(addButton); actionsContainer.appendChild(menuButton); memoryContainer.appendChild(contentWrapper); memoryContainer.appendChild(actionsContainer); memoriesContent.appendChild(memoryContainer); // Add hover effect memoryContainer.addEventListener('mouseenter', () => { memoryContainer.style.backgroundColor = isExpanded ? '#25272a' : '#484b4f'; }); memoryContainer.addEventListener('mouseleave', () => { memoryContainer.style.backgroundColor = isExpanded ? '#2d2e30' : '#3c4043'; }); } // If after filtering for already added memories, there are no items to show, // check if we need to go to previous page if (memoriesContent.children.length === 0 && availableMemories.length > 0) { if (currentMemoryIndex > 0) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); showMemories(); } else { showEmptyState(); } } } // Function to show empty state function showEmptyState() { memoriesContent.innerHTML = ''; const emptyContainer = document.createElement('div'); emptyContainer.style.cssText = ` display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 32px 16px; text-align: center; flex: 1; min-height: 200px; `; const emptyIcon = document.createElement('div'); emptyIcon.innerHTML = ` `; emptyIcon.style.marginBottom = '16px'; const emptyText = document.createElement('div'); emptyText.textContent = 'No relevant memories found'; emptyText.style.cssText = ` color: #9aa0a6; font-size: 14px; font-weight: 500; `; emptyContainer.appendChild(emptyIcon); emptyContainer.appendChild(emptyText); memoriesContent.appendChild(emptyContainer); } // Add content to modal contentSection.appendChild(memoriesCounter); contentSection.appendChild(memoriesContent); // Navigation section at bottom const navigationSection = document.createElement('div'); navigationSection.style.cssText = ` display: flex; justify-content: center; gap: 12px; padding: 10px; border-top: none; flex-shrink: 0; `; // Navigation buttons const prevButton = document.createElement('button'); prevButton.innerHTML = ` `; prevButton.style.cssText = ` background: #3c4043; border: none; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.2s; `; const nextButton = document.createElement('button'); nextButton.innerHTML = ` `; nextButton.style.cssText = prevButton.style.cssText; // Add navigation button handlers prevButton.addEventListener('click', () => { if (currentMemoryIndex >= memoriesPerPage) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); showMemories(); } }); nextButton.addEventListener('click', () => { const availableMemories = memoryItems.filter(memory => !allMemoriesById.has(String(memory.id))); if (currentMemoryIndex + memoriesPerPage < availableMemories.length) { currentMemoryIndex = currentMemoryIndex + memoriesPerPage; showMemories(); } }); // Add hover effects [prevButton, nextButton].forEach(button => { button.addEventListener('mouseenter', () => { if (!button.disabled) { button.style.backgroundColor = '#484b4f'; } }); button.addEventListener('mouseleave', () => { if (!button.disabled) { button.style.backgroundColor = '#3c4043'; } }); }); navigationSection.appendChild(prevButton); navigationSection.appendChild(nextButton); // Assemble modal headerLeft.appendChild(logoImg); headerLeft.appendChild(title); headerRight.appendChild(addToPromptBtn); headerRight.appendChild(settingsBtn); modalHeader.appendChild(headerLeft); modalHeader.appendChild(headerRight); modalContainer.appendChild(modalHeader); modalContainer.appendChild(contentSection); modalContainer.appendChild(navigationSection); modalOverlay.appendChild(modalContainer); // Append to body document.body.appendChild(modalOverlay); // Show initial memories showMemories(); // Update navigation button states function updateNavigationState(currentPage: number, totalPages: number): void { if (memoryItems.length === 0 || totalPages === 0) { prevButton.disabled = true; prevButton.style.opacity = '0.5'; prevButton.style.cursor = 'not-allowed'; nextButton.disabled = true; nextButton.style.opacity = '0.5'; nextButton.style.cursor = 'not-allowed'; return; } if (currentPage <= 1) { prevButton.disabled = true; prevButton.style.opacity = '0.5'; prevButton.style.cursor = 'not-allowed'; } else { prevButton.disabled = false; prevButton.style.opacity = '1'; prevButton.style.cursor = 'pointer'; } if (currentPage >= totalPages) { nextButton.disabled = true; nextButton.style.opacity = '0.5'; nextButton.style.cursor = 'not-allowed'; } else { nextButton.disabled = false; nextButton.style.opacity = '1'; nextButton.style.cursor = 'pointer'; } } // Update Add to Prompt button click handler addToPromptBtn.addEventListener('click', () => { // Only add memories that are not already added const newMemories = memoryItems .filter(memory => !allMemoriesById.has(String(memory.id))) .map(memory => { allMemoriesById.add(String(memory.id)); return String(memory.text || ''); }); sendExtensionEvent('memory_injection', { provider: 'gemini', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), injected_all: true, memory_count: newMemories.length, }); // Add all new memories to allMemories allMemories.push(...newMemories); // Update the input with all memories if (allMemories.length > 0) { updateInputWithMemories(); closeModal(); } else { // If no new memories were added but we have existing ones, just close if (allMemoriesById.size > 0) { closeModal(); } } }); // Function to close the modal function closeModal(): void { if (currentModalOverlay && document.body.contains(currentModalOverlay)) { document.body.removeChild(currentModalOverlay); } currentModalOverlay = null; memoryModalShown = false; } } // Handler for the modal approach async function handleMem0Modal(): Promise { // Check if there are actually memories in the current prompt const currentPrompt = getTextarea() ? (getTextarea() as HTMLElement).textContent || '' : ''; const hasMemoriesInPrompt = currentPrompt.includes(OPENMEMORY_PROMPTS.memory_marker_prefix); if (!hasMemoriesInPrompt) { // If there are no memories in the current prompt, clear the tracking allMemoriesById.clear(); allMemories = []; } const memoryEnabled = await getMemoryEnabledState(); if (!memoryEnabled) { return; } // Check if user is logged in const loginData: StorageData = await new Promise(resolve => { chrome.storage.sync.get( [StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN], function (items) { resolve(items); } ); }); // If no API key and no access token, show login popup if (!loginData.apiKey && !loginData.access_token) { showLoginPopup(); return; } const textarea = getTextarea(); let message = textarea ? ((textarea as HTMLElement).textContent || '').trim() : ''; // If no message, show a popup and return if (!message) { // Show message that requires input const mem0Button = (document.querySelector('#mem0-icon-button') as HTMLElement) || (document.querySelector('button[aria-label="Mem0"]') as HTMLElement); if (mem0Button) { showButtonPopup(mem0Button as HTMLElement, 'Please enter some text first'); } return; } // Clean the message of any existing memory content message = getContentWithoutMemories(); if (isProcessingMem0) { // Don't return, allow the modal to open anyway } isProcessingMem0 = true; // Add a timeout to reset the flag if something goes wrong const timeoutId = setTimeout(() => { isProcessingMem0 = false; }, 30000); // 30 second timeout // Show the loading modal immediately with the source button ID createMemoryModal([], true); try { const data: StorageData = await new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, StorageKey.SIMILARITY_THRESHOLD, StorageKey.TOP_K, ], function (items) { resolve(items); } ); }); const apiKey = data[StorageKey.API_KEY]; const userId = data[StorageKey.USER_ID_CAMEL] || data[StorageKey.USER_ID] || 'chrome-extension-user'; const accessToken = data[StorageKey.ACCESS_TOKEN]; // const threshold = data.similarity_threshold !== undefined ? data.similarity_threshold : 0.1; // const topK = data.top_k !== undefined ? data.top_k : 10; if (!apiKey && !accessToken) { isProcessingMem0 = false; return; } sendExtensionEvent('modal_clicked', { provider: 'gemini', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), }); const authHeader = accessToken ? `Bearer ${accessToken}` : `Token ${apiKey}`; const messages = [{ role: MessageRole.User, content: message }]; const optionalParams: OptionalApiParams = {}; if (data[StorageKey.SELECTED_ORG]) { optionalParams.org_id = data[StorageKey.SELECTED_ORG]; } if (data[StorageKey.SELECTED_PROJECT]) { optionalParams.project_id = data[StorageKey.SELECTED_PROJECT]; } (geminiSearch as { runImmediate: (message: string) => void }).runImmediate(message); // Proceed with adding memory asynchronously without awaiting fetch('https://api.mem0.ai/v1/memories/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify({ messages: messages, user_id: userId, infer: true, metadata: { provider: 'Gemini', }, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }), }).catch(() => { // Ignore errors }); } catch { // Still show the modal but with empty state if there was an error createMemoryModal([], false); } finally { clearTimeout(timeoutId); isProcessingMem0 = false; } } function initializeMem0Integration(): void { // **PERFORMANCE FIX: Prevent multiple initializations** if (isInitialized) { return; } try { setupInputObserver(); try { hookGeminiBackgroundSearchTyping(); } catch { // Ignore errors } injectMem0Button(); addSendButtonListener(); // **TIMING FIX: Start periodic element detection** startElementDetection(); // **PERFORMANCE FIX: Clean up existing main observer** if (mainObserver) { mainObserver.disconnect(); if (mainObserver.memoryStateInterval) { clearInterval(mainObserver.memoryStateInterval); } } // **PERFORMANCE FIX: Consolidated debounced observer with self-trigger prevention** let isObserverRunning = false; // Prevent self-triggering mainObserver = new MutationObserver(async mutations => { // **PERFORMANCE FIX: Prevent observer self-triggering** if (isObserverRunning) { return; } // **PERFORMANCE FIX: Filter out our own changes** const relevantMutations = mutations.filter(mutation => { // Skip mutations on our own elements const targetEl = mutation.target as Element; if ( targetEl && ((targetEl as ExtendedElement).id === 'mem0-notification-dot' || targetEl.classList?.contains('mem0-button-popover') || targetEl.classList?.contains('toolbox-drawer-item') || (targetEl as ExtendedElement).querySelector?.('[aria-label="Mem0"]')) ) { return false; } // Only care about significant structural changes if (mutation.type === 'childList') { // Only if nodes were added/removed, not just text changes return mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0; } return ( mutation.type === 'attributes' && (mutation.attributeName === 'class' || mutation.attributeName === 'id') ); }); if (relevantMutations.length === 0) { return; } // Clear existing debounce timer if (mainObserver) { clearTimeout(mainObserver.debounceTimer); } // Debounce the actual work if (mainObserver) { mainObserver.debounceTimer = window.setTimeout(async () => { if (isObserverRunning) { return; } isObserverRunning = true; try { // **PERFORMANCE FIX: Temporarily disconnect observer during DOM modifications** if (mainObserver) { mainObserver.disconnect(); } // Check memory state first const memoryEnabled = await getMemoryEnabledState(); // Only inject the button if memory is enabled if (memoryEnabled) { if (!document.querySelector('#mem0-icon-button')) { injectMem0Button(); } if (!sendListenerAdded) { addSendButtonListener(); } updateNotificationDot(); } else { const host = document.querySelector('#mem0-icon-button') as HTMLElement | null; if (host && host.parentElement) { host.parentElement.remove(); } } // **PERFORMANCE FIX: Reconnect observer after DOM modifications** setTimeout(() => { if (mainObserver) { mainObserver.observe(document.body, { childList: true, subtree: true, attributeFilter: ['class', 'id'], }); } }, 100); } catch { // Reconnect observer even if there was an error setTimeout(() => { if (mainObserver) { mainObserver.observe(document.body, { childList: true, subtree: true, attributeFilter: ['class', 'id'], }); } }, 100); } finally { isObserverRunning = false; } }, 500); } // Increased debounce to 500ms }); // Add keyboard shortcut for Ctrl+M document.addEventListener('keydown', function (event: KeyboardEvent) { if (event.ctrlKey && event.key === 'm') { event.preventDefault(); (async () => { await handleMem0Modal(); })(); } }); // **PERFORMANCE FIX: Observe with more specific targeting** mainObserver.observe(document.body, { childList: true, subtree: true, attributeFilter: ['class', 'id'], // Only observe relevant attribute changes }); // **PERFORMANCE FIX: Reduce polling frequency and add cleanup** const memoryStateCheckInterval = window.setInterval(async () => { try { const memoryEnabled = await getMemoryEnabledState(); if (!memoryEnabled) { const existingButton = document.querySelector('#mem0-icon-button'); if (existingButton && existingButton.parentElement) { existingButton.parentElement.remove(); } } else if (!document.querySelector('#mem0-icon-button')) { injectMem0Button(); } } catch { // Ignore errors } }, 15000); // Increased from 10s to 15s to reduce frequency further // Store reference for cleanup mainObserver.memoryStateInterval = memoryStateCheckInterval; // **PERFORMANCE FIX: Mark as initialized** isInitialized = true; } catch { // Ignore errors } } // **PERFORMANCE FIX: Add cleanup function** function cleanup(): void { if (mainObserver) { mainObserver.disconnect(); if (mainObserver.memoryStateInterval) { clearInterval(mainObserver.memoryStateInterval); } if (mainObserver.debounceTimer) { clearTimeout(mainObserver.debounceTimer); } } if (notificationObserver) { notificationObserver.disconnect(); } if (inputObserver) { inputObserver.disconnect(); } // **TIMING FIX: Clean up element detection interval** if (elementDetectionInterval) { clearInterval(elementDetectionInterval); } // Reset flags isInitialized = false; sendListenerAdded = false; setupRetryCount = 0; lastFoundTextarea = null; lastFoundSendButton = null; } // **PERFORMANCE FIX: Clean up on page unload** window.addEventListener('beforeunload', cleanup); // Initialize the integration when the page loads initializeMem0Integration(); ================================================ FILE: src/grok/content.ts ================================================ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { MessageRole } from '../types/api'; import type { MemoryItem, MemorySearchItem, OptionalApiParams } from '../types/memory'; import { SidebarAction } from '../types/messages'; import { type StorageItems, StorageKey } from '../types/storage'; import { createOrchestrator, type SearchStorage } from '../utils/background_search'; import { OPENMEMORY_PROMPTS } from '../utils/llm_prompts'; import { SITE_CONFIG } from '../utils/site_config'; import { getBrowser, sendExtensionEvent } from '../utils/util_functions'; import { OPENMEMORY_UI, type Placement } from '../utils/util_positioning'; export {}; let isProcessingMem0 = false; let memoryModalShown: boolean = false; // Global variable to store all memories let allMemories: string[] = []; // Track added memories by ID const allMemoriesById: Set = new Set(); // Reference to the modal overlay for updates let currentModalOverlay: HTMLDivElement | null = null; // Track modal position for drag functionality const modalPosition: { x: number | null; y: number | null } = { x: null, y: null }; const grokSearch = createOrchestrator({ fetch: async function (query: string, opts: { signal?: AbortSignal }) { const data = await new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, StorageKey.SIMILARITY_THRESHOLD, StorageKey.TOP_K, ], function (items) { resolve(items as SearchStorage); } ); }); const apiKey = data[StorageKey.API_KEY]; const accessToken = data[StorageKey.ACCESS_TOKEN]; if (!apiKey && !accessToken) { return []; } const authHeader = accessToken ? `Bearer ${accessToken}` : `Token ${apiKey}`; const userId = data[StorageKey.USER_ID_CAMEL] || data[StorageKey.USER_ID] || 'chrome-extension-user'; const threshold = data[StorageKey.SIMILARITY_THRESHOLD] !== undefined ? data[StorageKey.SIMILARITY_THRESHOLD] : 0.1; const topK = data[StorageKey.TOP_K] !== undefined ? data[StorageKey.TOP_K] : 10; const optionalParams: OptionalApiParams = {}; if (data[StorageKey.SELECTED_ORG]) { optionalParams.org_id = data[StorageKey.SELECTED_ORG]; } if (data[StorageKey.SELECTED_PROJECT]) { optionalParams.project_id = data[StorageKey.SELECTED_PROJECT]; } // Clean query by stripping any appended memory header/content (debounced path) const cleanQuery = (function () { try { const MEM0_PLAIN = OPENMEMORY_PROMPTS.memory_header_plain_regex; return String(query).replace(MEM0_PLAIN, '').trim(); } catch (_e) { return query; } })(); const payload = { query: cleanQuery, filters: { user_id: userId }, rerank: true, threshold: threshold, top_k: topK, filter_memories: false, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }; const res = await fetch('https://api.mem0.ai/v2/memories/search/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify(payload), signal: opts && opts.signal, }); if (!res.ok) { throw new Error(`API request failed with status ${res.status}`); } return await res.json(); }, // Don’t render on prefetch. When modal is open, update it. onSuccess: function (normQuery: string, responseData: MemorySearchItem[]) { if (!memoryModalShown) { return; } const memoryItems = ((responseData as MemorySearchItem[]) || []).map( (item: MemorySearchItem) => ({ id: String(item.id), text: item.memory, categories: item.categories || [], }) ); createMemoryModal(memoryItems, false); }, onError: function () { if (memoryModalShown) { createMemoryModal([], false); } }, minLength: 3, debounceMs: 75, cacheTTL: 60000, }); function getTextarea(): HTMLTextAreaElement | null { const selectors = [ 'textarea.w-full.px-2.\\@\\[480px\\]\\/input\\:px-3.bg-transparent.focus\\:outline-none.text-primary.align-bottom.min-h-14.pt-5.my-0.mb-5', 'textarea.w-full.px-2.\\@\\[480px\\]\\/input\\:px-3.pt-5.mb-5.bg-transparent.focus\\:outline-none.text-primary.align-bottom', 'textarea[dir="auto"][spellcheck="false"][placeholder="Ask anything"]', 'textarea[dir="auto"][spellcheck="false"][placeholder="Ask follow-up"]', 'textarea[dir="auto"][spellcheck="false"]', 'textarea[aria-label="Ask Grok anything"]', ]; for (const selector of selectors) { const textarea = document.querySelector(selector) as HTMLTextAreaElement | null; if (textarea) { return textarea; } } return null; } let grokBackgroundSearchHandler: (() => void) | null = null; function hookGrokBackgroundSearchTyping() { const textarea = getTextarea(); if (!textarea) { return; } if (textarea.dataset.grokBackgroundHooked) { return; } textarea.dataset.grokBackgroundHooked = 'true'; if (!grokBackgroundSearchHandler) { grokBackgroundSearchHandler = function () { let text = textarea.value || ''; (grokSearch as { setText: (text: string) => void }).setText(text); }; } textarea.addEventListener('input', grokBackgroundSearchHandler); textarea.addEventListener('keyup', grokBackgroundSearchHandler); } function setupInputObserver(): void { const textarea = getTextarea(); if (!textarea) { setTimeout(setupInputObserver, 500); return; } hookGrokBackgroundSearchTyping(); } function setInputValue(inputElement: HTMLTextAreaElement | null, value: string): void { if (inputElement) { inputElement.value = value; inputElement.dispatchEvent(new Event('input', { bubbles: true })); } } // Add a function to handle send button actions and clear memories after sending // function addSendButtonListener(): void { // const selectors = [ // 'button.group.flex.flex-col.justify-center.rounded-full[type="submit"]', // 'button.group.flex.flex-col.justify-center.rounded-full.focus\\:outline-none.focus-visible\\:outline-none[type="submit"]', // 'button[type="submit"]:not([aria-label="Submit attachment"])', // 'button[aria-label="Grok something"][role="button"]', // 'button[aria-label="Submit"][type="submit"]', // 'button[type="submit"].group.flex.flex-col.justify-center.rounded-full', // ]; // // Handle capturing and storing the current message // function captureAndStoreMemory(): void { // const textarea = getTextarea(); // if (!textarea) { // return; // } // const message = (textarea.value || '').trim(); // if (!message) { // return; // } // // Clean message from any existing memory content // const cleanMessage = getContentWithoutMemories(); // // Asynchronously store the memory // chrome.storage.sync.get( // [ // StorageKey.API_KEY, // StorageKey.USER_ID_CAMEL, // StorageKey.ACCESS_TOKEN, // StorageKey.MEMORY_ENABLED, // StorageKey.SELECTED_ORG, // StorageKey.SELECTED_PROJECT, // StorageKey.USER_ID, // ], // function (items) { // // Skip if memory is disabled or no credentials // if (items.memory_enabled === false || (!items.apiKey && !items.access_token)) { // return; // } // const authHeader = items.access_token // ? `Bearer ${items.access_token}` // : `Token ${items.apiKey}`; // const userId = items.userId || items.user_id || 'chrome-extension-user'; // const optionalParams: OptionalApiParams = {}; // if (items.selected_org) { // optionalParams.org_id = items.selected_org; // } // if (items.selected_project) { // optionalParams.project_id = items.selected_project; // } // try { // const textarea = getTextarea(); // const rawInput = textarea && textarea.value ? textarea.value.trim() : message; // grokSearch.runImmediate(rawInput || message); // } catch (_) { // grokSearch.runImmediate(message); // } // // Send memory to mem0 API asynchronously without waiting for response // const storagePayload = { // messages: [{ role: MessageRole.User, content: cleanMessage }], // user_id: userId, // infer: true, // metadata: { // provider: 'Grok', // }, // source: 'OPENMEMORY_CHROME_EXTENSION', // ...optionalParams, // }; // fetch('https://api.mem0.ai/v1/memories/', { // method: 'POST', // headers: { // 'Content-Type': 'application/json', // Authorization: authHeader, // }, // body: JSON.stringify(storagePayload), // }).catch(error => { // console.error('Error saving memory:', error); // }); // } // ); // // Clear all memories after sending // setTimeout(() => { // allMemories = []; // allMemoriesById.clear(); // }, 100); // } // // Find and add listeners to the send button // let sendButton = null; // for (const selector of selectors) { // sendButton = document.querySelector(selector); // if (sendButton && !sendButton.dataset.mem0Listener) { // sendButton.dataset.mem0Listener = 'true'; // sendButton.addEventListener('click', function () { // captureAndStoreMemory(); // }); // // Also handle textarea for Enter key press // const textarea = getTextarea(); // if (textarea && !textarea.dataset.mem0KeyListener) { // textarea.dataset.mem0KeyListener = 'true'; // textarea.addEventListener('keydown', function (event: KeyboardEvent) { // // Check if Enter was pressed without Shift (standard send behavior) // if (event.key === 'Enter' && !event.shiftKey) { // captureAndStoreMemory(); // } // }); // } // break; // } // } // } function initializeMem0Integration(): void { setupInputObserver(); hookGrokBackgroundSearchTyping(); // Set up mutation observer to reinject elements when DOM changes // Cache-first mount (before focus) try { if (!document.getElementById('mem0-icon-button') && OPENMEMORY_UI) { OPENMEMORY_UI.resolveCachedAnchor( { learnKey: location.host + ':' + location.pathname }, null, 24 * 60 * 60 * 1000 ).then(function (hit) { if (!hit || !hit.el) { return; } let hs = OPENMEMORY_UI.createShadowRootHost('mem0-root'); let host = hs.host, shadow = hs.shadow; host.id = 'mem0-icon-button'; const cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.grok ? SITE_CONFIG.grok : null; let placement = hit.placement || (cfg && cfg.placement) || { strategy: 'inline', where: 'beforeend', inlineAlign: 'end' }; OPENMEMORY_UI.applyPlacement({ container: host, anchor: hit.el, placement: placement }); let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal(); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } // Try to move immediately to the right of the "Auto" button try { let anchor = hit.el; const cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.grok ? SITE_CONFIG.grok : null; let autoBtn = Array.from(anchor.querySelectorAll('button,[role="button"]')).find( function (b: Element) { let txt = (b.textContent || '').trim(); return cfg && cfg.autoButtonTextPattern ? cfg.autoButtonTextPattern.test(txt) : /\bAuto\b/i.test(txt); } ); if (autoBtn) { let child: Element | null = autoBtn; while (child && child.parentElement !== anchor) { child = child.parentElement; } if (child && child.parentElement === anchor && anchor) { anchor.insertBefore(host, child.nextSibling); let gap = getComputedStyle(anchor).gap; if (!gap || gap === 'normal') { gap = '4px'; } host.style.marginLeft = gap; host.style.marginRight = '0'; host.style.display = 'inline-flex'; host.style.alignItems = 'center'; host.style.flexShrink = '0'; } } } catch { // Ignore errors during re-initialization } }); } } catch { // Ignore errors during re-initialization } // Focus-driven mount if (OPENMEMORY_UI && OPENMEMORY_UI.mountOnEditorFocus) { OPENMEMORY_UI.mountOnEditorFocus({ existingHostSelector: '#mem0-icon-button', editorSelector: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.grok && SITE_CONFIG.grok.editorSelector ? SITE_CONFIG.grok.editorSelector : 'textarea, [contenteditable="true"], input[type="text"]', deriveAnchor: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.grok && typeof SITE_CONFIG.grok.deriveAnchor === 'function' ? SITE_CONFIG.grok.deriveAnchor : function (editor: Element) { return editor.closest('form') || editor.parentElement || document.body; }, placement: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.grok && SITE_CONFIG.grok.placement ? SITE_CONFIG.grok.placement : { strategy: 'inline', where: 'beforeend', inlineAlign: 'end' }, render: function (shadow: ShadowRoot, host: HTMLElement, anchor: Element | null) { host.id = 'mem0-icon-button'; let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal(); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } // Move host to immediately after the "Auto" button if present and normalize spacing try { let autoBtn = anchor && Array.from(anchor.querySelectorAll('button,[role="button"]')).find(function ( b: Element ) { return /\bAuto\b/i.test((b.textContent || '').trim()); }); if (autoBtn) { let child: Element | null = autoBtn; while (child && child.parentElement !== anchor) { child = child.parentElement; } if (child && child.parentElement === anchor && anchor) { anchor.insertBefore(host, child.nextSibling); // Use container gap for spacing; don't add extra margin to avoid double spacing host.style.marginLeft = '0'; // Rely on container gap; no additional margin host.style.marginLeft = '0'; host.style.marginRight = '0'; host.style.display = 'inline-flex'; host.style.alignItems = 'center'; host.style.flexShrink = '0'; } } } catch { // Ignore errors during re-initialization } }, }); } document.addEventListener('keydown', function (event: KeyboardEvent) { if (event.ctrlKey && event.key === 'm') { event.preventDefault(); (async () => { await handleMem0Modal(); })(); } }); } // function injectMem0Button(): void { // // Function to periodically check and add the button if the parent element exists // async function tryAddButton() { // // First check if memory is enabled // const memoryEnabled = await getMemoryEnabledState(); // // Remove existing button if memory is disabled // if (!memoryEnabled) { // const existingContainer = document.querySelector('#mem0-button-container'); // if (existingContainer) { // existingContainer.remove(); // } // // Check again after some time in case the state changes // setTimeout(tryAddButton, 5000); // return; // } // // Check if our button already exists // if ( // document.querySelector('button[aria-label="OpenMemory"]') || // document.querySelector('#mem0-button-container') // ) { // return; // } // // Look specifically for the Auto button to position next to it // let referenceButton = null; // const textarea = getTextarea(); // if (textarea) { // // Find the Auto button by looking in the immediate parent container of the textarea // // This is more specific and should avoid finding multiple buttons // let container = textarea.parentElement; // while (container && !referenceButton) { // const buttons = container.querySelectorAll('button'); // for (let i = 0; i < buttons.length; i++) { // const btn = buttons[i]!; // // Check if this button contains "Auto" text and is visible // if (btn.textContent && btn.textContent.trim() === 'Auto' && btn.offsetParent !== null) { // referenceButton = btn; // break; // } // } // // Move to parent if we haven't found the Auto button yet // container = container.parentElement; // // Don't go too far up the DOM tree // if (container === document.body) { // break; // } // } // } // if (!referenceButton) { // // If we can't find the Auto button, wait and try again // setTimeout(tryAddButton, 1000); // return; // } // const parentDiv = referenceButton.parentElement; // if (!parentDiv) { // setTimeout(tryAddButton, 1000); // return; // } // // Create mem0 button container // const mem0ButtonContainer = document.createElement('div'); // mem0ButtonContainer.id = 'mem0-button-container'; // mem0ButtonContainer.style.position = 'relative'; // For positioning popover // mem0ButtonContainer.style.marginLeft = '4px'; // Smaller margin to be closer to Auto button // mem0ButtonContainer.style.display = 'flex'; // mem0ButtonContainer.style.alignItems = 'center'; // Ensure vertical alignment // // Create mem0 button // const mem0Button = document.createElement('button'); // mem0Button.className = referenceButton.className; // mem0Button.setAttribute('type', 'button'); // mem0Button.setAttribute('tabindex', '0'); // mem0Button.setAttribute('aria-pressed', 'false'); // mem0Button.setAttribute('aria-label', 'OpenMemory'); // mem0Button.setAttribute('data-state', 'closed'); // mem0Button.id = 'mem0-icon-button'; // // Add additional styling to match the Auto button better // mem0Button.style.minWidth = 'auto'; // mem0Button.style.padding = '0'; // mem0Button.style.width = '32px'; // mem0Button.style.height = '32px'; // mem0Button.style.display = 'flex'; // mem0Button.style.alignItems = 'center'; // mem0Button.style.justifyContent = 'center'; // mem0Button.style.flexShrink = '0'; // Prevent shrinking // mem0Button.style.margin = '0'; // Reset any inherited margins // // Create notification dot // const notificationDot = document.createElement('div'); // notificationDot.id = 'mem0-notification-dot'; // notificationDot.style.cssText = ` // position: absolute; // top: -2px; // right: -2px; // width: 8px; // height: 8px; // background-color:rgb(128, 221, 162); // border-radius: 50%; // border: 1px solid #1C1C1E; // display: none; // z-index: 1001; // pointer-events: none; // `; // // Add keyframe animation for the dot // if (!document.getElementById('notification-dot-animation')) { // const style = document.createElement('style'); // style.id = 'notification-dot-animation'; // style.innerHTML = ` // @keyframes popIn { // 0% { transform: scale(0); } // 50% { transform: scale(1.2); } // 100% { transform: scale(1); } // } // #mem0-notification-dot.active { // display: block !important; // animation: popIn 0.3s ease-out forwards; // } // `; // document.head.appendChild(style); // } // // Create button content - icon only, similar to Claude style // mem0Button.innerHTML = ` // // `; // // Create popover element (hidden by default) // const popover = document.createElement('div'); // popover.className = 'mem0-button-popover'; // popover.style.cssText = ` // position: absolute; // bottom: 48px; // left: 50%; // transform: translateX(-50%); // background-color: #1C1C1E; // border: 1px solid #27272A; // color: white; // padding: 8px 12px; // border-radius: 6px; // font-size: 12px; // white-space: nowrap; // z-index: 10001; // box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); // display: none; // transition: opacity 0.2s; // `; // popover.textContent = 'Add memories to your prompt'; // // Add arrow // const arrow = document.createElement('div'); // arrow.style.cssText = ` // position: absolute; // top: 100%; // left: 50%; // transform: translateX(-50%) rotate(45deg); // width: 10px; // height: 10px; // background-color: #1C1C1E; // border-right: 1px solid #27272A; // border-bottom: 1px solid #27272A; // `; // popover.appendChild(arrow); // // Add hover event for popover // mem0ButtonContainer.addEventListener('mouseenter', () => { // popover.style.display = 'block'; // setTimeout(() => (popover.style.opacity = '1'), 10); // }); // mem0ButtonContainer.addEventListener('mouseleave', () => { // popover.style.opacity = '0'; // setTimeout(() => (popover.style.display = 'none'), 200); // }); // // Add click event to the mem0 button to show memory modal // mem0Button.addEventListener('click', function () { // // Check if the memories are enabled // getMemoryEnabledState().then(memoryEnabled => { // if (memoryEnabled) { // handleMem0Modal(); // } else { // // If memories are disabled, open options // chrome.runtime.sendMessage({ action: SidebarAction.OPEN_OPTIONS }); // } // }); // }); // // Assemble button components // mem0ButtonContainer.appendChild(mem0Button); // mem0ButtonContainer.appendChild(notificationDot); // mem0ButtonContainer.appendChild(popover); // // Insert after the Auto button (or reference button if Auto not found) // parentDiv.insertBefore(mem0ButtonContainer, referenceButton.nextSibling); // // Update notification dot based on input content // updateNotificationDot(); // // Ensure notification dot is updated after DOM is fully loaded // setTimeout(updateNotificationDot, 500); // } // // Start trying to add the button // tryAddButton(); // // Also observe DOM changes to add button when needed // const observer = new MutationObserver(() => { // if (!document.querySelector('#mem0-button-container')) { // tryAddButton(); // } // // Also update notification dot when DOM changes // updateNotificationDot(); // hookGrokBackgroundSearchTyping(); // }); // observer.observe(document.body, { // childList: true, // subtree: true, // }); // } // Function to update notification dot visibility based on text in the input function updateNotificationDot(): void { const textarea = getTextarea(); const host = document.getElementById('mem0-icon-button'); if (textarea && host) { // Function to check if input has text const checkForText = () => { const inputText = textarea.value || ''; host.setAttribute('data-has-text', inputText.trim() ? '1' : '0'); }; // Observe and listen for changes const mo = new MutationObserver(checkForText); mo.observe(textarea, { characterData: true, subtree: true }); if (!textarea.dataset.grokCheckTextHooked) { textarea.dataset.grokCheckTextHooked = 'true'; textarea.addEventListener('input', checkForText); textarea.addEventListener('keyup', checkForText); textarea.addEventListener('focus', checkForText); } checkForText(); setTimeout(checkForText, 500); } else { setTimeout(updateNotificationDot, 1000); } } function createMemoryModal(memoryItems: MemoryItem[], isLoading: boolean = false) { // Close existing modal if it exists if (memoryModalShown && currentModalOverlay) { document.body.removeChild(currentModalOverlay); } memoryModalShown = true; let currentMemoryIndex = 0; // Calculate modal dimensions (estimated) const modalWidth = 447; let modalHeight = 400; // Default height let memoriesPerPage = 3; // Default number of memories per page let topPosition; let leftPosition; // Use stored position if available, otherwise calculate based on button position if (modalPosition.x !== null && modalPosition.y !== null) { leftPosition = modalPosition.x; topPosition = modalPosition.y; } else { // Position relative to the OpenMemory button const mem0Button = document.getElementById('mem0-icon-button') || document.querySelector('button[aria-label="OpenMemory"]'); if (mem0Button) { const buttonRect = mem0Button.getBoundingClientRect(); // Determine if there's enough space below the button const viewportHeight = window.innerHeight; const spaceBelow = viewportHeight - buttonRect.bottom; // Position the modal centered under the button leftPosition = Math.max(buttonRect.left + buttonRect.width / 2 - modalWidth / 2, 10); // Ensure the modal doesn't go off the right edge of the screen const rightEdgePosition = leftPosition + modalWidth; if (rightEdgePosition > window.innerWidth - 10) { leftPosition = window.innerWidth - modalWidth - 10; } if (spaceBelow >= modalHeight) { // Place below the button topPosition = buttonRect.bottom + 10; } else { // Place above the button if not enough space below topPosition = buttonRect.top - modalHeight - 10; // Check if it's in the upper half of the screen if (buttonRect.top < viewportHeight / 2) { modalHeight = 300; // Reduced height memoriesPerPage = 2; // Show only 2 memories } } } else { // Fallback positioning topPosition = 100; leftPosition = window.innerWidth / 2 - modalWidth / 2; } // Store the initial position modalPosition.x = leftPosition; modalPosition.y = topPosition; } // Create modal overlay const modalOverlay = document.createElement('div'); modalOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: transparent; display: flex; z-index: 10000; pointer-events: auto; `; // Save reference to current modal overlay currentModalOverlay = modalOverlay; // Add event listener to close modal when clicking outside modalOverlay.addEventListener('click', event => { // Only close if clicking directly on the overlay, not its children if (event.target === modalOverlay) { closeModal(); } }); // Cache-first mount for Grok (aim to sit left of the send/auto area) try { if (!document.getElementById('mem0-icon-button') && OPENMEMORY_UI) { OPENMEMORY_UI.resolveCachedAnchor( { learnKey: location.host + ':' + location.pathname }, null, 24 * 60 * 60 * 1000 ).then(function (hit: { el: Element; placement: Placement | null } | null) { if (!hit || !hit.el) { return; } let hs = OPENMEMORY_UI.createShadowRootHost('mem0-root'); let host = hs.host, shadow = hs.shadow; host.id = 'mem0-icon-button'; let placement = hit.placement || { strategy: 'inline', where: 'beforeend', inlineAlign: 'end', }; OPENMEMORY_UI.applyPlacement({ container: host, anchor: hit.el, placement: placement }); let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal(); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } }); } } catch (_) { // Ignore errors during re-initialization } // Focus-driven mount for Grok if (OPENMEMORY_UI && OPENMEMORY_UI.mountOnEditorFocus) { OPENMEMORY_UI.mountOnEditorFocus({ existingHostSelector: '#mem0-icon-button', editorSelector: 'textarea, [contenteditable="true"], input[type="text"]', deriveAnchor: function (editor: Element) { return editor.closest('form') || editor.parentElement; }, placement: { strategy: 'inline', where: 'beforeend', inlineAlign: 'end' }, render: function (shadow: ShadowRoot, host: HTMLElement) { host.id = 'mem0-icon-button'; let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal(); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } }, }); } // Create modal container with positioning const modalContainer = document.createElement('div'); modalContainer.style.cssText = ` background-color: #1C1C1E; border-radius: 12px; width: ${modalWidth}px; height: ${modalHeight}px; display: flex; flex-direction: column; color: white; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); position: absolute; top: ${topPosition}px; left: ${leftPosition}px; pointer-events: auto; border: 1px solid #27272A; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; overflow: hidden; `; // Add drag functionality let isDraggingModal: boolean = false; const modalDragOffset: { x: number; y: number } = { x: 0, y: 0 }; function handleDragStart(e: MouseEvent) { // Don't start dragging if clicking on buttons or interactive elements const target = e.target as HTMLElement | null; if (target && (target.tagName === 'BUTTON' || target.closest('button'))) { return; } isDraggingModal = true; const rect = modalContainer.getBoundingClientRect(); modalDragOffset.x = e.clientX - rect.left; modalDragOffset.y = e.clientY - rect.top; document.addEventListener('mousemove', handleDragMove); document.addEventListener('mouseup', handleDragEnd); modalContainer.style.transition = 'none'; } function handleDragMove(e: MouseEvent) { if (!isDraggingModal) { return; } e.preventDefault(); const newX = e.clientX - modalDragOffset.x; const newY = e.clientY - modalDragOffset.y; // Constrain to viewport const maxX = window.innerWidth - modalWidth; const maxY = window.innerHeight - modalHeight; const constrainedX = Math.max(0, Math.min(newX, maxX)); const constrainedY = Math.max(0, Math.min(newY, maxY)); modalContainer.style.left = constrainedX + 'px'; modalContainer.style.top = constrainedY + 'px'; // Update stored position modalPosition.x = constrainedX; modalPosition.y = constrainedY; } function handleDragEnd() { isDraggingModal = false; document.removeEventListener('mousemove', handleDragMove); document.removeEventListener('mouseup', handleDragEnd); modalContainer.style.transition = ''; } // Create modal header const modalHeader = document.createElement('div'); modalHeader.style.cssText = ` display: flex; align-items: center; padding: 10px 16px; justify-content: space-between; background-color: #232325; flex-shrink: 0; cursor: move; user-select: none; `; // Create header left section with just the logo const headerLeft = document.createElement('div'); headerLeft.style.cssText = ` display: flex; flex-direction: row; align-items: center; `; // Add Mem0 logo (updated to SVG) const logoImg = document.createElement('img'); logoImg.src = chrome.runtime.getURL('icons/mem0-claude-icon.png'); logoImg.style.cssText = ` width: 26px; height: 26px; border-radius: 50%; `; // Add "OpenMemory" title const title = document.createElement('div'); title.textContent = 'OpenMemory'; title.style.cssText = ` font-size: 16px; font-weight: 600; color: white; margin-left: 8px; `; // Create header right section const headerRight = document.createElement('div'); headerRight.style.cssText = ` display: flex; flex-direction: row; align-items: center; gap: 8px; `; // Create Add to Prompt button with arrow const addToPromptBtn = document.createElement('button'); addToPromptBtn.style.cssText = ` display: flex; flex-direction: row; align-items: center; padding: 5px 16px; gap: 8px; background-color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 12px; font-weight: 600; color: black; `; addToPromptBtn.textContent = 'Add to Prompt'; // Add arrow icon to button const arrowIcon = document.createElement('span'); arrowIcon.innerHTML = ` `; addToPromptBtn.appendChild(arrowIcon); // Create settings button const settingsBtn = document.createElement('button'); settingsBtn.style.cssText = ` background: none; border: none; cursor: pointer; padding: 8px; opacity: 0.6; transition: opacity 0.2s; `; settingsBtn.innerHTML = ` `; // Add click event to open app.mem0.ai in a new tab settingsBtn.addEventListener('click', () => { if (currentModalOverlay && document.body.contains(currentModalOverlay)) { document.body.removeChild(currentModalOverlay); memoryModalShown = false; currentModalOverlay = null; } chrome.runtime.sendMessage({ action: SidebarAction.SIDEBAR_SETTINGS }); }); // Add hover effect for the settings button settingsBtn.addEventListener('mouseenter', () => { settingsBtn.style.opacity = '1'; }); settingsBtn.addEventListener('mouseleave', () => { settingsBtn.style.opacity = '0.6'; }); // Add drag event listener to header modalHeader.addEventListener('mousedown', handleDragStart); // Content section const contentSection = document.createElement('div'); const contentSectionHeight = modalHeight - 130; // Account for header and navigation contentSection.style.cssText = ` display: flex; flex-direction: column; padding: 0 16px; gap: 12px; overflow: hidden; flex: 1; height: ${contentSectionHeight}px; `; // Create memories counter const memoriesCounter = document.createElement('div'); memoriesCounter.style.cssText = ` font-size: 16px; font-weight: 600; color: #FFFFFF; margin-top: 16px; flex-shrink: 0; `; // Update counter text based on loading state and number of memories if (isLoading) { memoriesCounter.textContent = `Loading Relevant Memories...`; } else { memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`; } // Calculate max height for memories content based on modal height const memoriesContentMaxHeight = contentSectionHeight - 40; // Account for memories counter // Create memories content container with adjusted height const memoriesContent = document.createElement('div'); memoriesContent.style.cssText = ` display: flex; flex-direction: column; gap: 8px; overflow-y: auto; flex: 1; max-height: ${memoriesContentMaxHeight}px; padding-right: 8px; margin-right: -8px; scrollbar-width: none; -ms-overflow-style: none; `; memoriesContent.style.cssText += '::-webkit-scrollbar { display: none; }'; // Track currently expanded memory let currentlyExpandedMemory: HTMLElement | null = null; // Function to create skeleton loading items (adjusted for different heights) function createSkeletonItems() { memoriesContent.innerHTML = ''; for (let i = 0; i < memoriesPerPage; i++) { const skeletonItem = document.createElement('div'); skeletonItem.style.cssText = ` display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; padding: 12px; background-color: #27272A; border-radius: 8px; height: 72px; flex-shrink: 0; animation: pulse 1.5s infinite ease-in-out; `; const skeletonText = document.createElement('div'); skeletonText.style.cssText = ` background-color: #383838; border-radius: 4px; height: 14px; width: 85%; margin-bottom: 8px; `; const skeletonText2 = document.createElement('div'); skeletonText2.style.cssText = ` background-color: #383838; border-radius: 4px; height: 14px; width: 65%; `; const skeletonActions = document.createElement('div'); skeletonActions.style.cssText = ` display: flex; gap: 4px; margin-left: 10px; `; const skeletonButton1 = document.createElement('div'); skeletonButton1.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; background-color: #383838; `; const skeletonButton2 = document.createElement('div'); skeletonButton2.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; background-color: #383838; `; skeletonActions.appendChild(skeletonButton1); skeletonActions.appendChild(skeletonButton2); const textContainer = document.createElement('div'); textContainer.style.cssText = ` display: flex; flex-direction: column; flex-grow: 1; `; textContainer.appendChild(skeletonText); textContainer.appendChild(skeletonText2); skeletonItem.appendChild(textContainer); skeletonItem.appendChild(skeletonActions); memoriesContent.appendChild(skeletonItem); } // Add keyframe animation to document if not exists if (!document.getElementById('skeleton-animation')) { const style = document.createElement('style'); style.id = 'skeleton-animation'; style.innerHTML = ` @keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 0.8; } 100% { opacity: 0.6; } } `; document.head.appendChild(style); } } // Function to show memories with adjusted count based on modal position function showMemories() { memoriesContent.innerHTML = ''; if (isLoading) { createSkeletonItems(); return; } if (memoryItems.length === 0) { showEmptyState(); updateNavigationState(0, 0); return; } // Use the dynamically set memoriesPerPage value const memoriesToShow = Math.min(memoriesPerPage, memoryItems.length); // Calculate total pages and current page const totalPages = Math.ceil(memoryItems.length / memoriesToShow); const currentPage = Math.floor(currentMemoryIndex / memoriesToShow) + 1; // Update navigation buttons state updateNavigationState(currentPage, totalPages); for (let i = 0; i < memoriesToShow; i++) { const memoryIndex = currentMemoryIndex + i; if (memoryIndex >= memoryItems.length) { break; } // Stop if we've reached the end const memory = memoryItems[memoryIndex]!; // Skip memories that have been added already if (allMemoriesById.has(String(memory.id))) { continue; } // Ensure memory has an ID if (!memory.id) { memory.id = `memory-${Date.now()}-${memoryIndex}`; } const memoryContainer = document.createElement('div'); memoryContainer.style.cssText = ` display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; padding: 12px; background-color: #27272A; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; min-height: 72px; max-height: 72px; overflow: hidden; flex-shrink: 0; `; const memoryText = document.createElement('div'); memoryText.style.cssText = ` font-size: 14px; line-height: 1.5; color: #D4D4D8; flex-grow: 1; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; transition: all 0.2s ease; height: 42px; /* Height for 2 lines of text */ `; memoryText.textContent = memory.text || ''; const actionsContainer = document.createElement('div'); actionsContainer.style.cssText = ` display: flex; gap: 4px; margin-left: 10px; flex-shrink: 0; `; // Add button const addButton = document.createElement('button'); addButton.style.cssText = ` border: none; cursor: pointer; padding: 4px; background:rgb(66, 66, 69); color:rgb(199, 199, 201); border-radius: 100%; transition: all 0.2s ease; `; addButton.innerHTML = ` `; // Add click handler for add button addButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); sendExtensionEvent('memory_injection', { provider: 'grok', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), injected_all: false, memory_id: memory.id, }); // Add this memory allMemoriesById.add(String(memory.id)); allMemories.push(String(memory.text || '')); updateInputWithMemories(); // Remove this memory from the list const index = memoryItems.findIndex((m: MemoryItem) => m.id === memory.id); if (index !== -1) { memoryItems.splice(index, 1); // Recalculate pagination after removing an item // If we're on a page that's now empty, go to previous page if (currentMemoryIndex > 0 && currentMemoryIndex >= memoryItems.length) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); } memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`; showMemories(); } }); // Menu button const menuButton = document.createElement('button'); menuButton.style.cssText = ` background: none; border: none; cursor: pointer; padding: 4px; color: #A1A1AA; `; menuButton.innerHTML = ` `; // Track expanded state let isExpanded = false; // Create remove button (hidden by default) const removeButton = document.createElement('button'); removeButton.style.cssText = ` display: none; align-items: center; gap: 6px; background:rgb(66, 66, 69); color:rgb(199, 199, 201); border-radius: 8px; padding: 2px 4px; border: none; cursor: pointer; font-size: 13px; margin-top: 12px; width: fit-content; `; removeButton.innerHTML = ` Remove `; // Create content wrapper for text and remove button const contentWrapper = document.createElement('div'); contentWrapper.style.cssText = ` display: flex; flex-direction: column; flex-grow: 1; `; contentWrapper.appendChild(memoryText); contentWrapper.appendChild(removeButton); // Function to expand memory const expandMemory = () => { if (currentlyExpandedMemory && currentlyExpandedMemory !== memoryContainer) { currentlyExpandedMemory.dispatchEvent(new Event('collapse')); } isExpanded = true; memoryText.style.webkitLineClamp = 'unset'; memoryText.style.height = 'auto'; contentWrapper.style.overflowY = 'auto'; contentWrapper.style.maxHeight = '240px'; // Limit height to prevent overflow contentWrapper.style.scrollbarWidth = 'none'; contentWrapper.style.msOverflowStyle = 'none'; contentWrapper.style.cssText += '::-webkit-scrollbar { display: none; }'; memoryContainer.style.backgroundColor = '#1C1C1E'; memoryContainer.style.maxHeight = '300px'; // Allow expansion but within container memoryContainer.style.overflow = 'hidden'; removeButton.style.display = 'flex'; currentlyExpandedMemory = memoryContainer; // Scroll to make expanded memory visible if needed memoriesContent.scrollTop = memoryContainer.offsetTop - memoriesContent.offsetTop; }; // Function to collapse memory const collapseMemory = () => { isExpanded = false; memoryText.style.webkitLineClamp = '2'; memoryText.style.height = '42px'; contentWrapper.style.overflowY = 'visible'; memoryContainer.style.backgroundColor = '#27272A'; memoryContainer.style.maxHeight = '72px'; memoryContainer.style.overflow = 'hidden'; removeButton.style.display = 'none'; currentlyExpandedMemory = null; }; memoryContainer.addEventListener('collapse', collapseMemory); menuButton.addEventListener('click', e => { e.stopPropagation(); if (isExpanded) { collapseMemory(); } else { expandMemory(); } }); // Add click handler for remove button removeButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); // Remove from memoryItems const index = memoryItems.findIndex((m: MemoryItem) => m.id === memory.id); if (index !== -1) { memoryItems.splice(index, 1); // Recalculate pagination after removing an item // If we're on the last page and it's now empty, go to previous page if (currentMemoryIndex > 0 && currentMemoryIndex >= memoryItems.length) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); } memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`; showMemories(); } }); actionsContainer.appendChild(addButton); actionsContainer.appendChild(menuButton); memoryContainer.appendChild(contentWrapper); memoryContainer.appendChild(actionsContainer); memoriesContent.appendChild(memoryContainer); // Add hover effect memoryContainer.addEventListener('mouseenter', () => { memoryContainer.style.backgroundColor = isExpanded ? '#18181B' : '#323232'; }); memoryContainer.addEventListener('mouseleave', () => { memoryContainer.style.backgroundColor = isExpanded ? '#1C1C1E' : '#27272A'; }); } // If after filtering for already added memories, there are no items to show, // check if we need to go to previous page if (memoriesContent.children.length === 0 && memoryItems.length > 0) { if (currentMemoryIndex > 0) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); showMemories(); } else { showEmptyState(); } } } // Function to show empty state function showEmptyState() { memoriesContent.innerHTML = ''; const emptyContainer = document.createElement('div'); emptyContainer.style.cssText = ` display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 32px 16px; text-align: center; flex: 1; min-height: 200px; `; const emptyIcon = document.createElement('div'); emptyIcon.innerHTML = ` `; emptyIcon.style.marginBottom = '16px'; const emptyText = document.createElement('div'); emptyText.textContent = 'No relevant memories found'; emptyText.style.cssText = ` color: #71717A; font-size: 14px; font-weight: 500; `; emptyContainer.appendChild(emptyIcon); emptyContainer.appendChild(emptyText); memoriesContent.appendChild(emptyContainer); } // Add content to modal contentSection.appendChild(memoriesCounter); contentSection.appendChild(memoriesContent); // Navigation section at bottom const navigationSection = document.createElement('div'); navigationSection.style.cssText = ` display: flex; justify-content: center; gap: 12px; padding: 10px; border-top: none; flex-shrink: 0; `; // Navigation buttons const prevButton = document.createElement('button'); prevButton.innerHTML = ` `; prevButton.style.cssText = ` background: #27272A; border: none; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.2s; `; const nextButton = document.createElement('button'); nextButton.innerHTML = ` `; nextButton.style.cssText = prevButton.style.cssText; // Add navigation button handlers prevButton.addEventListener('click', () => { if (currentMemoryIndex >= memoriesPerPage) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); showMemories(); } }); nextButton.addEventListener('click', () => { if (currentMemoryIndex + memoriesPerPage < memoryItems.length) { currentMemoryIndex = currentMemoryIndex + memoriesPerPage; showMemories(); } }); // Add hover effects [prevButton, nextButton].forEach(button => { button.addEventListener('mouseenter', () => { if (!button.disabled) { button.style.backgroundColor = '#323232'; } }); button.addEventListener('mouseleave', () => { if (!button.disabled) { button.style.backgroundColor = '#27272A'; } }); }); navigationSection.appendChild(prevButton); navigationSection.appendChild(nextButton); // Assemble modal headerLeft.appendChild(logoImg); headerLeft.appendChild(title); headerRight.appendChild(addToPromptBtn); headerRight.appendChild(settingsBtn); modalHeader.appendChild(headerLeft); modalHeader.appendChild(headerRight); modalContainer.appendChild(modalHeader); modalContainer.appendChild(contentSection); modalContainer.appendChild(navigationSection); modalOverlay.appendChild(modalContainer); // Append to body document.body.appendChild(modalOverlay); // Show initial memories showMemories(); // Update navigation button states function updateNavigationState(currentPage: number, totalPages: number) { if (memoryItems.length === 0 || totalPages === 0) { prevButton.disabled = true; prevButton.style.opacity = '0.5'; prevButton.style.cursor = 'not-allowed'; nextButton.disabled = true; nextButton.style.opacity = '0.5'; nextButton.style.cursor = 'not-allowed'; return; } if (currentPage <= 1) { prevButton.disabled = true; prevButton.style.opacity = '0.5'; prevButton.style.cursor = 'not-allowed'; } else { prevButton.disabled = false; prevButton.style.opacity = '1'; prevButton.style.cursor = 'pointer'; } if (currentPage >= totalPages) { nextButton.disabled = true; nextButton.style.opacity = '0.5'; nextButton.style.cursor = 'not-allowed'; } else { nextButton.disabled = false; nextButton.style.opacity = '1'; nextButton.style.cursor = 'pointer'; } } // Update Add to Prompt button click handler addToPromptBtn.addEventListener('click', () => { // Only add memories that are not already added const newMemories = memoryItems .filter(memory => !allMemoriesById.has(String(memory.id)) && !memory.removed) .map(memory => { allMemoriesById.add(String(memory.id)); return String(memory.text || ''); }); sendExtensionEvent('memory_injection', { provider: 'grok', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), injected_all: true, memory_count: newMemories.length, }); // Add all new memories to allMemories allMemories.push(...newMemories); // Update the input with all memories if (allMemories.length > 0) { updateInputWithMemories(); closeModal(); } else { // If no new memories were added but we have existing ones, just close if (allMemoriesById.size > 0) { closeModal(); } } // Remove all added memories from the memoryItems list for (let i = memoryItems.length - 1; i >= 0; i--) { if (allMemoriesById.has(String(memoryItems[i]?.id))) { memoryItems.splice(i, 1); } } }); // Function to close the modal function closeModal() { if (currentModalOverlay && document.body.contains(currentModalOverlay)) { document.body.removeChild(currentModalOverlay); } currentModalOverlay = null; memoryModalShown = false; // Reset modal position when closing completely modalPosition.x = null; modalPosition.y = null; } } // Shared function to update the input field with all collected memories function updateInputWithMemories() { const inputElement = getTextarea(); if (inputElement && allMemories.length > 0) { // Get the content without any existing memory wrappers const baseContent = getContentWithoutMemories(); // Create the memory string with all collected memories let memoriesContent = '\n\n' + OPENMEMORY_PROMPTS.memory_header_text + '\n'; // Add all memories to the content allMemories.forEach((mem, index) => { memoriesContent += `- ${mem}`; if (index < allMemories.length - 1) { memoriesContent += '\n'; } }); // Add the final content to the input setInputValue(inputElement, baseContent + memoriesContent); } } // Function to get the content without any memory wrappers function getContentWithoutMemories(): string { const inputElement = getTextarea(); if (!inputElement) { return ''; } let content: string = inputElement.value || ''; // Remove any memory headers and content const memoryPrefix = OPENMEMORY_PROMPTS.memory_header_text; const prefixIndex = content.indexOf(memoryPrefix); if (prefixIndex !== -1) { content = content.substring(0, prefixIndex).trim(); } // Also try with regex pattern try { const MEM0_PLAIN = OPENMEMORY_PROMPTS.memory_header_plain_regex; content = content.replace(MEM0_PLAIN, '').trim(); } catch { // Ignore regex errors } return content; } // Function to check if memory is enabled function getMemoryEnabledState(): Promise { return new Promise(resolve => { chrome.storage.sync.get([StorageKey.MEMORY_ENABLED], function (result) { resolve(result.memory_enabled !== false); // Default to true if not set }); }); } // Handler for the modal approach async function handleMem0Modal() { const memoryEnabled = await getMemoryEnabledState(); if (!memoryEnabled) { return; } // Check if user is logged in const loginData = await new Promise(resolve => { chrome.storage.sync.get( [StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN], function (items) { resolve(items as StorageItems); } ); }); // If no API key and no access token, show login popup if (!loginData.apiKey && !loginData.access_token) { showLoginPopup(); return; } const textarea = getTextarea(); let message = textarea ? (textarea.value || '').trim() : ''; // If no message, show a popup and return if (!message) { // Show message that requires input const mem0Button = document.querySelector( 'button[aria-label="OpenMemory"]' ) as HTMLElement | null; if (mem0Button) { showButtonPopup(mem0Button, 'Please enter some text first'); } return; } // Clean the message of any existing memory content message = getContentWithoutMemories(); if (isProcessingMem0) { return; } isProcessingMem0 = true; // Show the loading modal immediately with the source button ID createMemoryModal([], true); try { const data = await new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, StorageKey.SIMILARITY_THRESHOLD, StorageKey.TOP_K, ], function (items) { resolve(items as StorageItems); } ); }); const apiKey = data[StorageKey.API_KEY]; const userId = (data[StorageKey.USER_ID_CAMEL] || data[StorageKey.USER_ID] || 'chrome-extension-user') as string; const accessToken = data[StorageKey.ACCESS_TOKEN]; if (!apiKey && !accessToken) { isProcessingMem0 = false; return; } sendExtensionEvent('modal_clicked', { provider: 'grok', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), }); const authHeader = accessToken ? `Bearer ${accessToken}` : `Token ${apiKey}`; const messages = [{ role: MessageRole.User, content: message }]; const optionalParams: OptionalApiParams = {}; if (data[StorageKey.SELECTED_ORG]) { optionalParams.org_id = data[StorageKey.SELECTED_ORG]; } if (data[StorageKey.SELECTED_PROJECT]) { optionalParams.project_id = data[StorageKey.SELECTED_PROJECT]; } try { const textarea = getTextarea(); const rawInput2 = textarea && textarea.value ? textarea.value.trim() : message; grokSearch.runImmediate(rawInput2 || message); } catch (_) { grokSearch.runImmediate(message); } // Proceed with adding memory asynchronously without awaiting fetch('https://api.mem0.ai/v1/memories/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify({ messages: messages, user_id: userId, infer: true, metadata: { provider: 'Grok', }, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }), }).catch(error => { console.error('Error adding memory:', error); }); } catch (error) { console.error('Error:', error); // Still show the modal but with empty state if there was an error createMemoryModal([], false); } finally { isProcessingMem0 = false; } } // Function to show a small popup message near the button function showButtonPopup(button: HTMLElement, message: string): void { // Remove any existing popups const existingPopup = document.querySelector('.mem0-button-popup'); if (existingPopup) { existingPopup.remove(); } const popup = document.createElement('div'); popup.className = 'mem0-button-popup'; popup.style.cssText = ` position: absolute; top: -40px; left: 50%; transform: translateX(-50%); background-color: #1C1C1E; border: 1px solid #27272A; color: white; padding: 8px 12px; border-radius: 6px; font-size: 12px; white-space: nowrap; z-index: 10001; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); `; popup.textContent = message; // Create arrow const arrow = document.createElement('div'); arrow.style.cssText = ` position: absolute; bottom: -5px; left: 50%; transform: translateX(-50%) rotate(45deg); width: 10px; height: 10px; background-color: #1C1C1E; border-right: 1px solid #27272A; border-bottom: 1px solid #27272A; `; popup.appendChild(arrow); // Position relative to button button.style.position = 'relative'; button.appendChild(popup); // Auto-remove after 3 seconds setTimeout(() => { if (popup && popup.parentElement) { popup.remove(); } }, 3000); } // Function to show login popup function showLoginPopup(): void { // First remove any existing popups const existingPopup = document.querySelector('#mem0-login-popup'); if (existingPopup) { existingPopup.remove(); } // Create popup container const popupOverlay = document.createElement('div'); popupOverlay.id = 'mem0-login-popup'; popupOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10001; `; const popupContainer = document.createElement('div'); popupContainer.style.cssText = ` background-color: #1C1C1E; border-radius: 12px; width: 320px; padding: 24px; color: white; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; // Close button const closeButton = document.createElement('button'); closeButton.style.cssText = ` position: absolute; top: 16px; right: 16px; background: none; border: none; color: #A1A1AA; font-size: 16px; cursor: pointer; `; closeButton.innerHTML = '×'; closeButton.addEventListener('click', () => { document.body.removeChild(popupOverlay); }); // Logo and heading const logoContainer = document.createElement('div'); logoContainer.style.cssText = ` display: flex; align-items: center; justify-content: center; margin-bottom: 16px; `; const logo = document.createElement('img'); logo.src = chrome.runtime.getURL('icons/mem0-claude-icon.png'); logo.style.cssText = ` width: 24px; height: 24px; border-radius: 50%; margin-right: 12px; `; const logoDark = document.createElement('img'); logoDark.src = chrome.runtime.getURL('icons/mem0-icon-black.png'); logoDark.style.cssText = ` width: 24px; height: 24px; border-radius: 50%; margin-right: 12px; `; const heading = document.createElement('h2'); heading.textContent = 'Sign in to OpenMemory'; heading.style.cssText = ` margin: 0; font-size: 18px; font-weight: 600; `; logoContainer.appendChild(heading); // Message const message = document.createElement('p'); message.textContent = 'Please sign in to access your memories and enhance your conversations!'; message.style.cssText = ` margin-bottom: 24px; color: #D4D4D8; font-size: 14px; line-height: 1.5; text-align: center; `; // Sign in button const signInButton = document.createElement('button'); signInButton.style.cssText = ` display: flex; align-items: center; justify-content: center; width: 100%; padding: 10px; background-color: white; color: black; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: background-color 0.2s; `; // Add text in span for better centering const signInText = document.createElement('span'); signInText.textContent = 'Sign in with Mem0'; signInButton.appendChild(logoDark); signInButton.appendChild(signInText); signInButton.addEventListener('mouseenter', () => { signInButton.style.backgroundColor = '#f5f5f5'; }); signInButton.addEventListener('mouseleave', () => { signInButton.style.backgroundColor = 'white'; }); // Open sign-in page when clicked signInButton.addEventListener('click', () => { window.open('https://app.mem0.ai/login', '_blank'); document.body.removeChild(popupOverlay); }); // Assemble popup popupContainer.appendChild(logoContainer); popupContainer.appendChild(message); popupContainer.appendChild(signInButton); popupOverlay.appendChild(popupContainer); popupOverlay.appendChild(closeButton); // Add click event to close when clicking outside popupOverlay.addEventListener('click', e => { if (e.target === popupOverlay) { document.body.removeChild(popupOverlay); } }); // Add to body document.body.appendChild(popupOverlay); } initializeMem0Integration(); ================================================ FILE: src/mem0/content.ts ================================================ import { getBrowser, sendExtensionEvent } from '../utils/util_functions'; function fetchAndSaveSession() { fetch('https://app.mem0.ai/api/auth/session') .then(response => response.json()) .then(data => { if (data && data.access_token) { chrome.storage.sync.set({ access_token: data.access_token }); chrome.storage.sync.set({ userLoggedIn: true }); //Track successful login sendExtensionEvent('login_success', { browser: getBrowser(), source: 'OPENMEMORY_CHROME_EXTENSION', }); } }) .catch(error => { console.error('Error fetching session:', error); }); } // Check if the URL contains the login page and update userLoggedIn if (window.location.href.includes('https://app.mem0.ai/login')) { chrome.storage.sync.set({ userLoggedIn: false }); } fetchAndSaveSession(); ================================================ FILE: src/perplexity/content.ts ================================================ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { MessageRole } from '../types/api'; import type { MemoryItem, MemorySearchItem, OptionalApiParams } from '../types/memory'; import { SidebarAction } from '../types/messages'; import { type StorageItems, StorageKey } from '../types/storage'; import { createOrchestrator, type SearchStorage } from '../utils/background_search'; import { OPENMEMORY_PROMPTS } from '../utils/llm_prompts'; import { SITE_CONFIG } from '../utils/site_config'; import { getBrowser, sendExtensionEvent } from '../utils/util_functions'; import { OPENMEMORY_UI, type Placement } from '../utils/util_positioning'; export {}; // Add global variables for memory modal let memoryModalShown: boolean = false; let allMemories: string[] = []; // Track added memories by ID const allMemoriesById: Set = new Set(); // Reference to the modal overlay for updates let currentModalOverlay: HTMLDivElement | null = null; // Add a variable to track the submit button observer let submitButtonObserver: MutationObserver | null = null; // Add variable to track if mem0 processing is happening let isProcessingMem0: boolean = false; // Track modal position for dragging let modalPosition: { top: number | null; left: number | null } = { top: null, left: null }; let isDragging: boolean = false; const perplexitySearch = createOrchestrator({ fetch: async function (query: string, opts: { signal?: AbortSignal }) { const data = await new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, StorageKey.SIMILARITY_THRESHOLD, StorageKey.TOP_K, ], function (items) { resolve(items as SearchStorage); } ); }); const apiKey = data[StorageKey.API_KEY]; const accessToken = data[StorageKey.ACCESS_TOKEN]; if (!apiKey && !accessToken) { return []; } const authHeader = accessToken ? `Bearer ${accessToken}` : `Token ${apiKey}`; const userId = data[StorageKey.USER_ID_CAMEL] || data[StorageKey.USER_ID] || 'chrome-extension-user'; const threshold = data[StorageKey.SIMILARITY_THRESHOLD] !== undefined ? data[StorageKey.SIMILARITY_THRESHOLD] : 0.1; const topK = data[StorageKey.TOP_K] !== undefined ? data[StorageKey.TOP_K] : 10; const optionalParams: OptionalApiParams = {}; if (data[StorageKey.SELECTED_ORG]) { optionalParams.org_id = data[StorageKey.SELECTED_ORG]; } if (data[StorageKey.SELECTED_PROJECT]) { optionalParams.project_id = data[StorageKey.SELECTED_PROJECT]; } const payload = { query, filters: { user_id: userId }, rerank: true, threshold: threshold, top_k: topK, filter_memories: false, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }; const res = await fetch('https://api.mem0.ai/v2/memories/search/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify(payload), signal: opts && opts.signal, }); if (!res.ok) { throw new Error(`API request failed with status ${res.status}`); } return await res.json(); }, // Don’t render on prefetch. When modal is open, update it. onSuccess: function (normQuery: string, responseData: MemorySearchItem[]) { if (!memoryModalShown) { return; } const memoryItems = ((responseData as MemorySearchItem[]) || []).map( (item: MemorySearchItem) => ({ id: String(item.id), text: item.memory, categories: item.categories || [], }) ); createMemoryModal(memoryItems, false); }, onError: function () { if (memoryModalShown) { createMemoryModal([], false); } }, minLength: 3, debounceMs: 75, cacheTTL: 60000, }); let perplexityBackgroundSearchHandler: (() => void) | null = null; function hookPerplexityBackgroundSearchTyping() { const textarea = getTextarea(); if (!textarea) { return; } if (textarea.dataset.perplexityBackgroundHooked) { return; } textarea.dataset.perplexityBackgroundHooked = 'true'; if (!perplexityBackgroundSearchHandler) { perplexityBackgroundSearchHandler = function () { const text = getInputText(textarea).trim(); (perplexitySearch as { setText: (text: string) => void }).setText(text); }; } textarea.addEventListener('input', perplexityBackgroundSearchHandler); textarea.addEventListener('keyup', perplexityBackgroundSearchHandler); } function getTextarea(): HTMLElement | null { return ( document.querySelector('textarea[id="ask-input"]') || // Follow-up screen textarea document.querySelector('textarea[placeholder="Ask a follow-up…"]') || // Follow-up screen textarea document.querySelector('div[contenteditable="true"][id="ask-input"]') || // Main screen Lexical editor document.querySelector('div[contenteditable="true"][aria-placeholder="Ask anything…"]') || // Main screen Lexical editor document.querySelector('textarea[placeholder="Ask anything…"]') // Fallback for older versions ); } // Helper function to get text content from either textarea or contenteditable div function getInputText(inputElement: HTMLElement | null): string { if (!inputElement) { return ''; } if (inputElement.tagName === 'TEXTAREA') { return (inputElement as HTMLTextAreaElement).value || ''; } else if (inputElement.contentEditable === 'true') { // For Lexical editor, properly handle the structure const paragraph = inputElement.querySelector('p[dir="ltr"]') as HTMLElement | null; if (paragraph) { let text = ''; const childNodes = paragraph.childNodes; for (let i = 0; i < childNodes.length; i++) { const node = childNodes[i] as HTMLElement | Node; if (node.nodeType === Node.TEXT_NODE) { text += node.textContent || ''; } else if ( (node as HTMLElement).tagName === 'SPAN' && (node as HTMLElement).getAttribute('data-lexical-text') === 'true' ) { text += (node as HTMLElement).textContent || ''; } else if ((node as HTMLElement).tagName === 'BR') { text += '\n'; } } return text; } // Fallback to textContent if structure is different return inputElement.textContent || ''; } return ''; } // Helper function to set text content for either textarea or contenteditable div function setInputText(inputElement: HTMLElement | null, text: string): void { console.log('setInputText called with text:', text); console.log('inputElement:', inputElement); if (!inputElement) { console.log('No input element, returning'); return; } if (inputElement.tagName === 'TEXTAREA') { console.log('Using textarea approach'); (inputElement as HTMLTextAreaElement).value = text; inputElement.dispatchEvent(new Event('input', { bubbles: true })); } else if (inputElement.contentEditable === 'true') { console.log('Using contenteditable approach for Lexical editor'); // New approach: Use clipboard with actual paste event console.log('Attempting clipboard-based approach'); // Focus the input first inputElement.focus(); // Select all existing content document.execCommand('selectAll', false, ''); // Try to write to clipboard and then trigger paste if (navigator.clipboard && navigator.clipboard.writeText) { console.log('Using modern Clipboard API'); navigator.clipboard .writeText(text) .then(() => { console.log('Text written to clipboard successfully'); // Wait a bit then trigger paste setTimeout(() => { // Create and dispatch a paste event const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: new DataTransfer(), }); // Add the text to clipboard data if (pasteEvent.clipboardData) { pasteEvent.clipboardData.setData('text/plain', text); } console.log('Dispatching paste event'); const pasteResult = inputElement.dispatchEvent(pasteEvent); console.log('Paste event result:', pasteResult); // Check if it worked setTimeout(() => { console.log('Content after paste event:', getInputText(inputElement)); // If paste event didn't work, try execCommand paste if (!getInputText(inputElement).includes(text.substring(0, 10))) { console.log('Paste event failed, trying execCommand paste'); const execPasteResult = document.execCommand('paste'); console.log('execCommand paste result:', execPasteResult); setTimeout(() => { console.log('Content after execCommand paste:', getInputText(inputElement)); // If still not working, try the typing simulation if (!getInputText(inputElement).includes(text.substring(0, 10))) { console.log('All clipboard approaches failed, trying typing simulation'); simulateTyping(inputElement, text); } }, 100); } }, 100); }, 100); }) .catch(error => { console.log('Clipboard write failed:', error); // Fallback to typing simulation simulateTyping(inputElement, text); }); } else { console.log('Clipboard API not available, falling back to typing simulation'); simulateTyping(inputElement, text); } } } // Helper function to simulate typing using Selection API function simulateTyping(inputElement: HTMLElement, text: string): void { console.log('simulateTyping called with text:', text); console.log('inputElement:', inputElement); inputElement.focus(); console.log('Input element focused'); // Try using Selection API with Range to insert text const selection = window.getSelection(); // Clear existing content by selecting all if (!selection) { return; } selection.selectAllChildren(inputElement); console.log('Selected all children'); // Try to delete existing content first selection.deleteFromDocument(); console.log('Deleted existing content'); // Now try to insert the new text using different methods // Method 1: Try using insertText with Selection API console.log('Attempting Method 1: Selection API insertText'); try { if (!selection) { throw new Error('No selection'); } const range = selection.getRangeAt(0); range.deleteContents(); range.insertNode(document.createTextNode(text)); console.log('Method 1 succeeded - inserted text node'); console.log('Content after Method 1:', getInputText(inputElement)); // If this worked, we're done if (getInputText(inputElement).includes(text.substring(0, 10))) { console.log('Method 1 worked! Content updated successfully'); return; } } catch (error) { console.log('Method 1 failed:', error); } // Method 2: Try using execCommand with composition events console.log('Attempting Method 2: execCommand with composition'); try { // Start composition const compositionStart = new CompositionEvent('compositionstart', { bubbles: true, cancelable: true, data: '', }); inputElement.dispatchEvent(compositionStart); // Update composition const compositionUpdate = new CompositionEvent('compositionupdate', { bubbles: true, cancelable: true, data: text, }); inputElement.dispatchEvent(compositionUpdate); // End composition const compositionEnd = new CompositionEvent('compositionend', { bubbles: true, cancelable: true, data: text, }); inputElement.dispatchEvent(compositionEnd); console.log('Method 2 composition events dispatched'); console.log('Content after Method 2:', getInputText(inputElement)); // If this worked, we're done if (getInputText(inputElement).includes(text.substring(0, 10))) { console.log('Method 2 worked! Content updated successfully'); return; } } catch (error) { console.log('Method 2 failed:', error); } // Method 3: Try direct DOM manipulation with mutation observer disabled console.log('Attempting Method 3: Direct DOM manipulation'); try { // Find or create the paragraph structure let paragraph = inputElement.querySelector('p[dir="ltr"]'); if (!paragraph) { paragraph = document.createElement('p'); paragraph.setAttribute('dir', 'ltr'); inputElement.appendChild(paragraph); } // Create a span with the text const span = document.createElement('span'); span.setAttribute('data-lexical-text', 'true'); span.textContent = text; // Clear existing content and add new span paragraph.innerHTML = ''; paragraph.appendChild(span); console.log('Method 3 DOM manipulation completed'); console.log('Content after Method 3:', getInputText(inputElement)); // Dispatch events to notify Lexical inputElement.dispatchEvent(new Event('input', { bubbles: true })); inputElement.dispatchEvent(new Event('change', { bubbles: true })); // If this worked, we're done if (getInputText(inputElement).includes(text.substring(0, 10))) { console.log('Method 3 worked! Content updated successfully'); return; } } catch (error) { console.log('Method 3 failed:', error); } // Method 4: Try using keyboard simulation console.log('Attempting Method 4: Keyboard simulation'); try { // Clear content with Ctrl+A and Delete inputElement.dispatchEvent( new KeyboardEvent('keydown', { key: 'a', ctrlKey: true, bubbles: true }) ); inputElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true })); // Type each character with keyboard events for (let i = 0; i < text.length; i++) { const char = text.charAt(i); inputElement.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true })); inputElement.dispatchEvent(new KeyboardEvent('keypress', { key: char, bubbles: true })); inputElement.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true })); } console.log('Method 4 keyboard simulation completed'); console.log('Content after Method 4:', getInputText(inputElement)); } catch (error) { console.log('Method 4 failed:', error); } console.log('All methods attempted. Final content:', getInputText(inputElement)); } // Function to add the mem0 button to the UI async function addMem0Button() { // First check if memory is enabled const memoryEnabled = await getMemoryEnabledState(); if (!memoryEnabled) { // If memory is disabled, remove the button if it exists const existingButton = document.querySelector('.mem0-button-wrapper'); if (existingButton) { existingButton.remove(); } return; } // Prefer OPENMEMORY_UI mounts; if available, use them instead of manual injection if (OPENMEMORY_UI && OPENMEMORY_UI.mountOnEditorFocus) { try { if (!document.getElementById('mem0-icon-button')) { OPENMEMORY_UI.resolveCachedAnchor( { learnKey: location.host + ':' + location.pathname }, null, 24 * 60 * 60 * 1000 ).then(function (hit: { el: Element; placement: Placement | null } | null) { if (!hit || !hit.el) { return; } let hs = OPENMEMORY_UI.createShadowRootHost('mem0-root'); let host = hs.host, shadow = hs.shadow; host.id = 'mem0-icon-button'; const cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.perplexity ? SITE_CONFIG.perplexity : null; let placement = hit.placement || (cfg && cfg.placement) || { strategy: 'inline', where: 'beforeend', inlineAlign: 'end', }; OPENMEMORY_UI.applyPlacement({ container: host, anchor: hit.el, placement: placement }); let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal(); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } try { const cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.perplexity ? SITE_CONFIG.perplexity : null; let modelSel = cfg && cfg.modelButtonSelector; let sendSel = cfg && cfg.sendButtonSelector; let modelBtn = modelSel ? document.querySelector(modelSel) : null; let anchorBtn = modelBtn || (sendSel ? document.querySelector(sendSel) : null); if (anchorBtn) { let container = anchorBtn.parentElement || anchorBtn; let probe: Element | null = container; let hops = 0; while (probe && hops < 5) { let cs = getComputedStyle(probe); if (cs.display === 'flex' && cs.flexDirection !== 'column') { container = probe as HTMLElement; break; } probe = probe.parentElement; hops++; } if (modelBtn) { container.insertBefore(host, anchorBtn.nextSibling); } else { if (host.parentElement !== container || host !== container.firstElementChild) { container.insertBefore(host, container.firstElementChild || anchorBtn); } } host.style.marginLeft = '0'; host.style.marginRight = '0'; if (OPENMEMORY_UI && OPENMEMORY_UI.saveAnchorHint) { try { OPENMEMORY_UI.saveAnchorHint( { learnKey: location.host + ':' + location.pathname }, container, { strategy: 'inline', where: 'beforeend', inlineAlign: 'end' }, true ); } catch (_) { // Ignore errors during re-initialization } } } } catch (_) { // Ignore errors during re-initialization } }); } } catch (_) { // Ignore errors during re-initialization } OPENMEMORY_UI.mountOnEditorFocus({ existingHostSelector: '#mem0-icon-button, .mem0-root', editorSelector: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.perplexity && SITE_CONFIG.perplexity.editorSelector ? SITE_CONFIG.perplexity.editorSelector : 'textarea, [contenteditable], input[type="text"]', deriveAnchor: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.perplexity && typeof SITE_CONFIG.perplexity.deriveAnchor === 'function' ? SITE_CONFIG.perplexity.deriveAnchor : function (editor: Element) { return editor.closest('form') || editor.parentElement; }, placement: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.perplexity && SITE_CONFIG.perplexity.placement ? SITE_CONFIG.perplexity.placement : { strategy: 'inline', where: 'beforeend', inlineAlign: 'end' }, render: function (shadow: ShadowRoot, host: HTMLElement) { host.id = 'mem0-icon-button'; let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal(); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } try { // Dedupe existing hosts, keep the current one Array.from(document.querySelectorAll('#mem0-icon-button, .mem0-root')) .filter(function (n) { return n !== host; }) .forEach(function (n) { try { n.remove(); } catch (_) { // Ignore errors during re-initialization } }); let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.perplexity ? SITE_CONFIG.perplexity : null; let modelSel = cfg && cfg.modelButtonSelector; let sendSel = cfg && cfg.sendButtonSelector; let modelBtn = modelSel ? document.querySelector(modelSel) : null; let anchorBtn = modelBtn || (sendSel ? document.querySelector(sendSel) : null); if (anchorBtn) { let container = anchorBtn.parentElement || anchorBtn; let probe: Element | null = container; let hops = 0; while (probe && hops < 5) { let cs = getComputedStyle(probe); if (cs.display === 'flex' && cs.flexDirection !== 'column') { container = probe as HTMLElement; break; } probe = probe.parentElement; hops++; } // Always place as the left-most icon in the group let first = container.firstElementChild || anchorBtn; if (host.parentElement !== container || host !== first) { container.insertBefore(host, first); } let gap = getComputedStyle(container).gap; if (!gap || gap === 'normal') { gap = '8px'; } host.style.marginLeft = gap; host.style.marginRight = '0'; } } catch (_) { // Ignore errors during re-initialization } }, fallback: function () { let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.perplexity ? SITE_CONFIG.perplexity : null; return OPENMEMORY_UI.mountResilient({ anchors: [ { find: function () { let sel = (cfg && cfg.editorSelector) || 'textarea, [contenteditable], input[type="text"]'; let ed = document.querySelector(sel); if (!ed) { return null; } try { return cfg && typeof cfg.deriveAnchor === 'function' ? cfg.deriveAnchor(ed) : ed.closest('form') || ed.parentElement; } catch (_) { // Ignore errors during re-initialization return ed.closest('form') || ed.parentElement; } }, }, ], placement: (cfg && cfg.placement) || { strategy: 'inline', where: 'beforeend', inlineAlign: 'end', }, enableFloatingFallback: true, render: function (shadow: ShadowRoot, host: HTMLElement) { host.id = 'mem0-icon-button'; let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; } .mem0-btn img { width:18px; height:18px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal(); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } try { Array.from(document.querySelectorAll('#mem0-icon-button, .mem0-root')) .filter(function (n) { return n !== host; }) .forEach(function (n) { try { n.remove(); } catch (_) { // Ignore errors during re-initialization } }); let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.perplexity ? SITE_CONFIG.perplexity : null; let modelSel = cfg && cfg.modelButtonSelector; let sendSel = cfg && cfg.sendButtonSelector; let modelBtn = modelSel ? document.querySelector(modelSel) : null; let anchorBtn = modelBtn || (sendSel ? document.querySelector(sendSel) : null); if (anchorBtn) { let container = anchorBtn.parentElement || anchorBtn; let probe: Element | null = container; let hops = 0; while (probe && hops < 5) { let cs = getComputedStyle(probe); if (cs.display === 'flex' && cs.flexDirection !== 'column') { container = probe as HTMLElement; break; } probe = probe.parentElement; hops++; } if (modelBtn) { container.insertBefore(host, anchorBtn.nextSibling); } else { if (host.parentElement !== container || host.nextSibling !== anchorBtn) { container.insertBefore(host, anchorBtn); } } let gap = getComputedStyle(container).gap; if (!gap || gap === 'normal') { gap = '8px'; } host.style.marginLeft = gap; host.style.marginRight = '0'; } } catch (_) { // Ignore errors during re-initialization } }, }); }, }); return; } // Create a wrapper for the button and tooltip const mem0ButtonWrapper = document.createElement('div'); mem0ButtonWrapper.className = 'mem0-button-wrapper'; mem0ButtonWrapper.style.cssText = ` position: relative; display: inline-block; `; // Create tooltip element const tooltip = document.createElement('div'); tooltip.className = 'mem0-tooltip'; tooltip.style.cssText = ` visibility: hidden; background-color: #27272A; color: #fff; text-align: center; border-radius: 6px; padding: 6px 10px; position: absolute; z-index: 10000; bottom: 125%; left: 50%; transform: translateX(-50%); opacity: 0; transition: opacity 0.3s; font-size: 12px; white-space: nowrap; pointer-events: none; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); border: 1px solid #3B3B3F; `; tooltip.textContent = 'Add memories to your prompt'; // Add tooltip arrow const tooltipArrow = document.createElement('div'); tooltipArrow.style.cssText = ` content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #27272A transparent transparent transparent; `; tooltip.appendChild(tooltipArrow); mem0ButtonWrapper.appendChild(tooltip); // Create the mem0 button const mem0Button = document.createElement('button'); mem0Button.className = 'mem0-claude-btn focus-visible:bg-offsetPlus dark:focus-visible:bg-offsetPlusDark hover:bg-offsetPlus text-textOff dark:text-textOffDark hover:text-textMain dark:hover:bg-offsetPlusDark dark:hover:text-textMainDark font-sans focus:outline-none outline-none outline-transparent transition duration-300 ease-out font-sans select-none items-center relative group/button justify-center text-center items-center rounded-lg cursor-pointer active:scale-[0.97] active:duration-150 active:ease-outExpo origin-center whitespace-nowrap inline-flex text-sm h-8 aspect-[9/8]'; mem0Button.setAttribute('aria-label', 'Mem0 AI'); mem0Button.setAttribute('type', 'button'); mem0Button.style.position = 'relative'; // Create notification dot const notificationDot = document.createElement('div'); notificationDot.id = 'mem0-notification-dot'; notificationDot.style.cssText = ` position: absolute; top: -3px; right: -3px; width: 10px; height: 10px; background-color: rgb(128, 221, 162); border-radius: 50%; border: 2px solid #18181B; display: none; z-index: 1001; pointer-events: none; `; // Add keyframe animation for the dot if (!document.getElementById('notification-dot-animation')) { const style = document.createElement('style'); style.id = 'notification-dot-animation'; style.innerHTML = ` @keyframes popIn { 0% { transform: scale(0); } 50% { transform: scale(1.2); } 100% { transform: scale(1); } } #mem0-notification-dot.active { display: block !important; animation: popIn 0.3s ease-out forwards; } `; document.head.appendChild(style); } // Create inner structure similar to other buttons mem0Button.innerHTML = `
Mem0 AI
`; // Add the notification dot to the button mem0Button.appendChild(notificationDot); // Try to find the right-side icons container near the submit button and prepend there let iconsContainer = null; let modelBtn = null; try { // Prefer explicit model button from site config try { let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.perplexity ? SITE_CONFIG.perplexity : null; let modelSel = cfg && cfg.modelButtonSelector; if (modelSel) { modelBtn = document.querySelector(modelSel); } } catch (_) { // Ignore errors during re-initialization } // 1) Exact class combo (from screenshot) – Tailwind classes require escaping ':' iconsContainer = document.querySelector( 'div.bg-raised.dark\\:bg-offset.flex.items-center.justify-self-end.rounded-full' ); // 2) Slightly relaxed class combo if (!iconsContainer) { iconsContainer = document.querySelector('div.bg-raised.flex.items-center.rounded-full'); } // 3) Attribute-contains selector as a robust fallback if (!iconsContainer) { iconsContainer = document.querySelector( 'div[class*="bg-raised"][class*="items-center"][class*="rounded-full"]' ); } // 4) Fallback based on Submit button proximity if (!iconsContainer) { const submitBtn = document.querySelector('button[aria-label="Submit"]'); if (submitBtn && submitBtn.parentElement) { const sibling = submitBtn.parentElement.previousElementSibling; if (sibling && sibling.querySelectorAll('button').length > 0) { iconsContainer = sibling; } if (!iconsContainer) { const candidates = submitBtn.parentElement.querySelectorAll('div'); for (const c of Array.from(candidates)) { if (c.querySelectorAll('button').length >= 2) { iconsContainer = c; break; } } } } } } catch (_) { // Ignore errors during re-initialization } // If model button found, insert right after it; else, insert at start of icon group if (modelBtn && modelBtn.parentElement) { let container = modelBtn.parentElement; // If the immediate parent is not a horizontal flex, walk up a few levels to find one try { let probe: Element | null = container; let hops = 0; while (probe && hops < 5) { let cs = getComputedStyle(probe); if (cs.display === 'flex' && cs.flexDirection !== 'column') { container = probe as HTMLElement; break; } probe = probe.parentElement; hops++; } } catch (_) { // Ignore errors during re-initialization } // Place as the first icon in the group container.insertBefore(mem0ButtonWrapper, container.firstElementChild); } else if (iconsContainer) { iconsContainer.insertBefore(mem0ButtonWrapper, iconsContainer.firstChild); } // Add the button to the wrapper first mem0ButtonWrapper.appendChild(mem0Button); // Find the input container and insert the button to the LEFT of the input let inputContainer: HTMLElement | null = null; const inputEl = getTextarea(); if (inputEl) { // Try to find the input container by looking for the parent that contains the input let currentElement: HTMLElement | null = inputEl as HTMLElement; while (currentElement && currentElement !== document.body) { // Look for a container that has flex layout and contains the input const computedStyle = window.getComputedStyle(currentElement); if (computedStyle.display === 'flex' && currentElement.contains(inputEl)) { inputContainer = currentElement; break; } currentElement = currentElement.parentElement; } // Fallback: use the direct parent of the input if (!inputContainer) { inputContainer = inputEl.parentElement; } } if (!inputContainer) { setTimeout(addMem0Button, 500); return; } // Insert the button as the FIRST child of the input container (leftmost position) inputContainer.insertBefore(mem0ButtonWrapper, inputContainer.firstChild); // Style the button to match the input area mem0Button.style.width = '32px'; mem0Button.style.height = '32px'; mem0Button.style.borderRadius = '8px'; mem0Button.style.background = 'transparent'; mem0Button.style.display = 'inline-flex'; mem0Button.style.alignItems = 'center'; mem0Button.style.justifyContent = 'center'; mem0Button.style.marginRight = '8px'; mem0ButtonWrapper.style.display = 'inline-flex'; mem0ButtonWrapper.style.alignItems = 'center'; mem0ButtonWrapper.style.justifyContent = 'center'; // Add hover effect for tooltip mem0ButtonWrapper.addEventListener('mouseenter', () => { tooltip.style.visibility = 'visible'; tooltip.style.opacity = '1'; }); mem0ButtonWrapper.addEventListener('mouseleave', () => { tooltip.style.visibility = 'hidden'; tooltip.style.opacity = '0'; }); // Add click event listener - modified to check login first and fix empty text case mem0Button.addEventListener('click', () => { // Get the current input text const textarea = getTextarea(); if (textarea && getInputText(textarea).trim()) { // If there's text in the input, process memories handleMem0Modal('mem0-icon-button'); } else { // If no text, check login status first chrome.storage.sync.get( [StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN], function (items) { if (!items.apiKey && !items.access_token) { // Not logged in, show login popup showLoginPopup(); } else { // Logged in but no text, show tooltip message const originalText = tooltip.textContent; tooltip.textContent = 'Add some text to find memories'; tooltip.style.visibility = 'visible'; tooltip.style.opacity = '1'; // Reset the tooltip after a delay setTimeout(() => { tooltip.textContent = originalText; if (!mem0ButtonWrapper.matches(':hover')) { tooltip.style.visibility = 'hidden'; tooltip.style.opacity = '0'; } }, 1500); } } ); } }); // Setup the notification dot based on input content updateNotificationDot(); } // Function to update the notification dot based on input content function updateNotificationDot() { const textarea = getTextarea(); const host = document.getElementById('mem0-icon-button'); if (!textarea || !host) { setTimeout(updateNotificationDot, 500); return; } const applyState = () => { const inputText = getInputText(textarea) || ''; const hasText = inputText.trim() !== ''; try { host.setAttribute('data-has-text', hasText ? '1' : '0'); } catch (_) { // Ignore errors during re-initialization } }; // Observe and listen for changes try { const mo = new MutationObserver(applyState); mo.observe(textarea, { attributes: true, childList: true, characterData: true, subtree: true }); } catch (_) { // Ignore errors during re-initialization } if (!textarea.dataset.perplexityApplyStateHooked) { textarea.dataset.perplexityApplyStateHooked = 'true'; textarea.addEventListener('input', applyState); textarea.addEventListener('keyup', applyState); textarea.addEventListener('focus', applyState); } applyState(); setTimeout(applyState, 300); } // Function to create memory modal function createMemoryModal( memoryItems: MemoryItem[], isLoading: boolean = false, sourceButtonId: string | null = null ) { // Close existing modal if it exists if (memoryModalShown && currentModalOverlay) { document.body.removeChild(currentModalOverlay); } memoryModalShown = true; let currentMemoryIndex = 0; // Calculate modal dimensions (estimated) const modalWidth = 447; let modalHeight = 400; // Default height let memoriesPerPage = 3; // Default number of memories per page let topPosition; let leftPosition; // Use saved position if available (for dragged modals) if (modalPosition.top !== null && modalPosition.left !== null) { topPosition = modalPosition.top; leftPosition = modalPosition.left; } else { // Different positioning based on which button triggered the modal if (sourceButtonId === 'mem0-icon-button') { // Anchor to the Mem0 host/button in the right icon group when present const iconButton = document.querySelector('#mem0-icon-button') || document.querySelector('.mem0-claude-btn'); if (iconButton) { const buttonRect = iconButton.getBoundingClientRect(); const viewportHeight = window.innerHeight; // Place the modal immediately to the left of the icon group leftPosition = Math.max(10, buttonRect.left - modalWidth - 10); // Vertically center relative to the icon height, but keep on-screen topPosition = buttonRect.top + buttonRect.height / 2 - modalHeight / 2; topPosition = Math.max(10, Math.min(topPosition, viewportHeight - modalHeight - 10)); } else { // Fallback to default positioning positionDefault(); } } else { // Default positioning positionDefault(); } } // Helper function for default positioning function positionDefault() { // Prefer the actual Mem0 host if present; otherwise, try to use the right icon group container let anchor = document.querySelector('#mem0-icon-button') || document.querySelector('.mem0-claude-btn'); if (!anchor) { // Heuristics to find the right-side icon group near the submit button try { anchor = document.querySelector( 'div.bg-raised.dark\\:bg-offset.flex.items-center.justify-self-end.rounded-full' ) || document.querySelector('div.bg-raised.flex.items-center.rounded-full') || document.querySelector( 'div[class*="bg-raised"][class*="items-center"][class*="rounded-full"]' ); if (!anchor) { const submitBtn = document.querySelector('button[aria-label="Submit"]'); if (submitBtn && submitBtn.parentElement) { const sibling = submitBtn.parentElement.previousElementSibling; if (sibling && sibling.querySelectorAll('button').length > 0) { anchor = sibling; } } } } catch (_) { // Ignore errors during re-initialization } } if (!anchor) { console.error('Mem0 anchor not found for positioning'); // As a last resort, place near center leftPosition = Math.max(10, (window.innerWidth - modalWidth) / 2); topPosition = Math.max(10, (window.innerHeight - modalHeight) / 2); return; } const rect = anchor.getBoundingClientRect(); const viewportHeight = window.innerHeight; // Place the modal immediately to the left of the anchor/group leftPosition = Math.max(10, rect.left - modalWidth - 10); // Vertically center relative to the group, clamped to viewport topPosition = rect.top + rect.height / 2 - modalHeight / 2; topPosition = Math.max(10, Math.min(topPosition, viewportHeight - modalHeight - 10)); } // Create modal overlay const modalOverlay = document.createElement('div'); modalOverlay.id = 'mem0-modal-overlay'; modalOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: transparent; display: flex; z-index: 10000; pointer-events: auto; `; // Save reference to current modal overlay currentModalOverlay = modalOverlay; // Add event listener to close modal when clicking outside modalOverlay.addEventListener('click', event => { // Only close if clicking directly on the overlay, not its children if (event.target === modalOverlay) { closeModal(); } }); // Create modal container with positioning const modalContainer = document.createElement('div'); // Position the modal below or above the button modalContainer.style.cssText = ` background-color: #1C1C1E; border-radius: 12px; width: ${modalWidth}px; height: ${modalHeight}px; display: flex; flex-direction: column; color: white; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); position: absolute; top: ${topPosition}px; left: ${leftPosition}px; pointer-events: auto; border: 1px solid #27272A; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; overflow: hidden; `; // Create modal header const modalHeader = document.createElement('div'); modalHeader.style.cssText = ` display: flex; align-items: center; padding: 10px 16px; justify-content: space-between; background-color: #232325; flex-shrink: 0; cursor: move; user-select: none; `; // Create header left section with logo and title const headerLeft = document.createElement('div'); headerLeft.style.cssText = ` display: flex; flex-direction: row; align-items: center; `; // Add Mem0 logo and title to header const logoImg = document.createElement('img'); logoImg.src = chrome.runtime.getURL('icons/mem0-claude-icon.png'); logoImg.style.cssText = ` width: 26px; height: 26px; border-radius: 50%; `; // Create title element const title = document.createElement('div'); title.textContent = 'OpenMemory'; title.style.cssText = ` font-size: 16px; font-weight: 600; color: #FFFFFF; margin-left: 8px; `; // Create header right section with Add to Prompt button const headerRight = document.createElement('div'); headerRight.style.cssText = ` display: flex; flex-direction: row; align-items: center; gap: 8px; `; // Create Add to Prompt button with arrow const addToPromptBtn = document.createElement('button'); addToPromptBtn.style.cssText = ` display: flex; flex-direction: row; align-items: center; padding: 5px 16px; gap: 8px; background-color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 12px; font-weight: 600; color: black; `; addToPromptBtn.textContent = 'Add to Prompt'; // Add arrow icon to button const arrowIcon = document.createElement('span'); arrowIcon.innerHTML = ` `; addToPromptBtn.appendChild(arrowIcon); // Add click handler for the Add to Prompt button addToPromptBtn.addEventListener('click', () => { // Only add memories that are not already added const newMemories = memoryItems .filter(memory => !allMemoriesById.has(String(memory.id))) .map(memory => { allMemoriesById.add(String(memory.id)); return String(memory.text || ''); }); sendExtensionEvent('memory_injection', { provider: 'perplexity', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), injected_all: true, memory_count: newMemories.length, }); // Add all new memories to allMemories allMemories.push(...newMemories); // Update the input with all memories if (allMemories.length > 0) { updateInputWithMemories(); closeModal(); } else { // If no new memories were added but we have existing ones, just close if (allMemoriesById.size > 0) { closeModal(); } } // Remove all added memories from the memoryItems list for (let i = memoryItems.length - 1; i >= 0; i--) { if (allMemoriesById.has(String(memoryItems[i]?.id))) { memoryItems.splice(i, 1); } } }); // Create settings button const settingsBtn = document.createElement('button'); settingsBtn.style.cssText = ` background: none; border: none; cursor: pointer; padding: 8px; opacity: 0.6; transition: opacity 0.2s; `; settingsBtn.innerHTML = ` `; // Add click event to open app.mem0.ai in a new tab settingsBtn.addEventListener('click', () => { if (currentModalOverlay && document.body.contains(currentModalOverlay)) { document.body.removeChild(currentModalOverlay); memoryModalShown = false; currentModalOverlay = null; } chrome.runtime.sendMessage({ action: SidebarAction.SIDEBAR_SETTINGS }); }); // Add hover effect for the settings button settingsBtn.addEventListener('mouseenter', () => { settingsBtn.style.opacity = '1'; }); settingsBtn.addEventListener('mouseleave', () => { settingsBtn.style.opacity = '0.6'; }); // Assemble header headerLeft.appendChild(logoImg); headerLeft.appendChild(title); headerRight.appendChild(addToPromptBtn); headerRight.appendChild(settingsBtn); modalHeader.appendChild(headerLeft); modalHeader.appendChild(headerRight); // Add drag functionality let startX = 0; let startY = 0; let initialX = 0; let initialY = 0; modalHeader.addEventListener('mousedown', (e: MouseEvent) => { const target = e.target as HTMLElement | null; if (target && (target === modalHeader || modalHeader.contains(target))) { // Don't start drag if clicking on buttons if (target.tagName === 'BUTTON' || target.closest('button')) { return; } isDragging = true; startX = e.clientX; startY = e.clientY; // Get current position const rect = modalContainer.getBoundingClientRect(); initialX = rect.left; initialY = rect.top; modalContainer.style.transition = 'none'; modalContainer.style.opacity = '1'; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); e.preventDefault(); } }); function handleMouseMove(e: MouseEvent) { if (!isDragging) { return; } const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; let newX = initialX + deltaX; let newY = initialY + deltaY; // Keep modal within viewport bounds const maxX = window.innerWidth - modalWidth; const maxY = window.innerHeight - modalHeight; newX = Math.max(0, Math.min(newX, maxX)); newY = Math.max(0, Math.min(newY, maxY)); modalContainer.style.left = newX + 'px'; modalContainer.style.top = newY + 'px'; // Update stored position modalPosition.left = newX; modalPosition.top = newY; } function handleMouseUp() { if (isDragging) { isDragging = false; modalContainer.style.transition = ''; modalContainer.style.opacity = '1'; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); } } // Content section const contentSection = document.createElement('div'); const contentSectionHeight = modalHeight - 130; // Account for header and navigation contentSection.style.cssText = ` display: flex; flex-direction: column; padding: 0 16px; gap: 12px; overflow: hidden; flex: 1; height: ${contentSectionHeight}px; `; // Create memories counter const memoriesCounter = document.createElement('div'); memoriesCounter.style.cssText = ` font-size: 16px; font-weight: 600; color: #FFFFFF; margin-top: 16px; flex-shrink: 0; `; // Update counter text based on loading state and number of memories if (isLoading) { memoriesCounter.textContent = `Loading Relevant Memories...`; } else { memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`; } // Calculate max height for memories content based on modal height const memoriesContentMaxHeight = contentSectionHeight - 40; // Account for memories counter // Create memories content container with adjusted height const memoriesContent = document.createElement('div'); memoriesContent.style.cssText = ` display: flex; flex-direction: column; gap: 8px; overflow-y: auto; flex: 1; max-height: ${memoriesContentMaxHeight}px; padding-right: 8px; margin-right: -8px; scrollbar-width: none; -ms-overflow-style: none; `; memoriesContent.style.cssText += '::-webkit-scrollbar { display: none; }'; // Track currently expanded memory let currentlyExpandedMemory: HTMLElement | null = null; // Create category section const categorySection = document.createElement('div'); categorySection.style.cssText = ` display: flex; gap: 8px; padding: 0 8px; `; // Function to create skeleton loading items (adjusted for different heights) function createSkeletonItems() { memoriesContent.innerHTML = ''; for (let i = 0; i < memoriesPerPage; i++) { const skeletonItem = document.createElement('div'); skeletonItem.style.cssText = ` display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; padding: 12px; background-color: #27272A; border-radius: 8px; height: 72px; flex-shrink: 0; animation: pulse 1.5s infinite ease-in-out; `; const skeletonText = document.createElement('div'); skeletonText.style.cssText = ` background-color: #383838; border-radius: 4px; height: 14px; width: 85%; margin-bottom: 8px; `; const skeletonText2 = document.createElement('div'); skeletonText2.style.cssText = ` background-color: #383838; border-radius: 4px; height: 14px; width: 65%; `; const skeletonActions = document.createElement('div'); skeletonActions.style.cssText = ` display: flex; gap: 4px; margin-left: 10px; `; const skeletonButton1 = document.createElement('div'); skeletonButton1.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; background-color: #383838; `; const skeletonButton2 = document.createElement('div'); skeletonButton2.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; background-color: #383838; `; skeletonActions.appendChild(skeletonButton1); skeletonActions.appendChild(skeletonButton2); const textContainer = document.createElement('div'); textContainer.style.cssText = ` display: flex; flex-direction: column; flex-grow: 1; `; textContainer.appendChild(skeletonText); textContainer.appendChild(skeletonText2); skeletonItem.appendChild(textContainer); skeletonItem.appendChild(skeletonActions); memoriesContent.appendChild(skeletonItem); } // Add keyframe animation to document if not exists if (!document.getElementById('skeleton-animation')) { const style = document.createElement('style'); style.id = 'skeleton-animation'; style.innerHTML = ` @keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 0.8; } 100% { opacity: 0.6; } } `; document.head.appendChild(style); } } // Function to show memories with adjusted count based on modal position function showMemories() { memoriesContent.innerHTML = ''; if (isLoading) { createSkeletonItems(); return; } if (memoryItems.length === 0) { showEmptyState(); updateNavigationState(0, 0); return; } // Use the dynamically set memoriesPerPage value const memoriesToShow = Math.min(memoriesPerPage, memoryItems.length); // Calculate total pages and current page const totalPages = Math.ceil(memoryItems.length / memoriesToShow); const currentPage = Math.floor(currentMemoryIndex / memoriesToShow) + 1; // Reset currentMemoryIndex if it's beyond the available memories if (currentMemoryIndex >= memoryItems.length) { currentMemoryIndex = 0; } // Update navigation buttons state updateNavigationState(currentPage, totalPages); // Count how many memories we've displayed let displayedCount = 0; // Start from the current index let index = currentMemoryIndex; while (displayedCount < memoriesToShow && index < memoryItems.length) { const memory = memoryItems[index]!; // Only display memories that haven't been added yet if (!allMemoriesById.has(String(memory.id))) { // Ensure memory has an ID if (!memory.id) { memory.id = `memory-${Date.now()}-${index}`; } const memoryContainer = document.createElement('div'); memoryContainer.style.cssText = ` display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; padding: 12px; background-color: #27272A; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; min-height: 72px; max-height: 72px; overflow: hidden; flex-shrink: 0; `; const memoryText = document.createElement('div'); memoryText.style.cssText = ` font-size: 14px; line-height: 1.5; color: #D4D4D8; flex-grow: 1; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; transition: all 0.2s ease; height: 42px; /* Height for 2 lines of text */ `; memoryText.textContent = String(memory.text || ''); const actionsContainer = document.createElement('div'); actionsContainer.style.cssText = ` display: flex; gap: 4px; margin-left: 10px; flex-shrink: 0; `; // Add button const addButton = document.createElement('button'); addButton.style.cssText = ` border: none; cursor: pointer; padding: 4px; background:rgb(66, 66, 69); color:rgb(199, 199, 201); border-radius: 100%; transition: all 0.2s ease; `; addButton.innerHTML = ` `; // Add click handler for add button addButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); sendExtensionEvent('memory_injection', { provider: 'perplexity', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), injected_all: false, memory_id: memory.id, }); // Add this memory allMemoriesById.add(String(memory.id)); allMemories.push(String(memory.text || '')); updateInputWithMemories(); // Remove this memory from the list const index = memoryItems.findIndex((m: MemoryItem) => m.id === memory.id); if (index !== -1) { memoryItems.splice(index, 1); // Recalculate pagination after removing an item // If we're on a page that's now empty, go to previous page if (currentMemoryIndex > 0 && currentMemoryIndex >= memoryItems.length) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); } memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`; showMemories(); } }); // Menu button const menuButton = document.createElement('button'); menuButton.style.cssText = ` background: none; border: none; cursor: pointer; padding: 4px; color: #A1A1AA; `; menuButton.innerHTML = ` `; // Track expanded state let isExpanded = false; // Create remove button (hidden by default) const removeButton = document.createElement('button'); removeButton.style.cssText = ` display: none; align-items: center; gap: 6px; background:rgb(66, 66, 69); color:rgb(199, 199, 201); border-radius: 8px; padding: 2px 4px; border: none; cursor: pointer; font-size: 13px; margin-top: 12px; width: fit-content; `; removeButton.innerHTML = ` Remove `; // Create content wrapper for text and remove button const contentWrapper = document.createElement('div'); contentWrapper.style.cssText = ` display: flex; flex-direction: column; flex-grow: 1; `; contentWrapper.appendChild(memoryText); contentWrapper.appendChild(removeButton); // Function to expand memory const expandMemory = () => { if (currentlyExpandedMemory && currentlyExpandedMemory !== memoryContainer) { currentlyExpandedMemory.dispatchEvent(new Event('collapse')); } isExpanded = true; memoryText.style.webkitLineClamp = 'unset'; memoryText.style.height = 'auto'; contentWrapper.style.overflowY = 'auto'; contentWrapper.style.maxHeight = '240px'; // Limit height to prevent overflow contentWrapper.style.scrollbarWidth = 'none'; contentWrapper.style.msOverflowStyle = 'none'; contentWrapper.style.cssText += '::-webkit-scrollbar { display: none; }'; memoryContainer.style.backgroundColor = '#1C1C1E'; memoryContainer.style.maxHeight = '300px'; // Allow expansion but within container memoryContainer.style.overflow = 'hidden'; removeButton.style.display = 'flex'; currentlyExpandedMemory = memoryContainer; // Scroll to make expanded memory visible if needed memoriesContent.scrollTop = memoryContainer.offsetTop - memoriesContent.offsetTop; }; // Function to collapse memory const collapseMemory = () => { isExpanded = false; memoryText.style.webkitLineClamp = '2'; memoryText.style.height = '42px'; contentWrapper.style.overflowY = 'visible'; memoryContainer.style.backgroundColor = '#27272A'; memoryContainer.style.maxHeight = '72px'; memoryContainer.style.overflow = 'hidden'; removeButton.style.display = 'none'; currentlyExpandedMemory = null; }; memoryContainer.addEventListener('collapse', collapseMemory); menuButton.addEventListener('click', e => { e.stopPropagation(); if (isExpanded) { collapseMemory(); } else { expandMemory(); } }); // Add click handler for remove button removeButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); // Remove from memoryItems const index = memoryItems.findIndex((m: MemoryItem) => m.id === memory.id); if (index !== -1) { memoryItems.splice(index, 1); // If we're on the last page and it's now empty, go to previous page if (currentMemoryIndex > 0 && currentMemoryIndex >= memoryItems.length) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); } memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`; showMemories(); } }); actionsContainer.appendChild(addButton); actionsContainer.appendChild(menuButton); memoryContainer.appendChild(contentWrapper); memoryContainer.appendChild(actionsContainer); memoriesContent.appendChild(memoryContainer); // Add hover effect memoryContainer.addEventListener('mouseenter', () => { memoryContainer.style.backgroundColor = isExpanded ? '#18181B' : '#323232'; }); memoryContainer.addEventListener('mouseleave', () => { memoryContainer.style.backgroundColor = isExpanded ? '#1C1C1E' : '#27272A'; }); // Increment displayed count displayedCount++; } // Move to next memory index++; } // If we didn't display any memories but there are available ones, // reset the index and try again (this handles the case where all visible memories // have been filtered out) if (displayedCount === 0 && memoryItems.length > 0) { currentMemoryIndex = 0; showMemories(); } else if (displayedCount === 0) { // If truly no memories available, show empty state showEmptyState(); } } // Function to show empty state function showEmptyState() { memoriesContent.innerHTML = ''; const emptyContainer = document.createElement('div'); emptyContainer.style.cssText = ` display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 32px 16px; text-align: center; flex: 1; min-height: 200px; `; const emptyIcon = document.createElement('div'); emptyIcon.innerHTML = ` `; emptyIcon.style.marginBottom = '16px'; const emptyText = document.createElement('div'); emptyText.textContent = 'No relevant memories found'; emptyText.style.cssText = ` color: #71717A; font-size: 14px; font-weight: 500; `; emptyContainer.appendChild(emptyIcon); emptyContainer.appendChild(emptyText); memoriesContent.appendChild(emptyContainer); } // Navigation section at bottom const navigationSection = document.createElement('div'); navigationSection.style.cssText = ` display: flex; justify-content: center; gap: 12px; padding: 10px; border-top: none; flex-shrink: 0; `; // Navigation buttons const prevButton = document.createElement('button'); prevButton.innerHTML = ` `; prevButton.style.cssText = ` background: #27272A; border: none; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.2s; `; const nextButton = document.createElement('button'); nextButton.innerHTML = ` `; nextButton.style.cssText = prevButton.style.cssText; // Update navigation button states function updateNavigationState(currentPage: number, totalPages: number) { if (memoryItems.length === 0 || totalPages === 0) { prevButton.disabled = true; prevButton.style.opacity = '0.5'; prevButton.style.cursor = 'not-allowed'; nextButton.disabled = true; nextButton.style.opacity = '0.5'; nextButton.style.cursor = 'not-allowed'; return; } if (isLoading || currentPage <= 1) { prevButton.disabled = true; prevButton.style.opacity = '0.5'; prevButton.style.cursor = 'not-allowed'; } else { prevButton.disabled = false; prevButton.style.opacity = '1'; prevButton.style.cursor = 'pointer'; } if (isLoading || currentPage >= totalPages) { nextButton.disabled = true; nextButton.style.opacity = '0.5'; nextButton.style.cursor = 'not-allowed'; } else { nextButton.disabled = false; nextButton.style.opacity = '1'; nextButton.style.cursor = 'pointer'; } } // Add navigation button handlers prevButton.addEventListener('click', () => { if (!isLoading && currentMemoryIndex > 0) { currentMemoryIndex -= memoriesPerPage; showMemories(); } }); nextButton.addEventListener('click', () => { if (!isLoading && currentMemoryIndex < memoryItems.length - memoriesPerPage) { currentMemoryIndex += memoriesPerPage; showMemories(); } }); // Add hover effects [prevButton, nextButton].forEach(button => { button.addEventListener('mouseenter', () => { if (!button.disabled) { button.style.backgroundColor = '#323232'; } }); button.addEventListener('mouseleave', () => { if (!button.disabled) { button.style.backgroundColor = '#27272A'; } }); }); // Assemble modal contentSection.appendChild(memoriesCounter); contentSection.appendChild(memoriesContent); modalContainer.appendChild(modalHeader); modalContainer.appendChild(contentSection); // Only add navigation when not in loading state if (!isLoading) { modalContainer.appendChild(navigationSection); navigationSection.appendChild(prevButton); navigationSection.appendChild(nextButton); } modalOverlay.appendChild(modalContainer); // Append to body document.body.appendChild(modalOverlay); // Show the first memory and update navigation showMemories(); updateNavigationState(1, Math.ceil(memoryItems.length / memoriesPerPage)); } // Shared function to update the input field with all collected memories function updateInputWithMemories() { const inputElement = getTextarea(); if (!inputElement || allMemories.length === 0) { return; } // First, remove any existing memory content from the input let currentContent = getInputText(inputElement); const memoryMarker = '\n\n' + OPENMEMORY_PROMPTS.memory_marker_prefix; if (currentContent.includes(memoryMarker)) { currentContent = currentContent.substring(0, currentContent.indexOf(memoryMarker)).trim(); } // Create the memory content string let memoriesContent = '\n\n' + OPENMEMORY_PROMPTS.memory_header_text + '\n'; // Add all memories to the content allMemories.forEach((mem, index) => { memoriesContent += `- ${mem}`; if (index < allMemories.length - 1) { memoriesContent += '\n'; } }); // Set the input value with the cleaned content + memories setInputValue(inputElement, currentContent + memoriesContent); } // Add a function to get the memory_enabled state function getMemoryEnabledState(): Promise { return new Promise(resolve => { chrome.storage.sync.get([StorageKey.MEMORY_ENABLED], function (result) { resolve(result.memory_enabled !== false); // Default to true if not set }); }); } // Function to capture and store the current message as a memory function captureAndStoreMemory() { // Get the message content const textarea = getTextarea(); if (!textarea) { return; } // Get raw content from the input element let message = getInputText(textarea); if (!message || message.trim() === '') { return; } // Skip if message contains the memory wrapper if ((message || '').includes('Here is some of my memories to help')) { // Extract only the user's original message const parts = (message || '').split('Here is some of my memories to help'); message = (parts[0] || '').trim(); } // Skip if message is empty after cleaning if (!message || message.trim() === '') { return; } // Asynchronously store the memory chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.MEMORY_ENABLED, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, ], function (items) { // Skip if memory is disabled or no credentials if (items.memory_enabled === false || (!items.apiKey && !items.access_token)) { return; } const authHeader = items.access_token ? `Bearer ${items.access_token}` : `Token ${items.apiKey}`; const userId = items.userId || items.user_id || 'chrome-extension-user'; const optionalParams: OptionalApiParams = {}; if (items.selected_org) { optionalParams.org_id = items.selected_org; } if (items.selected_project) { optionalParams.project_id = items.selected_project; } // Send memory to mem0 API asynchronously without waiting for response fetch('https://api.mem0.ai/v1/memories/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify({ messages: [{ role: MessageRole.User, content: message }], user_id: userId, infer: true, metadata: { provider: 'Perplexity', }, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }), }).catch(error => { console.error('Error saving memory:', error); }); } ); } // Modify the setupSubmitButtonListener function to call captureAndStoreMemory function setupSubmitButtonListener() { // Find the submit button const submitButton = document.querySelector('button[aria-label="Submit"]'); if (!submitButton) { setTimeout(setupSubmitButtonListener, 500); return; } // Check if we already added a listener if (submitButton.dataset.mem0Listener) { return; } // Mark the button as having our listener submitButton.dataset.mem0Listener = 'true'; // Add click event listener to the submit button submitButton.addEventListener('click', () => { // Capture and save memory before clearing captureAndStoreMemory(); // Give a small delay to allow the submission to process setTimeout(() => { // Clear all memories allMemories = []; console.log('Message sent, memories cleared'); }, 100); }); // Also monitor for Enter key submission const textarea = getTextarea(); if (textarea && !textarea.dataset.mem0EnterListener) { textarea.dataset.mem0EnterListener = 'true'; textarea.addEventListener('keydown', (event: KeyboardEvent) => { if (event.key === 'Enter' && !event.shiftKey) { // Capture and save memory before clearing captureAndStoreMemory(); // User pressed Enter to submit setTimeout(() => { // Clear all memories allMemories = []; console.log('Message sent via Enter key, memories cleared'); }, 100); } }); } // Set up a MutationObserver to monitor conversation flow and clear memories after answers appear setupConversationObserver(); } // Monitor the conversation for new responses function setupConversationObserver() { // If we already have an observer, disconnect it if (submitButtonObserver) { submitButtonObserver.disconnect(); } // Find the conversation container const conversationContainer = document.querySelector('main'); if (!conversationContainer) { setTimeout(setupConversationObserver, 1000); return; } // Create a new observer submitButtonObserver = new MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { // Check if a new answer block has been added const answersAdded = Array.from(mutation.addedNodes).some(node => { if (node.nodeType === Node.ELEMENT_NODE) { const el = node as Element; return el.classList.contains('answer-container'); } return false; }); if (answersAdded) { // New answer appeared, clear memories allMemories = []; console.log('New answer detected, memories cleared'); } } } }); // Start observing submitButtonObserver.observe(conversationContainer, { childList: true, subtree: true, }); } function setupInputObserver() { const textarea = getTextarea(); if (!textarea) { setTimeout(setupInputObserver, 500); return; } // Remove Enter key event listeners } async function handleMem0Processing( capturedText?: string, clickSendButton: boolean = false, sourceButtonId: string | null = null ) { const textarea = getTextarea(); if (!textarea) { console.error('No input textarea found'); return; } const message = capturedText || getInputText(textarea).trim(); // Store the original message to preserve it const originalMessage = message; if (!message) { console.error('No input message found'); return; } // If already processing, don't start another operation if (isProcessingMem0) { return; } isProcessingMem0 = true; try { const data = await new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.MEMORY_ENABLED, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, StorageKey.SIMILARITY_THRESHOLD, StorageKey.TOP_K, ], function (items) { resolve(items as StorageItems); } ); }); const apiKey = data[StorageKey.API_KEY]; const userId = (data[StorageKey.USER_ID_CAMEL] || data[StorageKey.USER_ID] || 'chrome-extension-user') as string; const accessToken = data[StorageKey.ACCESS_TOKEN]; const memoryEnabled = data[StorageKey.MEMORY_ENABLED] !== false; // Default to true if not set const optionalParams: OptionalApiParams = {}; if (data[StorageKey.SELECTED_ORG]) { optionalParams.org_id = data[StorageKey.SELECTED_ORG]; } if (data[StorageKey.SELECTED_PROJECT]) { optionalParams.project_id = data[StorageKey.SELECTED_PROJECT]; } if (!apiKey && !accessToken) { console.error('No API Key or Access Token found'); isProcessingMem0 = false; // Show login popup instead of just returning showLoginPopup(); return; } if (!memoryEnabled) { console.log('Memory is disabled. Skipping API calls.'); if (clickSendButton) { clickSendButtonWithDelay(); } isProcessingMem0 = false; return; } // Show loading modal now that we've confirmed credentials and memory enabled createMemoryModal([], true, sourceButtonId); sendExtensionEvent('modal_clicked', { provider: 'perplexity', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), }); const authHeader = accessToken ? `Bearer ${accessToken}` : `Token ${apiKey}`; const messages = [{ role: MessageRole.User, content: message }]; // Use orchestrator immediate run perplexitySearch.runImmediate(message); // If no memories found, the createMemoryModal function will show empty state // Only send the message if explicitly requested and modal isn't shown if (clickSendButton && !memoryModalShown) { clickSendButtonWithDelay(); } // Preserve original text regardless setInputValue(textarea, originalMessage); // New add memory API call (non-blocking) fetch('https://api.mem0.ai/v1/memories/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify({ messages: messages, user_id: userId, infer: true, source: 'OPENMEMORY_CHROME_EXTENSION', metadata: { provider: 'Perplexity', }, ...optionalParams, }), }) .then(response => { if (!response.ok) { console.error(`Failed to add memory: ${response.status}`); } }) .catch(error => { console.error('Error adding memory:', error); }); } catch (error) { console.error('Error:', error); // Ensure the original message is preserved even if there's an error const inputElement = getTextarea(); if (inputElement && originalMessage) { setInputValue(inputElement, originalMessage); } // Close the modal if there was an error closeModal(); } finally { isProcessingMem0 = false; } } // Thin wrapper to trigger memory flow from the UI button function handleMem0Modal(sourceButtonId: string | null = null) { try { const textarea = getTextarea(); const captured = textarea ? (getInputText(textarea) || '').trim() : ''; handleMem0Processing(captured, false, sourceButtonId); } catch (_) { // Ignore errors during re-initialization } } function setInputValue(inputElement: HTMLElement | null, value: string) { if (inputElement) { setInputText(inputElement, value); } } function clickSendButtonWithDelay() { setTimeout(() => { const sendButton = document.querySelector( 'button[aria-label="Submit"]' ) as HTMLButtonElement | null; if (sendButton) { sendButton.click(); // Clear memories after clicking the send button setTimeout(() => { allMemories = []; console.log('Message sent via clickSendButtonWithDelay, memories cleared'); }, 100); } else { console.error('Send button not found'); } }, 0); } function initializeMem0Integration() { // First check if memory is enabled getMemoryEnabledState().then(memoryEnabled => { if (!memoryEnabled) { // If memory is disabled, remove any existing button const existingButton = document.querySelector('.mem0-button-wrapper'); if (existingButton) { existingButton.remove(); } return; } setupInputObserver(); try { hookPerplexityBackgroundSearchTyping(); } catch { // Ignore errors } // Add the Mem0 button to the UI addMem0Button(); // Set up the submit button listener to clear memories setupSubmitButtonListener(); // Add DOM mutation observer to monitor for UI changes const bodyObserver = new MutationObserver(() => { addMem0Button(); setupSubmitButtonListener(); updateNotificationDot(); }); bodyObserver.observe(document.body, { childList: true, subtree: true, }); // Re-check periodically in case of navigation or UI changes setInterval(() => { addMem0Button(); setupSubmitButtonListener(); updateNotificationDot(); }, 3000); // Set up keyboard shortcut to trigger Mem0 (Ctrl+M) document.addEventListener('keydown', function (event: KeyboardEvent) { if (event.ctrlKey && event.key === 'm') { event.preventDefault(); const textarea = getTextarea(); if (textarea && getInputText(textarea).trim()) { handleMem0Modal('mem0-icon-button'); } } }); }); } initializeMem0Integration(); // Function to show login popup function showLoginPopup() { // First remove any existing popups const existingPopup = document.querySelector('#mem0-login-popup'); if (existingPopup) { existingPopup.remove(); } // Create popup container const popupOverlay = document.createElement('div'); popupOverlay.id = 'mem0-login-popup'; popupOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10001; `; const popupContainer = document.createElement('div'); popupContainer.style.cssText = ` background-color: #1C1C1E; border-radius: 12px; width: 320px; padding: 24px; color: white; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; position: relative; `; // Close button const closeButton = document.createElement('button'); closeButton.style.cssText = ` position: absolute; top: 16px; right: 16px; background: none; border: none; color: #A1A1AA; font-size: 16px; cursor: pointer; `; closeButton.innerHTML = '×'; closeButton.addEventListener('click', () => { document.body.removeChild(popupOverlay); }); // Logo and heading const logoContainer = document.createElement('div'); logoContainer.style.cssText = ` display: flex; align-items: center; justify-content: center; margin-bottom: 16px; `; const logo = document.createElement('img'); logo.src = chrome.runtime.getURL('icons/mem0-claude-icon.png'); logo.style.cssText = ` width: 24px; height: 24px; border-radius: 50%; margin-right: 12px; `; const logoDark = document.createElement('img'); logoDark.src = chrome.runtime.getURL('icons/mem0-icon-black.png'); logoDark.style.cssText = ` width: 24px; height: 24px; border-radius: 50%; margin-right: 12px; `; const heading = document.createElement('h2'); heading.textContent = 'Sign in to OpenMemory'; heading.style.cssText = ` margin: 0; font-size: 18px; font-weight: 600; `; logoContainer.appendChild(heading); // Message const message = document.createElement('p'); message.textContent = 'Please sign in to access your memories and enhance your conversations!'; message.style.cssText = ` margin-bottom: 24px; color: #D4D4D8; font-size: 14px; line-height: 1.5; text-align: center; `; // Sign in button const signInButton = document.createElement('button'); signInButton.style.cssText = ` display: flex; align-items: center; justify-content: center; width: 100%; padding: 10px; background-color: white; color: black; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: background-color 0.2s; `; // Add text in span for better centering const signInText = document.createElement('span'); signInText.textContent = 'Sign in with Mem0'; signInButton.appendChild(logoDark); signInButton.appendChild(signInText); signInButton.addEventListener('mouseenter', () => { signInButton.style.backgroundColor = '#f5f5f5'; }); signInButton.addEventListener('mouseleave', () => { signInButton.style.backgroundColor = 'white'; }); // Open sign-in page when clicked signInButton.addEventListener('click', () => { window.open('https://app.mem0.ai/login', '_blank'); document.body.removeChild(popupOverlay); }); // Assemble popup popupContainer.appendChild(closeButton); popupContainer.appendChild(logoContainer); popupContainer.appendChild(message); popupContainer.appendChild(signInButton); popupOverlay.appendChild(popupContainer); // Add click event to close when clicking outside popupOverlay.addEventListener('click', e => { if (e.target === popupOverlay) { document.body.removeChild(popupOverlay); } }); // Add to body document.body.appendChild(popupOverlay); } // Global closeModal function to fix the reference error function closeModal() { if (memoryModalShown && currentModalOverlay) { document.body.removeChild(currentModalOverlay); memoryModalShown = false; // Reset modal position when closing modalPosition = { top: null, left: null }; } } ================================================ FILE: src/popup.html ================================================ Mem0 Sign In

Sign in to OpenMemory

Please sign in to access your memories and personalize your conversations!

================================================ FILE: src/popup.ts ================================================ import { DEFAULT_USER_ID } from './types/api'; import { SidebarAction } from './types/messages'; import { StorageKey } from './types/storage'; document.addEventListener('DOMContentLoaded', () => { const googleSignInButton = document.getElementById('googleSignInButton') as HTMLButtonElement; const checkAuth = (): void => { chrome.storage.sync.get([StorageKey.API_KEY, StorageKey.ACCESS_TOKEN], data => { if (data[StorageKey.API_KEY] || data[StorageKey.ACCESS_TOKEN]) { chrome.tabs.query({ active: true, currentWindow: true }, tabs => { const tabId = tabs[0]?.id; if (tabId !== null && tabId !== undefined) { chrome.tabs.sendMessage(tabId, { action: SidebarAction.TOGGLE_SIDEBAR }); } window.close(); }); } }); }; if (googleSignInButton) { googleSignInButton.addEventListener('click', () => { chrome.storage.sync.set({ [StorageKey.USER_ID_CAMEL]: DEFAULT_USER_ID }); chrome.storage.sync.get([StorageKey.USER_LOGGED_IN], data => { const url = data[StorageKey.USER_LOGGED_IN] ? 'https://app.mem0.ai/extension?source=chrome-extension' : 'https://app.mem0.ai/login?source=chrome-extension'; chrome.tabs.create({ url }, () => { window.close(); }); }); }); } checkAuth(); }); ================================================ FILE: src/replit/content.ts ================================================ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable no-inner-declarations */ import { MessageRole } from '../types/api'; import type { ExtendedHTMLElement } from '../types/dom'; import type { MemoryItem, MemorySearchItem, OptionalApiParams } from '../types/memory'; import { SidebarAction } from '../types/messages'; import { type StorageItems, StorageKey } from '../types/storage'; import { createOrchestrator, type SearchStorage } from '../utils/background_search'; import { OPENMEMORY_PROMPTS } from '../utils/llm_prompts'; import { SITE_CONFIG } from '../utils/site_config'; import { getBrowser, sendExtensionEvent } from '../utils/util_functions'; import { OPENMEMORY_UI, type Placement } from '../utils/util_positioning'; // Local types for this file type MutableMutationObserver = MutationObserver & { memoryStateInterval?: ReturnType; debounceTimer?: ReturnType; }; export {}; try { let isProcessingMem0: boolean = false; let memoryModalShown: boolean = false; // Global variable to store all memories let allMemories: string[] = []; // Track added memories by ID const allMemoriesById: Set = new Set(); // Reference to the modal overlay for updates let currentModalOverlay: HTMLDivElement | null = null; let inputObserver: MutationObserver | null = null; let lastInputValue: string = ''; // Global flags to prevent duplicate initialization let isInitialized: boolean = false; // let buttonInjected: boolean = false; let sendListenerAdded: boolean = false; // Store references to observers for cleanup let mainObserver: MutableMutationObserver | null = null; let notificationObserver: MutationObserver | null = null; const replitSearch = createOrchestrator({ fetch: async function (query: string, opts: { signal?: AbortSignal }) { const data = await new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, StorageKey.SIMILARITY_THRESHOLD, StorageKey.TOP_K, ], function (items) { resolve(items as SearchStorage); } ); }); const apiKey = data[StorageKey.API_KEY]; const accessToken = data[StorageKey.ACCESS_TOKEN]; if (!apiKey && !accessToken) { return []; } const authHeader = accessToken ? `Bearer ${accessToken}` : `Token ${apiKey}`; const userId = data[StorageKey.USER_ID_CAMEL] || data[StorageKey.USER_ID] || 'chrome-extension-user'; const threshold = data[StorageKey.SIMILARITY_THRESHOLD] !== undefined ? data[StorageKey.SIMILARITY_THRESHOLD] : 0.1; const topK = data[StorageKey.TOP_K] !== undefined ? data[StorageKey.TOP_K] : 10; const optionalParams: OptionalApiParams = {}; if (data[StorageKey.SELECTED_ORG]) { optionalParams.org_id = data[StorageKey.SELECTED_ORG]; } if (data[StorageKey.SELECTED_PROJECT]) { optionalParams.project_id = data[StorageKey.SELECTED_PROJECT]; } const payload = { query, filters: { user_id: userId }, rerank: true, threshold: threshold, top_k: topK, filter_memories: false, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }; const res = await fetch('https://api.mem0.ai/v2/memories/search/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify(payload), signal: opts && opts.signal, }); if (!res.ok) { throw new Error(`API request failed with status ${res.status}`); } return await res.json(); }, // Don’t render on prefetch. When modal is open, update it. onSuccess: function (normQuery: string, responseData: MemorySearchItem[]) { if (!memoryModalShown) { return; } const memoryItems = ((responseData as MemorySearchItem[]) || []).map( (item: MemorySearchItem) => ({ id: String(item.id), text: item.memory, categories: item.categories || [], }) ); createMemoryModal(memoryItems, false); }, onError: function () { if (memoryModalShown) { createMemoryModal([], false); } }, minLength: 3, debounceMs: 75, cacheTTL: 60000, }); let replitBackgroundSearchHandler: (() => void) | null = null; function hookReplitBackgroundSearchTyping() { const textarea = getTextarea(); if (!textarea) { return; } if (textarea.dataset.replitBackgroundHooked) { return; } textarea.dataset.replitBackgroundHooked = 'true'; if (!replitBackgroundSearchHandler) { replitBackgroundSearchHandler = function () { const text = (textarea.textContent || textarea.innerText || '').trim(); replitSearch.setText(text); }; } textarea.addEventListener('input', replitBackgroundSearchHandler); textarea.addEventListener('keyup', replitBackgroundSearchHandler); } function getTextarea(): HTMLElement | null { const selectors = [ 'div[contenteditable="true"][class="cm-content cm-lineWrapping"][role="textbox"]', 'div.cm-content.cm-lineWrapping[contenteditable="true"]', '.cm-content[contenteditable="true"]', 'div[contenteditable="true"].cm-content', 'div.cm-content[role="textbox"]', '.cm-content', 'div[contenteditable="true"]', '[contenteditable="true"]', ]; // First try our specific selectors for (const selector of selectors) { const textarea = document.querySelector(selector) as HTMLElement | null; if (textarea) { // If textarea is found but listeners haven't been set up, trigger a retry if (!sendListenerAdded) { setTimeout(() => { if (!sendListenerAdded) { addSendButtonListener(); } }, 500); } return textarea; } } return null; } function setupInputObserver(): void { // Don't set up if already exists if (inputObserver) { return; } const textarea = getTextarea(); if (!textarea) { // Only retry a limited number of times to prevent infinite recursion let retryCount = 0; const maxRetries = 10; const retrySetup = () => { if (retryCount < maxRetries) { retryCount++; setTimeout(setupInputObserver, 500); } }; retrySetup(); return; } // Set initial value lastInputValue = textarea.textContent || (textarea as ExtendedHTMLElement).innerText || ''; inputObserver = new MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.type === 'characterData' || mutation.type === 'childList') { const newValue = textarea.textContent || (textarea as ExtendedHTMLElement).innerText || ''; if (newValue !== lastInputValue) { lastInputValue = newValue; } } } }); inputObserver.observe(textarea, { childList: true, characterData: true, subtree: true, }); // Add input listener only once if (!textarea.dataset.mem0InputListener) { textarea.dataset.mem0InputListener = 'true'; textarea.addEventListener('input', function (this: HTMLElement) { const newValue = this.textContent || (this as ExtendedHTMLElement).innerText || ''; if (newValue !== lastInputValue) { lastInputValue = newValue; } }); } } function setInputValue( inputElement: HTMLElement | HTMLInputElement | HTMLTextAreaElement | null, value: string ): void { if (inputElement) { // For contenteditable divs, we need to set innerHTML or textContent if (inputElement.contentEditable === 'true') { // Clear existing content inputElement.innerHTML = ''; // Split the value by newlines and create div elements for Replit's CodeMirror const lines = value.split('\n'); lines.forEach((line: string, index: number) => { const div = document.createElement('div'); div.className = 'cm-line'; if (index === 0) { div.className += ' cm-replit-active-line'; } if (line.trim() === '') { div.innerHTML = '
'; } else { div.textContent = line; } inputElement.appendChild(div); }); lastInputValue = value; // Trigger input event inputElement.dispatchEvent(new Event('input', { bubbles: true })); // Focus and set cursor to end inputElement.focus(); // Set cursor to end of content const range = document.createRange(); const selection = window.getSelection(); range.selectNodeContents(inputElement); range.collapse(false); if (selection) { selection.removeAllRanges(); selection.addRange(range); } } else { // Fallback for regular input/textarea elements (inputElement as HTMLInputElement | HTMLTextAreaElement).value = value; lastInputValue = value; inputElement.dispatchEvent(new Event('input', { bubbles: true })); } } } // Function to get the content without any memory wrappers function getContentWithoutMemories(message: string | null = null): string { let content: string; if (message) { // Use provided message content = message; } else { // Fall back to reading from textarea const inputElement = getTextarea(); if (!inputElement) { return ''; } content = inputElement.textContent || (inputElement as ExtendedHTMLElement).innerText || ''; } // Remove any memory headers and content const memoryPrefix = OPENMEMORY_PROMPTS.memory_header_text; const prefixIndex = content.indexOf(memoryPrefix); if (prefixIndex !== -1) { content = content.substring(0, prefixIndex).trim(); } // Also try with regex pattern try { const MEM0_PLAIN = OPENMEMORY_PROMPTS.memory_header_plain_regex; content = content.replace(MEM0_PLAIN, '').trim(); } catch { // Ignore regex errors } return content; } // Function to check if memory is enabled function getMemoryEnabledState(): Promise { return new Promise(resolve => { chrome.storage.sync.get([StorageKey.MEMORY_ENABLED], function (result) { resolve(result.memory_enabled !== false); // Default to true if not set }); }); } // Track if memory has been captured for this session to prevent duplicates let memoryCaptured = false; let lastCapturedMessage = ''; // Add a function to handle send button actions and clear memories after sending function addSendButtonListener(): void { const selectors = [ 'button[data-cy="ai-prompt-submit"]', 'button[data-cy="ai-chat-send-button"]', 'button[aria-label="Send"]', 'button[type="button"][aria-label="Send"]', '.useView_view__C2mnv[aria-label="Send"]', 'button[type="submit"]', 'button:has(svg[data-testid="send"])', 'button:has([data-testid="send"])', ]; // Handle capturing and storing the current message function captureAndStoreMemory(): void { const textarea = getTextarea(); if (!textarea) { return; } // Get message from textarea first, then fall back to lastInputValue if textarea is empty let message = ( textarea.textContent || (textarea as ExtendedHTMLElement).innerText || '' ).trim(); // If textarea is empty (happens when Enter is pressed), use the stored value if (!message && lastInputValue) { message = lastInputValue.trim(); } if (!message) { return; } // Clean message from any existing memory content const cleanMessage = getContentWithoutMemories(message); // Prevent duplicate captures for the same message if (memoryCaptured && lastCapturedMessage === cleanMessage) { return; } memoryCaptured = true; lastCapturedMessage = cleanMessage; // Reset the capture flag after a short delay setTimeout(() => { memoryCaptured = false; lastCapturedMessage = ''; }, 1000); // Asynchronously store the memory chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.MEMORY_ENABLED, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, ], function (items) { // Skip if memory is disabled or no credentials if (items.memory_enabled === false || (!items.apiKey && !items.access_token)) { return; } const authHeader = items.access_token ? `Bearer ${items.access_token}` : `Token ${items.apiKey}`; const userId = items.userId || items.user_id || 'chrome-extension-user'; const optionalParams: OptionalApiParams = {}; if (items.selected_org) { optionalParams.org_id = items.selected_org; } if (items.selected_project) { optionalParams.project_id = items.selected_project; } // Send memory to mem0 API asynchronously without waiting for response fetch('https://api.mem0.ai/v1/memories/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify({ messages: [{ role: MessageRole.User, content: cleanMessage }], user_id: userId, infer: true, metadata: { provider: 'Replit', }, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }), }) .then(response => { return response.json(); }) .then(() => { // Memory saved successfully }) .catch(error => { console.error('[Mem0 Replit] Error saving memory:', error); }); } ); // Clear all memories after sending setTimeout(() => { allMemories = []; allMemoriesById.clear(); }, 100); } // Find and add listeners to the send button - check each time let sendButton: HTMLElement | null = null; let sendButtonFound = false; for (const selector of selectors) { sendButton = document.querySelector(selector); if (sendButton) { if (!sendButton.dataset.mem0Listener) { sendButton.dataset.mem0Listener = 'true'; sendButton.addEventListener('click', function () { captureAndStoreMemory(); }); sendButtonFound = true; break; } else { sendButtonFound = true; break; } } } // Handle textarea for Enter key press - check each time const textarea = getTextarea(); if (textarea) { if (!textarea.dataset.mem0KeyListener) { textarea.dataset.mem0KeyListener = 'true'; // Add keydown listener for Enter key textarea.addEventListener('keydown', function (event: KeyboardEvent) { // Update lastInputValue for non-control keys if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) { setTimeout(() => { lastInputValue = textarea.textContent || (textarea as ExtendedHTMLElement).innerText || ''; }, 0); } // Check if Enter was pressed without Shift (standard send behavior) if (event.key === 'Enter' && !event.shiftKey) { // Small delay to ensure content is captured before send setTimeout(() => { captureAndStoreMemory(); }, 10); } }); // Also add keyup listener as backup textarea.addEventListener('keyup', function () { lastInputValue = textarea.textContent || (textarea as ExtendedHTMLElement).innerText || ''; }); } } // Only set the flag if we successfully found and set up listeners if (sendButtonFound || textarea) { sendListenerAdded = true; } else { // Don't set the flag so we keep trying sendListenerAdded = false; } } // Handler for the modal approach async function handleMem0Modal() { // Prevent multiple simultaneous modals if (isProcessingMem0) { return; } // Clear only the modal display tracking, but keep the actual added memories // allMemoriesById tracks what memories have been shown in modals allMemoriesById.clear(); // Do NOT clear allMemories here - we want to keep previously added memories const memoryEnabled = await getMemoryEnabledState(); if (!memoryEnabled) { return; } // Check if user is logged in const loginData = await new Promise(resolve => { chrome.storage.sync.get( [StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN], function (items) { resolve(items as StorageItems); } ); }); // If no API key and no access token, show login popup if (!loginData.apiKey && !loginData.access_token) { showLoginPopup(); return; } const textarea = getTextarea(); let message = textarea ? (textarea.textContent || (textarea as ExtendedHTMLElement).innerText || '').trim() : ''; // If no message, show a popup and return if (!message) { // Show message that requires input const mem0Button = document.querySelector('button[aria-label="Mem0"]') as HTMLElement | null; if (mem0Button) { showButtonPopup(mem0Button, 'Please enter some text first'); } return; } // Clean the message of any existing memory content message = getContentWithoutMemories(); isProcessingMem0 = true; // Add a timeout to reset the flag if something goes wrong const timeoutId = setTimeout((): void => { isProcessingMem0 = false; }, 30000); // 30 second timeout // Show the loading modal immediately with the source button ID createMemoryModal([], true); try { const data = await new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.USER_ID, StorageKey.SIMILARITY_THRESHOLD, StorageKey.TOP_K, ], function (items) { resolve(items as StorageItems); } ); }); const apiKey = data[StorageKey.API_KEY]; const userId = (data[StorageKey.USER_ID_CAMEL] || data[StorageKey.USER_ID] || 'chrome-extension-user') as string; const accessToken = data[StorageKey.ACCESS_TOKEN]; const optionalParams: OptionalApiParams = {}; if (data[StorageKey.SELECTED_ORG]) { optionalParams.org_id = data[StorageKey.SELECTED_ORG]; } if (data[StorageKey.SELECTED_PROJECT]) { optionalParams.project_id = data[StorageKey.SELECTED_PROJECT]; } if (!apiKey && !accessToken) { isProcessingMem0 = false; return; } sendExtensionEvent('modal_clicked', { provider: 'replit', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), }); const authHeader = accessToken ? `Bearer ${accessToken}` : `Token ${apiKey}`; const messages = [{ role: MessageRole.User, content: message }]; // Use orchestrator immediate run replitSearch.runImmediate(message); // Proceed with adding memory asynchronously without awaiting fetch('https://api.mem0.ai/v1/memories/', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify({ messages: messages, user_id: userId, infer: true, metadata: { provider: 'Replit', }, source: 'OPENMEMORY_CHROME_EXTENSION', ...optionalParams, }), }).catch(error => { console.error('Error adding memory:', error); }); } catch (error) { console.error('Error:', error); // Still show the modal but with empty state if there was an error createMemoryModal([], false); } finally { clearTimeout(timeoutId); isProcessingMem0 = false; } } function initializeMem0Integration() { try { // Prevent duplicate initialization if (isInitialized) { return; } isInitialized = true; setupInputObserver(); try { hookReplitBackgroundSearchTyping(); } catch { // Ignore background search errors } if (OPENMEMORY_UI && OPENMEMORY_UI.mountOnEditorFocus) { try { if (!document.getElementById('mem0-icon-button')) { OPENMEMORY_UI.resolveCachedAnchor( { learnKey: location.host + ':' + location.pathname }, null, 24 * 60 * 60 * 1000 ).then(function (hit: { el: Element; placement: Placement | null } | null) { if (!hit || !hit.el) { return; } let hs = OPENMEMORY_UI.createShadowRootHost('mem0-root'); let host = hs.host, shadow = hs.shadow; host.id = 'mem0-icon-button'; let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.replit ? SITE_CONFIG.replit : null; let placement = hit.placement || (cfg && cfg.placement) || { strategy: 'float', placement: 'right-center', gap: 12 }; OPENMEMORY_UI.applyPlacement({ container: host, anchor: hit.el, placement: placement, }); let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:40px; height:40px; border-radius:50%; box-shadow: 0 6px 16px rgba(0,0,0,0.3); background:#1C1C1E; } .mem0-btn img { width:20px; height:20px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal(); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } }); } } catch (_) { // Ignore errors during re-initialization } OPENMEMORY_UI.mountOnEditorFocus({ existingHostSelector: '#mem0-icon-button', editorSelector: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.replit && SITE_CONFIG.replit.editorSelector ? SITE_CONFIG.replit.editorSelector : 'textarea, [contenteditable="true"], input[type="text"]', deriveAnchor: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.replit && typeof SITE_CONFIG.replit.deriveAnchor === 'function' ? SITE_CONFIG.replit.deriveAnchor : function (editor: Element) { return editor.closest('form') || editor.parentElement || document.body; }, placement: typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.replit && SITE_CONFIG.replit.placement ? SITE_CONFIG.replit.placement : { strategy: 'float', placement: 'right-center', gap: 12 }, render: function (shadow: ShadowRoot, host: HTMLElement) { host.id = 'mem0-icon-button'; let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:40px; height:40px; border-radius:50%; box-shadow: 0 6px 16px rgba(0,0,0,0.3); background:#1C1C1E; } .mem0-btn img { width:20px; height:20px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal(); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } }, fallback: function () { let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.replit ? SITE_CONFIG.replit : null; return OPENMEMORY_UI.mountResilient({ anchors: [ { find: function () { let sel = (cfg && cfg.editorSelector) || 'textarea, [contenteditable="true"], input[type="text"]'; let ed = document.querySelector(sel); if (!ed) { return null; } try { return cfg && typeof cfg.deriveAnchor === 'function' ? cfg.deriveAnchor(ed) : ed.closest('form') || ed.parentElement || document.body; } catch (_) { return ed.closest('form') || ed.parentElement || document.body; } }, }, ], placement: (cfg && cfg.placement) || { strategy: 'float', placement: 'right-center', gap: 12, }, enableFloatingFallback: true, render: function (shadow: ShadowRoot, host: HTMLElement) { host.id = 'mem0-icon-button'; let style = document.createElement('style'); style.textContent = ` :host { position: relative; } .mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:40px; height:40px; border-radius:50%; box-shadow: 0 6px 16px rgba(0,0,0,0.3); background:#1C1C1E; } .mem0-btn img { width:20px; height:20px; border-radius:50%; } .dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; } :host([data-has-text="1"]) .dot { display:block; } `; let btn = document.createElement('button'); btn.className = 'mem0-btn'; let img = document.createElement('img'); img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png'); let dot = document.createElement('div'); dot.className = 'dot'; btn.appendChild(img); shadow.append(style, btn, dot); btn.addEventListener('click', function () { handleMem0Modal(); }); if (typeof updateNotificationDot === 'function') { setTimeout(updateNotificationDot, 0); } }, }); }, }); } addSendButtonListener(); } catch (error) { console.error('[Mem0] Error during initialization:', error); isInitialized = false; // Reset so we can try again } // Set up a single, more efficient mutation observer if (mainObserver) { mainObserver.disconnect(); } mainObserver = new MutationObserver(async () => { // Debounce the observer to prevent excessive calls if (mainObserver && (mainObserver as MutableMutationObserver).debounceTimer) { clearTimeout((mainObserver as MutableMutationObserver).debounceTimer); } if (!mainObserver) { return; } (mainObserver as MutableMutationObserver).debounceTimer = setTimeout(async () => { // Check memory state first const memoryEnabled = await getMemoryEnabledState(); if (!memoryEnabled) { // Remove the button if memory is disabled const existingButton = document.querySelector('button[aria-label="Mem0"]'); if (existingButton && existingButton.parentElement) { existingButton.parentElement.remove(); // buttonInjected = false; } } // Add send button listener if not already added or if elements might have changed if (memoryEnabled && !sendListenerAdded) { addSendButtonListener(); } // Update notification dot updateNotificationDot(); }, 100); // Reduce debounce to 100ms for faster response }); // Add keyboard shortcut for Ctrl+M (only once) if (!document.body.dataset.mem0KeyboardListener) { document.body.dataset.mem0KeyboardListener = 'true'; document.addEventListener('keydown', function (event: KeyboardEvent) { if (event.ctrlKey && event.key === 'm') { event.preventDefault(); (async () => { await handleMem0Modal(); })(); } }); } // Observe with more specific targeting to reduce noise mainObserver.observe(document.body, { childList: true, subtree: true, // Only observe specific changes that matter attributeFilter: ['class', 'style'], }); // Replace periodic button injection checks; OPENMEMORY_UI handles mounting const memoryStateCheckInterval = setInterval(async () => { const memoryEnabled = await getMemoryEnabledState(); const buttonExists = document.querySelector('button[aria-label="Mem0"]'); if (!memoryEnabled && buttonExists) { buttonExists.parentElement?.remove(); // buttonInjected = false; } }, 10000); // Store interval reference for cleanup if needed if (mainObserver) { (mainObserver as MutableMutationObserver).memoryStateInterval = memoryStateCheckInterval; } } // Initialize the integration when the page loads if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { initializeMem0Integration(); }); } else { initializeMem0Integration(); } // Also try to initialize after a delay as a fallback setTimeout(() => { if (!isInitialized) { initializeMem0Integration(); } }, 2000); // Add another retry after 5 seconds setTimeout(() => { return; }, 5000); // Shared function to update the input field with all collected memories function updateInputWithMemories() { const inputElement = getTextarea(); if (inputElement && allMemories.length > 0) { // Get the content without any existing memory wrappers const baseContent = getContentWithoutMemories(); // Create the memory string with all collected memories let memoriesContent = '\n\n' + OPENMEMORY_PROMPTS.memory_header_text + '\n'; // Add all memories to the content allMemories.forEach((mem, index) => { memoriesContent += `- ${mem}`; if (index < allMemories.length - 1) { memoriesContent += '\n'; } }); // Add the final content to the input setInputValue(inputElement, baseContent + memoriesContent); } } // Function to show a small popup message near the button function showButtonPopup(button: HTMLElement, message: string): void { // Remove any existing popups const existingPopup = document.querySelector('.mem0-button-popup'); if (existingPopup) { existingPopup.remove(); } const popup = document.createElement('div'); popup.className = 'mem0-button-popup'; popup.style.cssText = ` position: absolute; top: -40px; left: 50%; transform: translateX(-50%); background-color: #2d2e30; border: 1px solid #5f6368; color: white; padding: 8px 12px; border-radius: 6px; font-size: 12px; white-space: nowrap; z-index: 10001; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; popup.textContent = message; // Create arrow const arrow = document.createElement('div'); arrow.style.cssText = ` position: absolute; bottom: -5px; left: 50%; transform: translateX(-50%) rotate(45deg); width: 8px; height: 8px; background-color: #2d2e30; border-right: 1px solid #5f6368; border-bottom: 1px solid #5f6368; `; popup.appendChild(arrow); // Position relative to button button.style.position = 'relative'; button.appendChild(popup); // Auto-remove after 3 seconds setTimeout(() => { if (popup && popup.parentElement) { popup.remove(); } }, 3000); } // Function to show login popup function showLoginPopup() { // First remove any existing popups const existingPopup = document.querySelector('#mem0-login-popup'); if (existingPopup) { existingPopup.remove(); } // Create popup container const popupOverlay = document.createElement('div'); popupOverlay.id = 'mem0-login-popup'; popupOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10001; `; const popupContainer = document.createElement('div'); popupContainer.style.cssText = ` background-color: #2d2e30; border-radius: 12px; width: 320px; padding: 24px; color: white; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; // Close button const closeButton = document.createElement('button'); closeButton.style.cssText = ` position: absolute; top: 16px; right: 16px; background: none; border: none; color: #9aa0a6; font-size: 16px; cursor: pointer; `; closeButton.innerHTML = '×'; closeButton.addEventListener('click', () => { document.body.removeChild(popupOverlay); }); // Logo and heading const logoContainer = document.createElement('div'); logoContainer.style.cssText = ` display: flex; align-items: center; justify-content: center; margin-bottom: 16px; `; const heading = document.createElement('h2'); heading.textContent = 'Sign in to OpenMemory'; heading.style.cssText = ` margin: 0; font-size: 18px; font-weight: 600; `; logoContainer.appendChild(heading); // Message const message = document.createElement('p'); message.textContent = 'Please sign in to access your memories and enhance your conversations!'; message.style.cssText = ` margin-bottom: 24px; color: #e8eaed; font-size: 14px; line-height: 1.5; text-align: center; `; // Sign in button const signInButton = document.createElement('button'); signInButton.style.cssText = ` display: flex; align-items: center; justify-content: center; width: 100%; padding: 10px; background-color: #1a73e8; color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: background-color 0.2s; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; gap: 8px; `; // Add logo and text const logoDark = document.createElement('img'); logoDark.src = chrome.runtime.getURL('icons/mem0-claude-icon.png'); logoDark.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; `; const signInText = document.createElement('span'); signInText.textContent = 'Sign in with OpenMemory'; signInButton.appendChild(logoDark); signInButton.appendChild(signInText); signInButton.addEventListener('mouseenter', () => { signInButton.style.backgroundColor = '#1557b0'; }); signInButton.addEventListener('mouseleave', () => { signInButton.style.backgroundColor = '#1a73e8'; }); // Open sign-in page when clicked signInButton.addEventListener('click', () => { window.open('https://app.mem0.ai/login', '_blank'); document.body.removeChild(popupOverlay); }); // Assemble popup popupContainer.appendChild(logoContainer); popupContainer.appendChild(message); popupContainer.appendChild(signInButton); popupOverlay.appendChild(popupContainer); popupOverlay.appendChild(closeButton); // Add click event to close when clicking outside popupOverlay.addEventListener('click', e => { if (e.target === popupOverlay) { document.body.removeChild(popupOverlay); } }); // Add to body document.body.appendChild(popupOverlay); } function createMemoryModal(memoryItems: MemoryItem[], isLoading: boolean = false) { // Preserve current modal position if it exists let preservedPosition: { top: number; left: number } | null = null; if (memoryModalShown && currentModalOverlay) { const existingModal = currentModalOverlay.querySelector('div[style*="position: absolute"]'); if (existingModal) { preservedPosition = { top: parseInt(existingModal.style.top) || 0, left: parseInt(existingModal.style.left) || 0, }; } document.body.removeChild(currentModalOverlay); } memoryModalShown = true; let currentMemoryIndex = 0; // Calculate modal dimensions (estimated) const modalWidth = 447; let modalHeight = 400; // Default height let memoriesPerPage = 3; // Default number of memories per page let topPosition; let leftPosition; // Use preserved position if available, otherwise calculate new position if (preservedPosition) { topPosition = preservedPosition.top; leftPosition = preservedPosition.left; // Ensure the preserved position is still within viewport bounds const maxX = window.innerWidth - modalWidth; const maxY = window.innerHeight - modalHeight; leftPosition = Math.max(0, Math.min(leftPosition, maxX)); topPosition = Math.max(0, Math.min(topPosition, maxY)); } else { // Position relative to the Mem0 button (original logic) const mem0Button = document.querySelector('#mem0-icon-button'); if (mem0Button) { const buttonRect = mem0Button.getBoundingClientRect(); // Determine if there's enough space below the button const viewportHeight = window.innerHeight; const spaceBelow = viewportHeight - buttonRect.bottom; // Position the modal centered under the button leftPosition = Math.max(buttonRect.left + buttonRect.width / 2 - modalWidth / 2, 10); // Ensure the modal doesn't go off the right edge of the screen const rightEdgePosition = leftPosition + modalWidth; if (rightEdgePosition > window.innerWidth - 10) { leftPosition = window.innerWidth - modalWidth - 10; } if (spaceBelow >= modalHeight) { // Place below the button topPosition = buttonRect.bottom + 10; } else { // Place above the button if not enough space below topPosition = buttonRect.top - modalHeight - 10; // Check if it's in the upper half of the screen if (buttonRect.top < viewportHeight / 2) { modalHeight = 300; // Reduced height memoriesPerPage = 2; // Show only 2 memories } } } else { // Fallback positioning topPosition = 100; leftPosition = window.innerWidth / 2 - modalWidth / 2; } } // Create modal overlay const modalOverlay = document.createElement('div'); modalOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: transparent; display: flex; z-index: 10000; pointer-events: auto; `; // Save reference to current modal overlay currentModalOverlay = modalOverlay; // Add event listener to close modal when clicking outside modalOverlay.addEventListener('click', event => { // Only close if clicking directly on the overlay, not its children if (event.target === modalOverlay) { closeModal(); } }); // Create modal container with positioning const modalContainer = document.createElement('div'); modalContainer.style.cssText = ` background-color: #2d2e30; border-radius: 12px; width: ${modalWidth}px; height: ${modalHeight}px; display: flex; flex-direction: column; color: white; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); position: absolute; top: ${topPosition}px; left: ${leftPosition}px; pointer-events: auto; border: 1px solid #5f6368; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; overflow: hidden; cursor: move; `; // Add drag functionality let isDragging = false; let dragStartX = 0; let dragStartY = 0; let modalStartX = 0; let modalStartY = 0; function startDrag(e: MouseEvent) { // Only allow dragging from the header area const target = e.target as HTMLElement | null; if ((target && target.closest('.mem0-modal-header')) || e.target === modalContainer) { isDragging = true; dragStartX = e.clientX; dragStartY = e.clientY; modalStartX = parseInt(modalContainer.style.left); modalStartY = parseInt(modalContainer.style.top); modalContainer.style.cursor = 'grabbing'; e.preventDefault(); } } function doDrag(e: MouseEvent) { if (!isDragging) { return; } const deltaX = e.clientX - dragStartX; const deltaY = e.clientY - dragStartY; let newX = modalStartX + deltaX; let newY = modalStartY + deltaY; // Constrain to viewport const maxX = window.innerWidth - modalWidth; const maxY = window.innerHeight - modalHeight; newX = Math.max(0, Math.min(newX, maxX)); newY = Math.max(0, Math.min(newY, maxY)); modalContainer.style.left = newX + 'px'; modalContainer.style.top = newY + 'px'; } function stopDrag() { isDragging = false; modalContainer.style.cursor = 'move'; } modalContainer.addEventListener('mousedown', startDrag); document.addEventListener('mousemove', doDrag); document.addEventListener('mouseup', stopDrag); // Create modal header const modalHeader = document.createElement('div'); modalHeader.className = 'mem0-modal-header'; modalHeader.style.cssText = ` display: flex; align-items: center; padding: 10px 16px; justify-content: space-between; background-color: #35373a; flex-shrink: 0; cursor: grab; `; modalHeader.addEventListener('mousedown', () => { modalHeader.style.cursor = 'grabbing'; }); modalHeader.addEventListener('mouseup', () => { modalHeader.style.cursor = 'grab'; }); // Create header left section with just the logo const headerLeft = document.createElement('div'); headerLeft.style.cssText = ` display: flex; flex-direction: row; align-items: center; `; // Add Mem0 logo const logoImg = document.createElement('img'); logoImg.src = chrome.runtime.getURL('icons/mem0-claude-icon.png'); logoImg.style.cssText = ` width: 26px; height: 26px; border-radius: 50%; `; // Add "OpenMemory" title const title = document.createElement('div'); title.textContent = 'OpenMemory'; title.style.cssText = ` font-size: 16px; font-weight: 600; color: white; margin-left: 8px; `; // Create header right section const headerRight = document.createElement('div'); headerRight.style.cssText = ` display: flex; flex-direction: row; align-items: center; gap: 8px; `; // Create Add to Prompt button with arrow const addToPromptBtn = document.createElement('button'); addToPromptBtn.style.cssText = ` display: flex; flex-direction: row; align-items: center; padding: 5px 16px; gap: 8px; background-color:rgb(27, 27, 27); border: none; border-radius: 8px; cursor: pointer; font-size: 12px; font-weight: 600; color: white; transition: background-color 0.2s; `; addToPromptBtn.textContent = 'Add to Prompt'; // Add arrow icon to button const arrowIcon = document.createElement('span'); arrowIcon.innerHTML = ` `; arrowIcon.style.position = 'relative'; arrowIcon.style.top = '2px'; addToPromptBtn.appendChild(arrowIcon); // Add hover effect for Add to Prompt button addToPromptBtn.addEventListener('mouseenter', () => { addToPromptBtn.style.backgroundColor = 'rgb(36, 36, 36)'; }); addToPromptBtn.addEventListener('mouseleave', () => { addToPromptBtn.style.backgroundColor = 'rgb(27, 27, 27)'; }); // Create settings button const settingsBtn = document.createElement('button'); settingsBtn.style.cssText = ` background: none; border: none; cursor: pointer; padding: 8px; opacity: 0.6; transition: opacity 0.2s; `; settingsBtn.innerHTML = ` `; // Add click event to open app.mem0.ai in a new tab settingsBtn.addEventListener('click', () => { if (currentModalOverlay && document.body.contains(currentModalOverlay)) { document.body.removeChild(currentModalOverlay); memoryModalShown = false; currentModalOverlay = null; } chrome.runtime.sendMessage({ action: SidebarAction.SIDEBAR_SETTINGS }); }); // Add hover effect for the settings button settingsBtn.addEventListener('mouseenter', () => { settingsBtn.style.opacity = '1'; }); settingsBtn.addEventListener('mouseleave', () => { settingsBtn.style.opacity = '0.6'; }); // Content section const contentSection = document.createElement('div'); const contentSectionHeight = modalHeight - 130; // Account for header and navigation contentSection.style.cssText = ` display: flex; flex-direction: column; padding: 0 16px; gap: 12px; overflow: hidden; flex: 1; height: ${contentSectionHeight}px; `; // Create memories counter const memoriesCounter = document.createElement('div'); memoriesCounter.style.cssText = ` font-size: 16px; font-weight: 600; color: #FFFFFF; margin-top: 16px; flex-shrink: 0; `; // Update counter text based on loading state and number of memories if (isLoading) { memoriesCounter.textContent = `Loading Relevant Memories...`; } else { // Filter out memories that have already been added for accurate count const availableMemoriesCount = memoryItems.filter( (memory: MemoryItem) => memory && memory.id && !allMemoriesById.has(memory.id) ).length; memoriesCounter.textContent = `${availableMemoriesCount} Relevant Memories`; } // Calculate max height for memories content based on modal height const memoriesContentMaxHeight = contentSectionHeight - 40; // Account for memories counter // Create memories content container with adjusted height const memoriesContent = document.createElement('div'); memoriesContent.style.cssText = ` display: flex; flex-direction: column; gap: 8px; overflow-y: auto; flex: 1; max-height: ${memoriesContentMaxHeight}px; padding-right: 8px; margin-right: -8px; scrollbar-width: thin; scrollbar-color: #5f6368 transparent; `; // Track currently expanded memory let currentlyExpandedMemory: HTMLElement | null = null; // Function to create skeleton loading items (adjusted for different heights) function createSkeletonItems() { memoriesContent.innerHTML = ''; for (let i = 0; i < memoriesPerPage; i++) { const skeletonItem = document.createElement('div'); skeletonItem.style.cssText = ` display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; padding: 12px; background-color: #3c4043; border-radius: 8px; height: 72px; flex-shrink: 0; animation: pulse 1.5s infinite ease-in-out; `; const skeletonText = document.createElement('div'); skeletonText.style.cssText = ` background-color: #5f6368; border-radius: 4px; height: 14px; width: 85%; margin-bottom: 8px; `; const skeletonText2 = document.createElement('div'); skeletonText2.style.cssText = ` background-color: #5f6368; border-radius: 4px; height: 14px; width: 65%; `; const skeletonActions = document.createElement('div'); skeletonActions.style.cssText = ` display: flex; gap: 4px; margin-left: 10px; `; const skeletonButton1 = document.createElement('div'); skeletonButton1.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; background-color: #5f6368; `; const skeletonButton2 = document.createElement('div'); skeletonButton2.style.cssText = ` width: 20px; height: 20px; border-radius: 50%; background-color: #5f6368; `; skeletonActions.appendChild(skeletonButton1); skeletonActions.appendChild(skeletonButton2); const textContainer = document.createElement('div'); textContainer.style.cssText = ` display: flex; flex-direction: column; flex-grow: 1; `; textContainer.appendChild(skeletonText); textContainer.appendChild(skeletonText2); skeletonItem.appendChild(textContainer); skeletonItem.appendChild(skeletonActions); memoriesContent.appendChild(skeletonItem); } // Add keyframe animation to document if not exists if (!document.getElementById('skeleton-animation')) { const style = document.createElement('style'); style.id = 'skeleton-animation'; style.innerHTML = ` @keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 0.8; } 100% { opacity: 0.6; } } `; document.head.appendChild(style); } } // Function to show empty state function showEmptyState() { memoriesContent.innerHTML = ''; const emptyContainer = document.createElement('div'); emptyContainer.style.cssText = ` display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 32px 16px; text-align: center; flex: 1; min-height: 200px; `; const emptyIcon = document.createElement('div'); emptyIcon.innerHTML = ` `; emptyIcon.style.marginBottom = '16px'; const emptyText = document.createElement('div'); emptyText.textContent = 'No relevant memories found'; emptyText.style.cssText = ` color: #9aa0a6; font-size: 14px; font-weight: 500; `; emptyContainer.appendChild(emptyIcon); emptyContainer.appendChild(emptyText); memoriesContent.appendChild(emptyContainer); } // Add content to modal contentSection.appendChild(memoriesCounter); contentSection.appendChild(memoriesContent); // Navigation section at bottom const navigationSection = document.createElement('div'); navigationSection.style.cssText = ` display: flex; justify-content: center; gap: 12px; padding: 10px; border-top: none; flex-shrink: 0; `; // Navigation buttons const prevButton = document.createElement('button'); prevButton.innerHTML = ` `; prevButton.style.cssText = ` background: #3c4043; border: none; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.2s; `; const nextButton = document.createElement('button'); nextButton.innerHTML = ` `; nextButton.style.cssText = prevButton.style.cssText; navigationSection.appendChild(prevButton); navigationSection.appendChild(nextButton); // Assemble modal headerLeft.appendChild(logoImg); headerLeft.appendChild(title); headerRight.appendChild(addToPromptBtn); headerRight.appendChild(settingsBtn); modalHeader.appendChild(headerLeft); modalHeader.appendChild(headerRight); modalContainer.appendChild(modalHeader); modalContainer.appendChild(contentSection); modalContainer.appendChild(navigationSection); modalOverlay.appendChild(modalContainer); // Append to body document.body.appendChild(modalOverlay); // Function to show memories with adjusted count based on modal position function showMemories() { memoriesContent.innerHTML = ''; if (isLoading) { createSkeletonItems(); return; } // Filter out memories that have already been added const availableMemories = memoryItems.filter((memory: MemoryItem) => { const hasId = memory && typeof memory.id === 'string'; const isAlreadyAdded = hasId && allMemoriesById.has(memory.id as string); return hasId && !isAlreadyAdded; }); // Update counter with actual available memories count memoriesCounter.textContent = isLoading ? 'Loading Relevant Memories...' : `${availableMemories.length} Relevant Memories`; if (availableMemories.length === 0) { showEmptyState(); updateNavigationState(0, 0); return; } // Use the dynamically set memoriesPerPage value const memoriesToShow = Math.min(memoriesPerPage, availableMemories.length); // Calculate total pages and current page based on available memories const totalPages = Math.ceil(availableMemories.length / memoriesToShow); const currentPage = Math.floor(currentMemoryIndex / memoriesToShow) + 1; // Adjust currentMemoryIndex if it exceeds available memories if (currentMemoryIndex >= availableMemories.length) { currentMemoryIndex = Math.max(0, availableMemories.length - memoriesToShow); } // Update navigation buttons state updateNavigationState(currentPage, totalPages); for (let i = 0; i < memoriesToShow; i++) { const memoryIndex = currentMemoryIndex + i; if (memoryIndex >= availableMemories.length) { break; } // Stop if we've reached the end const memory = availableMemories[memoryIndex]!; // Ensure memory has an ID if (!memory.id) { memory.id = `memory-${Date.now()}-${memoryIndex}`; } const memoryContainer = document.createElement('div'); memoryContainer.style.cssText = ` display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; padding: 12px; background-color: #3c4043; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; min-height: 68px; max-height: 68px; overflow: hidden; flex-shrink: 0; `; const memoryText = document.createElement('div'); memoryText.style.cssText = ` font-size: 14px; line-height: 1.5; color: #e8eaed; flex-grow: 1; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; transition: all 0.2s ease; height: 42px; /* Height for 2 lines of text */ `; memoryText.textContent = String(memory.text || ''); const actionsContainer = document.createElement('div'); actionsContainer.style.cssText = ` display: flex; gap: 4px; margin-left: 10px; flex-shrink: 0; `; // Add button const addButton = document.createElement('button'); addButton.style.cssText = ` border: none; cursor: pointer; padding: 4px; background: #5f6368; color: #e8eaed; border-radius: 100%; width: 28px; height: 28px; transition: all 0.2s ease; `; addButton.innerHTML = ` `; // Add hover effect for add button addButton.addEventListener('mouseenter', () => { addButton.style.backgroundColor = 'rgb(36, 36, 36)'; }); addButton.addEventListener('mouseleave', () => { addButton.style.backgroundColor = '#5f6368'; }); // Add click handler for add button addButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); sendExtensionEvent('memory_injection', { provider: 'replit', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), injected_all: false, memory_id: memory.id, }); // Add this memory allMemoriesById.add(String(memory.id)); allMemories.push(String(memory.text || '')); updateInputWithMemories(); // Refresh the memories display (no need to remove from memoryItems) showMemories(); }); // Menu button const menuButton = document.createElement('button'); menuButton.style.cssText = ` background: none; border: none; cursor: pointer; padding: 4px; color: #9aa0a6; `; menuButton.innerHTML = ` `; // Track expanded state let isExpanded = false; // Create remove button (hidden by default) const removeButton = document.createElement('button'); removeButton.style.cssText = ` display: none; align-items: center; gap: 6px; background: #5f6368; color: #e8eaed; border-radius: 8px; padding: 2px 4px; border: none; cursor: pointer; font-size: 13px; margin-top: 12px; width: fit-content; transition: background-color 0.2s; `; removeButton.innerHTML = ` Remove `; // Add hover effect for remove button removeButton.addEventListener('mouseenter', () => { removeButton.style.backgroundColor = '#ea4335'; }); removeButton.addEventListener('mouseleave', () => { removeButton.style.backgroundColor = '#5f6368'; }); // Create content wrapper for text and remove button const contentWrapper = document.createElement('div'); contentWrapper.style.cssText = ` display: flex; flex-direction: column; flex-grow: 1; `; contentWrapper.appendChild(memoryText); contentWrapper.appendChild(removeButton); // Function to expand memory function expandMemory() { if (currentlyExpandedMemory && currentlyExpandedMemory !== memoryContainer) { currentlyExpandedMemory.dispatchEvent(new Event('collapse')); } isExpanded = true; memoryText.style.webkitLineClamp = 'unset'; memoryText.style.height = 'auto'; contentWrapper.style.overflowY = 'auto'; contentWrapper.style.maxHeight = '240px'; // Limit height to prevent overflow contentWrapper.style.scrollbarWidth = 'thin'; contentWrapper.style.scrollbarColor = '#5f6368 transparent'; memoryContainer.style.backgroundColor = '#2d2e30'; memoryContainer.style.maxHeight = '300px'; // Allow expansion but within container memoryContainer.style.overflow = 'hidden'; removeButton.style.display = 'flex'; currentlyExpandedMemory = memoryContainer; // Scroll to make expanded memory visible if needed memoriesContent.scrollTop = memoryContainer.offsetTop - memoriesContent.offsetTop; } // Function to collapse memory function collapseMemory() { isExpanded = false; memoryText.style.webkitLineClamp = '2'; memoryText.style.height = '42px'; contentWrapper.style.overflowY = 'visible'; memoryContainer.style.backgroundColor = '#3c4043'; memoryContainer.style.maxHeight = '72px'; memoryContainer.style.overflow = 'hidden'; removeButton.style.display = 'none'; currentlyExpandedMemory = null; } memoryContainer.addEventListener('collapse', collapseMemory); menuButton.addEventListener('click', e => { e.stopPropagation(); if (isExpanded) { collapseMemory(); } else { expandMemory(); } }); // Add click handler for remove button removeButton.addEventListener('click', (e: MouseEvent) => { e.stopPropagation(); // Remove from memoryItems const index = memoryItems.findIndex(m => m.id === memory.id); if (index !== -1) { memoryItems.splice(index, 1); // Refresh the memories display showMemories(); } }); actionsContainer.appendChild(addButton); actionsContainer.appendChild(menuButton); memoryContainer.appendChild(contentWrapper); memoryContainer.appendChild(actionsContainer); memoriesContent.appendChild(memoryContainer); // Add hover effect memoryContainer.addEventListener('mouseenter', () => { memoryContainer.style.backgroundColor = isExpanded ? '#25272a' : '#484b4f'; }); memoryContainer.addEventListener('mouseleave', () => { memoryContainer.style.backgroundColor = isExpanded ? '#2d2e30' : '#3c4043'; }); } // If after filtering for already added memories, there are no items to show, // check if we need to go to previous page if (memoriesContent.children.length === 0 && availableMemories.length > 0) { if (currentMemoryIndex > 0) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); showMemories(); } else { showEmptyState(); } } } // Add navigation button handlers prevButton.addEventListener('click', () => { if (currentMemoryIndex >= memoriesPerPage) { currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage); showMemories(); } }); nextButton.addEventListener('click', () => { const availableMemories = memoryItems.filter( (memory: MemoryItem) => !allMemoriesById.has(memory.id as string) ); if (currentMemoryIndex + memoriesPerPage < availableMemories.length) { currentMemoryIndex = currentMemoryIndex + memoriesPerPage; showMemories(); } }); // Add hover effects [prevButton, nextButton].forEach(button => { button.addEventListener('mouseenter', () => { if (!button.disabled) { button.style.backgroundColor = '#484b4f'; } }); button.addEventListener('mouseleave', () => { if (!button.disabled) { button.style.backgroundColor = '#3c4043'; } }); }); // Show initial memories showMemories(); // Update navigation button states function updateNavigationState(currentPage: number, totalPages: number) { if (memoryItems.length === 0 || totalPages === 0) { prevButton.disabled = true; prevButton.style.opacity = '0.5'; prevButton.style.cursor = 'not-allowed'; nextButton.disabled = true; nextButton.style.opacity = '0.5'; nextButton.style.cursor = 'not-allowed'; return; } if (currentPage <= 1) { prevButton.disabled = true; prevButton.style.opacity = '0.5'; prevButton.style.cursor = 'not-allowed'; } else { prevButton.disabled = false; prevButton.style.opacity = '1'; prevButton.style.cursor = 'pointer'; } if (currentPage >= totalPages) { nextButton.disabled = true; nextButton.style.opacity = '0.5'; nextButton.style.cursor = 'not-allowed'; } else { nextButton.disabled = false; nextButton.style.opacity = '1'; nextButton.style.cursor = 'pointer'; } } // Update Add to Prompt button click handler addToPromptBtn.addEventListener('click', () => { // Only add memories that are not already added const newMemories = memoryItems .filter((memory: MemoryItem) => !allMemoriesById.has(memory.id as string)) .map((memory: MemoryItem) => { allMemoriesById.add(memory.id as string); return memory.text; }); sendExtensionEvent('memory_injection', { provider: 'replit', source: 'OPENMEMORY_CHROME_EXTENSION', browser: getBrowser(), injected_all: true, memory_count: newMemories.length, }); // Add all new memories to allMemories allMemories.push(...newMemories); // Update the input with all memories if (allMemories.length > 0) { updateInputWithMemories(); closeModal(); } else { // If no new memories were added but we have existing ones, just close if (allMemoriesById.size > 0) { closeModal(); } } }); // Function to close the modal function closeModal() { if (currentModalOverlay && document.body.contains(currentModalOverlay)) { // Clean up drag event listeners document.removeEventListener('mousemove', doDrag); document.removeEventListener('mouseup', stopDrag); document.body.removeChild(currentModalOverlay); } currentModalOverlay = null; memoryModalShown = false; } } // function injectMem0Button() {} // Function to update notification dot visibility based on text in the input function updateNotificationDot() { const textarea = getTextarea(); const notificationDot = document.querySelector('#mem0-notification-dot'); if (!textarea || !notificationDot) { return; } // Prevent duplicate observers if (notificationObserver) { notificationObserver.disconnect(); } // Function to check if input has text const checkForText = () => { const inputText = textarea.textContent || textarea.innerText || ''; const hasText = inputText.trim() !== ''; if (hasText) { notificationDot.classList.add('active'); // Force display style notificationDot.style.display = 'block'; } else { notificationDot.classList.remove('active'); notificationDot.style.display = 'none'; } }; // Set up a single observer to watch for changes to the input field notificationObserver = new MutationObserver(checkForText); // Start observing the input element notificationObserver.observe(textarea, { characterData: true, subtree: true, childList: true, }); // Add event listeners only if not already added if (!textarea.dataset.mem0NotificationListener) { textarea.dataset.mem0NotificationListener = 'true'; textarea.addEventListener('input', checkForText); textarea.addEventListener('keyup', checkForText); textarea.addEventListener('focus', checkForText); } // Initial check checkForText(); } } catch (error) { console.error('[Mem0] Critical error in content script:', error); } ================================================ FILE: src/search_tracker.ts ================================================ import { DEFAULT_USER_ID } from './types/api'; import type { HistoryStateData, HistoryUrl } from './types/browser'; import type { Settings } from './types/settings'; import { StorageKey } from './types/storage'; (function () { // Utilities function normalize(text: string): string { return (text || '').replace(/\s+/g, ' ').trim(); } function getSettings(): Promise { return new Promise(resolve => { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.ACCESS_TOKEN, StorageKey.USER_ID, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.MEMORY_ENABLED, ], d => { resolve({ hasCreds: Boolean(d[StorageKey.API_KEY] || d[StorageKey.ACCESS_TOKEN]), apiKey: d[StorageKey.API_KEY], accessToken: d[StorageKey.ACCESS_TOKEN], userId: d[StorageKey.USER_ID] || DEFAULT_USER_ID, orgId: d[StorageKey.SELECTED_ORG], projectId: d[StorageKey.SELECTED_PROJECT], memoryEnabled: d[StorageKey.MEMORY_ENABLED] !== false, }); } ); }); } function maybeSend(engine: string, query: string): void { const q = normalize(query); if (!q || q.length < 2) { return; } getSettings().then(async settings => { if (!settings.hasCreds || settings.memoryEnabled === false) { return; } // Gate by track_searches toggle (default off if undefined) const allow = await new Promise(resolve => { try { chrome.storage.sync.get([StorageKey.TRACK_SEARCHES], d => { resolve(d[StorageKey.TRACK_SEARCHES] === true); }); } catch { resolve(false); } }); if (!allow) { return; } }); } // URL based capture for results pages function urlCapture(): void { const host = location.hostname || ''; const path = location.pathname || ''; const params = new URLSearchParams(location.search || ''); // Google results if (/(^|\.)google\./.test(host) && path.startsWith('/search')) { const q = params.get('q'); if (q) { maybeSend('Google', q); } return; } // Bing results if (host.endsWith('bing.com') && (path === '/search' || path === '/')) { const q = params.get('q'); if (q) { maybeSend('Bing', q); } return; } // Brave results if (host === 'search.brave.com' && (path === '/search' || path === '/images')) { const q = params.get('q'); if (q) { maybeSend('Brave', q); } return; } // Arc results if (host === 'search.arc.net' && (path === '/search' || path.startsWith('/search'))) { const q = params.get('q') || params.get('query'); if (q) { maybeSend('Arc', q); } return; } } function installSpaUrlWatcher(): void { const origPush = history.pushState.bind(history); const origReplace = history.replaceState.bind(history); const onUrlChange = () => { try { urlCapture(); } catch { return; } }; history.pushState = function (data: HistoryStateData, unused: string, url?: HistoryUrl) { origPush(data, unused, url); onUrlChange(); }; history.replaceState = function (data: HistoryStateData, unused: string, url?: HistoryUrl) { origReplace(data, unused, url); onUrlChange(); }; window.addEventListener('popstate', onUrlChange); } // Run urlCapture(); installSpaUrlWatcher(); })(); ================================================ FILE: src/selection_context.ts ================================================ import { MessageType, type SelectionContextMessage, type SelectionContextPayload, type SendResponse, ToastVariant, } from './types/messages'; (function () { chrome.runtime.onMessage.addListener( (msg: SelectionContextMessage, _sender, sendResponse: SendResponse) => { if (msg && msg.type === MessageType.GET_SELECTION_CONTEXT) { try { const payload: SelectionContextPayload = { selection: getSelectedText(), title: document.title || '', url: location.href, }; sendResponse({ type: MessageType.SELECTION_CONTEXT, payload }); } catch (e) { sendResponse({ type: MessageType.SELECTION_CONTEXT, error: String(e) }); } return true; } if (msg && msg.type === MessageType.TOAST) { const { message, variant = ToastVariant.SUCCESS } = msg.payload || {}; showToast(message || '', variant); } } ); function getSelectedText(): string { try { const sel = window.getSelection && window.getSelection(); const text = sel ? sel.toString().trim() : ''; return text; } catch { return ''; } } function showToast(message: string, variant: ToastVariant = ToastVariant.SUCCESS): void { try { const id = 'mem0-context-toast'; const existing = document.getElementById(id); if (existing) { existing.remove(); } const el = document.createElement('div'); el.id = id; el.textContent = message; el.style.cssText = ` position: fixed; top: 16px; right: 16px; z-index: 2147483647; background: ${variant === ToastVariant.ERROR ? '#7f1d1d' : '#14532d'}; color: #fff; padding: 10px 12px; border-radius: 8px; font-size: 13px; box-shadow: 0 6px 18px rgba(0,0,0,0.25); max-width: 360px; `; document.body.appendChild(el); setTimeout(() => { el.remove(); }, 2200); } catch { // no-op } } })(); ================================================ FILE: src/sidebar.ts ================================================ import { DEFAULT_USER_ID } from './types/api'; import type { MemoriesResponse, Memory } from './types/memory'; import { SidebarAction, type SidebarActionMessage } from './types/messages'; import type { Organization, Project } from './types/organizations'; import type { SidebarSettings } from './types/settings'; import { StorageKey } from './types/storage'; import { getBrowser, sendExtensionEvent } from './utils/util_functions'; (function () { let sidebarVisible = false; function initializeMem0Sidebar(): void { // Listen for messages from the extension chrome.runtime.onMessage.addListener( (request: SidebarActionMessage | { action: SidebarAction.SIDEBAR_SETTINGS }) => { if (request.action === SidebarAction.TOGGLE_SIDEBAR) { chrome.storage.sync.get([StorageKey.API_KEY, StorageKey.ACCESS_TOKEN], function (data) { if (data.apiKey || data.access_token) { toggleSidebar(); } else { chrome.runtime.sendMessage({ action: SidebarAction.OPEN_POPUP }); } }); } if (request.action === SidebarAction.SIDEBAR_SETTINGS) { chrome.storage.sync.get([StorageKey.API_KEY, StorageKey.ACCESS_TOKEN], function (data) { if (data[StorageKey.API_KEY] || data[StorageKey.ACCESS_TOKEN]) { toggleSidebar(); setTimeout(() => { const settingsTabButton = document.querySelector( '.tab-button[data-tab="settings"]' ); settingsTabButton?.click(); }, 200); } }); } return undefined; } ); } function toggleSidebar(): void { // Track extension usage when sidebar is toggled if (typeof sendExtensionEvent === 'function') { sendExtensionEvent('extension_browser_icon_clicked', { browser: getBrowser(), source: 'OPENMEMORY_CHROME_EXTENSION', tab_url: window.location.href, }); } const sidebar = document.getElementById('mem0-sidebar'); if (sidebar) { // If sidebar exists, toggle its visibility sidebarVisible = !sidebarVisible; sidebar.style.right = sidebarVisible ? '0px' : '-600px'; // Add or remove click listener based on sidebar visibility if (sidebarVisible) { document.addEventListener('click', handleOutsideClick); document.addEventListener('keydown', handleEscapeKey); fetchMemoriesAndCount(); } else { document.removeEventListener('click', handleOutsideClick); document.removeEventListener('keydown', handleEscapeKey); } } else { // If sidebar doesn't exist, create it createSidebar(); sidebarVisible = true; document.addEventListener('click', handleOutsideClick); document.addEventListener('keydown', handleEscapeKey); } } function handleEscapeKey(event: KeyboardEvent): void { if (event.key === 'Escape') { const searchInput = document.querySelector('.search-memory'); if (searchInput) { closeSearchInput(); } else { toggleSidebar(); } } } function handleOutsideClick(event: MouseEvent): void { const sidebar = document.getElementById('mem0-sidebar'); if ( sidebar && !sidebar.contains(event.target as Node) && !(event.target as HTMLElement)?.closest?.('.mem0-toggle-btn') ) { toggleSidebar(); } } function createSidebar(): void { if (document.getElementById('mem0-sidebar')) { return; } const sidebarContainer = document.createElement('div'); sidebarContainer.id = 'mem0-sidebar'; // Create fixed header const fixedHeader = document.createElement('div'); fixedHeader.className = 'fixed-header'; fixedHeader.innerHTML = `
OpenMemory Logo
`; // Create a container for search inputs const inputContainer = document.createElement('div'); inputContainer.className = 'input-container'; fixedHeader.appendChild(inputContainer); // Create tabs const tabsContainer = document.createElement('div'); tabsContainer.className = 'tabs-container'; tabsContainer.innerHTML = `
`; fixedHeader.appendChild(tabsContainer); sidebarContainer.appendChild(fixedHeader); // Create content container const contentContainer = document.createElement('div'); contentContainer.className = 'content'; // Create memory count display const memoryCountContainer = document.createElement('div'); memoryCountContainer.className = 'total-memories'; memoryCountContainer.innerHTML = `

Total Memories

Loading...

`; // Create memories tab content const memoriesTabContent = document.createElement('div'); memoriesTabContent.className = 'tab-content active'; memoriesTabContent.id = 'memories-tab'; memoriesTabContent.appendChild(memoryCountContainer); // Add memories section const memoriesSection = document.createElement('div'); memoriesSection.className = 'section'; memoriesSection.innerHTML = `

Recent Memories

`; memoriesTabContent.appendChild(memoriesSection); // Create settings tab content const settingsTabContent = document.createElement('div'); settingsTabContent.className = 'tab-content'; settingsTabContent.id = 'settings-tab'; // Move memory suggestions to settings tab const memoryToggleSection = document.createElement('div'); memoryToggleSection.className = 'section'; memoryToggleSection.innerHTML = `

Memory Suggestions

Get relevant memories suggested while interacting with AI Agents

`; settingsTabContent.appendChild(memoryToggleSection); // Track searches toggle section const trackSearchSection = document.createElement('div'); trackSearchSection.className = 'section'; trackSearchSection.innerHTML = `

Track searches

Save searches and typed URLs as memories

`; settingsTabContent.appendChild(trackSearchSection); // Add user ID input section const userIdSection = document.createElement('div'); userIdSection.className = 'section'; userIdSection.innerHTML = `

User ID

`; settingsTabContent.appendChild(userIdSection); // Add organization select section const orgSection = document.createElement('div'); orgSection.className = 'section'; orgSection.innerHTML = `

Organization

`; settingsTabContent.appendChild(orgSection); // Add project select section const projectSection = document.createElement('div'); projectSection.className = 'section'; projectSection.innerHTML = `

Project

`; settingsTabContent.appendChild(projectSection); // Add Auto-Inject toggle section const autoInjectSection = document.createElement('div'); autoInjectSection.className = 'section'; autoInjectSection.innerHTML = `

Enable Auto-Inject

Automatically inject relevant memories into conversations

`; // Disabling it for now as auto-inject is not working // settingsTabContent.appendChild(autoInjectSection); // Add threshold slider section const thresholdSection = document.createElement('div'); thresholdSection.className = 'section'; thresholdSection.innerHTML = `

Threshold

0.3

Set the minimum similarity score for memory suggestions

0 0.5 1
`; settingsTabContent.appendChild(thresholdSection); // Add top k section const topKSection = document.createElement('div'); topKSection.className = 'section'; topKSection.innerHTML = `

Top K

Maximum number of memories to suggest

`; settingsTabContent.appendChild(topKSection); // Add save button section const saveSection = document.createElement('div'); saveSection.className = 'section'; saveSection.innerHTML = ` `; settingsTabContent.appendChild(saveSection); contentContainer.appendChild(memoriesTabContent); contentContainer.appendChild(settingsTabContent); sidebarContainer.appendChild(contentContainer); // Create footer with shortcut and logout const footerToggle = document.createElement('div'); footerToggle.className = 'footer'; footerToggle.innerHTML = `
Shortcut : ^ + M
`; // Load saved settings chrome.storage.sync.get( [ StorageKey.MEMORY_ENABLED, StorageKey.USER_ID, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, StorageKey.AUTO_INJECT_ENABLED, StorageKey.SIMILARITY_THRESHOLD, StorageKey.TOP_K, StorageKey.TRACK_SEARCHES, ], function (result) { const toggleCheckbox = memoryToggleSection.querySelector('#mem0Toggle') as HTMLInputElement; if (toggleCheckbox) { toggleCheckbox.checked = result[StorageKey.MEMORY_ENABLED] !== false; } // Load track searches (default: disabled) const trackSearchesCheckbox = trackSearchSection.querySelector( '#trackSearchesToggle' ) as HTMLInputElement; if (trackSearchesCheckbox) { trackSearchesCheckbox.checked = result[StorageKey.TRACK_SEARCHES] === true; } const userIdInput = userIdSection.querySelector('#userIdInput') as HTMLInputElement; // Set saved value or keep default value if (result[StorageKey.USER_ID] && userIdInput) { userIdInput.value = result[StorageKey.USER_ID]; } // If no saved value, default is already set in HTML // Load auto-inject setting (default: enabled) const autoInjectCheckbox = autoInjectSection.querySelector( '#autoInjectToggle' ) as HTMLInputElement; if (autoInjectCheckbox) { autoInjectCheckbox.checked = result[StorageKey.AUTO_INJECT_ENABLED] !== false; } // Load threshold setting (default: 0.1) const thresholdSlider = thresholdSection.querySelector( '#thresholdSlider' ) as HTMLInputElement; const thresholdValue = thresholdSection.querySelector('.threshold-value') as HTMLElement; const threshold = result[StorageKey.SIMILARITY_THRESHOLD] !== undefined ? result[StorageKey.SIMILARITY_THRESHOLD] : 0.1; if (thresholdSlider) { thresholdSlider.value = String(threshold); } if (thresholdValue) { thresholdValue.textContent = Number(threshold).toFixed(1); } // Load top k setting (default: 10) const topKInput = topKSection.querySelector('#topKInput') as HTMLInputElement; const topK = result[StorageKey.TOP_K] !== undefined ? result[StorageKey.TOP_K] : 10; if (topKInput) { topKInput.value = String(topK); } } ); sidebarContainer.appendChild(footerToggle); // Add event listeners setupEventListeners( sidebarContainer, memoryToggleSection, userIdSection, orgSection, projectSection, autoInjectSection, thresholdSection, topKSection, saveSection, memoryCountContainer, footerToggle, trackSearchSection ); document.body.appendChild(sidebarContainer); // Slide in the sidebar immediately after creation setTimeout(() => { sidebarContainer.style.right = '0'; }, 0); // Prevent clicks within the sidebar from closing it sidebarContainer.addEventListener('click', event => { event.stopPropagation(); }); // Add styles addStyles(); // Fetch organizations and memories fetchOrganizations(); fetchMemoriesAndCount(); } function saveSettings( saveBtn: HTMLButtonElement, saveText: HTMLElement, saveLoader: HTMLElement, saveMessage: HTMLElement, userIdSection: HTMLElement, orgSection: HTMLElement, projectSection: HTMLElement, memoryToggleSection: HTMLElement, autoInjectSection: HTMLElement, thresholdSection: HTMLElement, topKSection: HTMLElement, trackSearchSection: HTMLElement ): void { // Show loading state saveBtn.disabled = true; saveText.style.display = 'none'; saveLoader.style.display = 'flex'; saveMessage.style.display = 'none'; // Get all the values const userIdInput = userIdSection.querySelector('#userIdInput') as HTMLInputElement; const orgSelect = orgSection.querySelector('#orgSelect') as HTMLSelectElement; const projectSelect = projectSection.querySelector('#projectSelect') as HTMLSelectElement; const toggleCheckbox = memoryToggleSection.querySelector('#mem0Toggle') as HTMLInputElement; const autoInjectCheckbox = autoInjectSection.querySelector( '#autoInjectToggle' ) as HTMLInputElement; const thresholdSlider = thresholdSection.querySelector('#thresholdSlider') as HTMLInputElement; const topKInput = topKSection.querySelector('#topKInput') as HTMLInputElement; const trackSearchesCheckbox = trackSearchSection.querySelector( '#trackSearchesToggle' ) as HTMLInputElement; const userId = (userIdInput?.value || '').trim(); const selectedOrgId = orgSelect?.value || ''; const selectedOrgName = orgSelect?.options[orgSelect.selectedIndex]?.text || ''; const selectedProjectId = projectSelect?.value || ''; const selectedProjectName = projectSelect?.options[projectSelect.selectedIndex]?.text || ''; const memoryEnabled = Boolean(toggleCheckbox?.checked); const autoInjectEnabled = Boolean(autoInjectCheckbox?.checked); const similarityThreshold = parseFloat(thresholdSlider?.value || '0.3'); const topK = parseInt(topKInput?.value || '10', 10); // Prepare settings object const settings: SidebarSettings = { user_id: userId || undefined, selected_org: selectedOrgId || undefined, selected_org_name: selectedOrgName || undefined, selected_project: selectedProjectId || undefined, selected_project_name: selectedProjectName || undefined, memory_enabled: memoryEnabled, auto_inject_enabled: autoInjectEnabled, similarity_threshold: similarityThreshold, top_k: topK, track_searches: Boolean(trackSearchesCheckbox?.checked), }; // Remove undefined values (Object.keys(settings) as Array).forEach(key => { if (settings[key] === undefined) { delete settings[key]; } }); // Save to chrome storage chrome.storage.sync.set(settings, function () { // Send toggle event to API chrome.storage.sync.get([StorageKey.API_KEY, StorageKey.ACCESS_TOKEN], function (data) { const headers = getHeaders(data.apiKey, data.access_token); fetch(`https://api.mem0.ai/v1/extension/`, { method: 'POST', headers: headers, body: JSON.stringify({ event_type: 'extension_toggle_button', additional_data: { status: memoryEnabled }, }), }).catch(error => { console.error('Error sending toggle event:', error); }); }); // Send message to runtime chrome.runtime.sendMessage({ action: SidebarAction.TOGGLE_MEM0, enabled: memoryEnabled, }); // Show success message setTimeout(() => { saveBtn.disabled = false; saveText.style.display = 'inline'; saveLoader.style.display = 'none'; saveMessage.style.display = 'block'; saveMessage.className = 'save-message success'; saveMessage.textContent = 'Settings saved successfully!'; // Hide message after 3 seconds setTimeout(() => { saveMessage.style.display = 'none'; }, 3000); // Refresh memories with new settings fetchMemoriesAndCount(); }, 500); }); } function setupEventListeners( sidebarContainer: HTMLElement, memoryToggleSection: HTMLElement, userIdSection: HTMLElement, orgSection: HTMLElement, projectSection: HTMLElement, autoInjectSection: HTMLElement, thresholdSection: HTMLElement, topKSection: HTMLElement, saveSection: HTMLElement, memoryCountContainer: HTMLElement, footerToggle: HTMLElement, trackSearchSection: HTMLElement ): void { // Close button const closeBtn = sidebarContainer.querySelector('#closeBtn') as HTMLButtonElement; closeBtn?.addEventListener('click', toggleSidebar); // Tab switching const tabButtons = sidebarContainer.querySelectorAll('.tab-button'); const tabContents = sidebarContainer.querySelectorAll('.tab-content'); tabButtons.forEach(button => { button.addEventListener('click', function (this: HTMLButtonElement) { const targetTab = this.getAttribute('data-tab'); // Remove active class from all tabs and contents tabButtons.forEach(btn => btn.classList.remove('active')); tabContents.forEach(content => content.classList.remove('active')); // Add active class to clicked tab and corresponding content this.classList.add('active'); document.getElementById(`${targetTab}-tab`)?.classList.add('active'); }); }); // Dashboard button const openDashboardBtn = memoryCountContainer.querySelector( '#openDashboardBtn' ) as HTMLButtonElement; openDashboardBtn?.addEventListener('click', openDashboard); // Logout button const logoutBtn = footerToggle.querySelector('#logoutBtn') as HTMLButtonElement; logoutBtn?.addEventListener('click', logout); // Toggle functionality is now handled by the save button // Organization select (for loading projects only) const orgSelect = orgSection.querySelector('#orgSelect') as HTMLSelectElement; orgSelect?.addEventListener('change', function (this: HTMLSelectElement) { const selectedOrgId = this.value; // Reset project selection const projectSelect = projectSection.querySelector('#projectSelect') as HTMLSelectElement; if (projectSelect) { projectSelect.innerHTML = ''; } // Fetch projects for selected org if (selectedOrgId) { fetchProjects(selectedOrgId, projectSelect); } else { if (projectSelect) { projectSelect.innerHTML = ''; } } }); // User dashboard link button const userDashboardBtn = userIdSection.querySelector('#userDashboardBtn') as HTMLButtonElement; userDashboardBtn?.addEventListener('click', function () { chrome.runtime.sendMessage({ action: SidebarAction.OPEN_DASHBOARD, url: 'https://app.mem0.ai/dashboard/users', }); }); // Threshold slider event listener const thresholdSlider = thresholdSection.querySelector('#thresholdSlider') as HTMLInputElement; const thresholdValue = thresholdSection.querySelector('.threshold-value') as HTMLElement; thresholdSlider?.addEventListener('input', function (this: HTMLInputElement) { if (thresholdValue) { thresholdValue.textContent = parseFloat(this.value).toFixed(1); } }); // Top K input validation const topKInput = topKSection.querySelector('#topKInput') as HTMLInputElement; topKInput?.addEventListener('input', function (this: HTMLInputElement) { const value = parseInt(this.value, 10); if (value < 1) { this.value = String(1); } else if (value > 50) { this.value = String(50); } }); // Save button const saveBtn = saveSection.querySelector('#saveSettingsBtn') as HTMLButtonElement; const saveText = saveSection.querySelector('.save-text') as HTMLElement; const saveLoader = saveSection.querySelector('.save-loader') as HTMLElement; const saveMessage = saveSection.querySelector('#saveMessage') as HTMLElement; saveBtn?.addEventListener('click', function () { if (!saveBtn || !saveText || !saveLoader || !saveMessage) { return; } saveSettings( saveBtn, saveText, saveLoader, saveMessage, userIdSection, orgSection, projectSection, memoryToggleSection, autoInjectSection, thresholdSection, topKSection, trackSearchSection ); }); } function fetchOrganizations(): void { chrome.storage.sync.get([StorageKey.API_KEY, StorageKey.ACCESS_TOKEN], function (data) { if (data.apiKey || data.access_token) { const headers = getHeaders(data.apiKey, data.access_token); fetch('https://api.mem0.ai/api/v1/orgs/organizations/', { method: 'GET', headers: headers, }) .then(response => response.json()) .then((orgs: Organization[]) => { const orgSelect = document.getElementById('orgSelect') as HTMLSelectElement; if (orgSelect) { orgSelect.innerHTML = ''; } orgs.forEach(org => { const option = document.createElement('option'); option.value = org.org_id; option.textContent = org.name; orgSelect?.appendChild(option); }); // Load saved org selection or select first org by default chrome.storage.sync.get([StorageKey.SELECTED_ORG], function (result) { if (result.selected_org) { if (orgSelect) { orgSelect.value = String(result.selected_org ?? ''); } const projectSelectEl = document.getElementById( 'projectSelect' ) as HTMLSelectElement; const orgIdStr = typeof result.selected_org === 'string' ? result.selected_org : String(result.selected_org || ''); fetchProjects(orgIdStr, projectSelectEl); } else if (orgs.length > 0) { // Select first org by default (but don't save until user clicks save) const firstOrg = orgs[0]; if (orgSelect) { orgSelect.value = String(firstOrg?.org_id ?? ''); } const projectSelectEl = document.getElementById( 'projectSelect' ) as HTMLSelectElement; fetchProjects(String(firstOrg?.org_id ?? ''), projectSelectEl); } }); }) .catch(error => { console.error('Error fetching organizations:', error); const orgSelect = document.getElementById('orgSelect') as HTMLSelectElement; if (orgSelect) { orgSelect.innerHTML = ''; } }); } }); } function fetchProjects(orgId: string, projectSelect: HTMLSelectElement): void { chrome.storage.sync.get([StorageKey.API_KEY, StorageKey.ACCESS_TOKEN], function (data) { if (data.apiKey || data.access_token) { const headers = getHeaders(data.apiKey, data.access_token); fetch(`https://api.mem0.ai/api/v1/orgs/organizations/${orgId}/projects/`, { method: 'GET', headers: headers, }) .then(response => response.json()) .then((projects: Project[]) => { if (!projectSelect) { return; } projectSelect.innerHTML = ''; projects.forEach(project => { const option = document.createElement('option'); option.value = project.project_id; option.textContent = project.name; projectSelect.appendChild(option); }); // Load saved project selection or select first project by default chrome.storage.sync.get([StorageKey.SELECTED_PROJECT], function (result) { if (!projectSelect) { return; } if (result.selected_project) { projectSelect.value = String(result.selected_project ?? ''); } else if (projects.length > 0) { // Select first project by default (but don't save until user clicks save) projectSelect.value = String(projects[0]?.project_id ?? ''); } }); }) .catch(error => { console.error('Error fetching projects:', error); if (projectSelect) { projectSelect.innerHTML = ''; } }); } }); } function fetchMemoriesAndCount(): void { chrome.storage.sync.get( [ StorageKey.API_KEY, StorageKey.ACCESS_TOKEN, StorageKey.USER_ID, StorageKey.SELECTED_ORG, StorageKey.SELECTED_PROJECT, ], function (data) { if (data.apiKey || data.access_token) { const headers = getHeaders(data.apiKey, data.access_token); // Build query parameters const params = new URLSearchParams(); const userId = data.user_id || DEFAULT_USER_ID; params.append('user_id', userId); params.append('page', '1'); params.append('page_size', '20'); if (data.selected_org) { params.append('org_id', data.selected_org); } if (data.selected_project) { params.append('project_id', data.selected_project); } fetch(`https://api.mem0.ai/v1/memories/?${params.toString()}`, { method: 'GET', headers: headers, }) .then(response => response.json()) .then((data: MemoriesResponse) => { // Update count and display memories updateMemoryCount(data.count || 0); displayMemories(data.results || []); }) .catch(error => { console.error('Error fetching memories:', error); updateMemoryCount('Error'); displayErrorMessage(); }); } else { updateMemoryCount('Login required'); displayErrorMessage('Login required to view memories'); } } ); } function updateMemoryCount(count: number | string): void { const countDisplay = document.querySelector('.memory-count') as HTMLElement; if (countDisplay) { countDisplay.classList.remove('loading'); countDisplay.textContent = typeof count === 'number' ? new Intl.NumberFormat().format(count) + ' Memories' : count; } } function getHeaders(apiKey?: string, accessToken?: string): Record { const headers: Record = { 'Content-Type': 'application/json', }; if (apiKey) { headers['Authorization'] = `Token ${apiKey}`; } else if (accessToken) { headers['Authorization'] = `Bearer ${accessToken}`; } return headers; } function closeSearchInput(): void { const inputContainer = document.querySelector('.input-container') as HTMLElement; const existingSearchInput = inputContainer?.querySelector('.search-memory'); const searchBtn = document.getElementById('searchBtn') as HTMLElement; if (existingSearchInput) { existingSearchInput.remove(); searchBtn?.classList.remove('active'); // Remove filter when search is closed filterMemories(''); } } function filterMemories(searchTerm: string): void { const memoryItems = document.querySelectorAll('.memory-item'); memoryItems.forEach(item => { const memoryText = item.querySelector('.memory-text')?.textContent?.toLowerCase() || ''; if (memoryText.includes(searchTerm)) { item.style.display = 'flex'; } else { item.style.display = 'none'; } }); // Add this line to maintain the width of the sidebar const sb = document.getElementById('mem0-sidebar') as HTMLElement; if (sb) { sb.style.width = '400px'; } } function addStyles() { const style = document.createElement('style'); style.textContent = ` @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); :root { --bg-dark: #18181b; --bg-card: #27272a; --bg-button: #3b3b3f; --bg-button-hover: #4b4b4f; --text-white: #ffffff; --text-gray: #a1a1aa; --purple: #7a5bf7; --border-color: #27272a; --tag-bg: #3b3b3f; --scrollbar-bg: #18181b; --scrollbar-thumb: #3b3b3f; --success-color: #22c55e; } #mem0-sidebar { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; position: fixed; top: 60px; right: 50px; width: 400px; height: auto; max-height: 85vh; background: var(--bg-dark); border: 1px solid var(--border-color); border-radius: 12px; box-sizing: border-box; display: flex; flex-direction: column; padding: 0px; color: var(--text-white); z-index: 2147483647; transition: right 0.3s ease-in-out; overflow: hidden; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); } .fixed-header { box-sizing: border-box; width: 100%; background: var(--bg-dark); border-bottom: 1px solid var(--border-color); } .tabs-container { padding: 0 16px; background: var(--bg-dark); border-bottom: 1px solid var(--border-color); } .tabs { display: flex; gap: 0; } .tab-button { flex: 1; padding: 12px 16px; background: none; border: none; color: var(--text-gray); font-size: 14px; font-weight: 500; cursor: pointer; transition: color 0.2s ease; position: relative; border-bottom: 2px solid transparent; } .tab-button.active { color: var(--text-white); border-bottom-color: var(--purple); } .tab-button:hover { color: var(--text-white); } .tab-content { display: none; flex-direction: column; gap: 24px; } .tab-content.active { display: flex; } .header { display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding: 16px; width: 100%; height: 62px; } .logo-container { display: flex; flex-direction: row; align-items: center; padding: 0px; gap: 8px; height: 24px; } .openmemory-icon { width: 24px; height: 24px; } .openmemory-logo { height: 24px; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-style: normal; font-weight: 600; font-size: 20px; line-height: 24px; letter-spacing: -0.03em; color: var(--text-white); } .header-buttons { display: flex; flex-direction: row; align-items: center; padding: 0px; gap: 16px; height: 30px; } .close-button { background: none; border: none; width: 24px; height: 24px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text-gray); font-size: 20px; transition: color 0.2s ease; } .close-button:hover { color: var(--text-white); } /* Custom scrollbar styles */ .content { padding: 16px; display: flex; flex-direction: column; gap: 24px; overflow-y: auto; max-height: calc(85vh - 62px - 49px - 60px); /* Subtract header, tab bar, and footer heights */ /* Firefox */ scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-bg); } /* WebKit browsers (Chrome, Safari, Edge) */ .content::-webkit-scrollbar { width: 4px; } .content::-webkit-scrollbar-track { background: var(--scrollbar-bg); } .content::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); border-radius: 4px; border: none; } .total-memories { background-color: var(--bg-card); border-radius: 8px; padding: 16px; } .total-memories-content { display: flex; justify-content: space-between; align-items: center; } .total-memories-label { color: var(--text-gray); font-size: 14px; margin-bottom: 4px; } .memory-count { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-style: normal; font-weight: 500; font-size: 18px; line-height: 140%; letter-spacing: -0.03em; color: var(--text-white); } .memory-count.loading { color: var(--text-gray); font-size: 16px; } .dashboard-button { display: flex; flex-direction: row; align-items: center; padding: 4px 8px; gap: 4px; background: var(--bg-button); background-opacity: 0.5; border-radius: 8px; border: none; cursor: pointer; transition: all 0.2s ease; color: var(--text-white); font-size: 14px; } .dashboard-button:hover { background: var(--bg-button-hover); } .external-link-icon { width: 14px; height: 14px; } .section { display: flex; flex-direction: column; gap: 8px; } .section-header { display: flex; justify-content: space-between; align-items: center; width: 100%; } .section-title { font-size: 18px; font-weight: 500; color: var(--text-white); } .section-description { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-style: normal; font-weight: 400; font-size: 14px; line-height: 140%; letter-spacing: -0.03em; color: var(--text-gray); } .switch { position: relative; display: inline-block; width: 44px; height: 22px; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--bg-card); transition: .4s; border-radius: 34px; } .slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 2px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .slider { background-color: var(--purple); } input:focus + .slider { box-shadow: 0 0 1px var(--purple); } input:checked + .slider:before { transform: translateX(20px); } .memory-cards { display: flex; flex-direction: column; gap: 12px; } .memory-card { background-color: var(--bg-card); border-radius: 8px; padding: 12px; display: flex; justify-content: space-between; align-items: flex-start; } .memory-content { flex: 1; padding-right: 8px; } .memory-text { color: var(--text-gray); font-size: 14px; margin: 0 0 8px 0; } .memory-categories { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; } .memory-category { background-color: var(--tag-bg); color: var(--text-white); font-size: 12px; padding: 2px 8px; border-radius: 4px; } .memory-actions { display: flex; gap: 4px; flex-shrink: 0; } .memory-action-button { background: none; border: none; color: var(--text-gray); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: color 0.2s; width: 24px; height: 24px; border-radius: 4px; } .memory-action-button:hover { color: var(--text-white); background-color: var(--bg-button); } .memory-action-button.copied { color: var(--success-color); } .memory-loader { display: flex; justify-content: center; align-items: center; padding: 20px 0; } .no-memories, .memory-error { color: var(--text-gray); text-align: center; font-style: italic; padding: 20px 0; } .footer { display: flex; justify-content: space-between; align-items: center; padding: 16px; border-top: 1px solid var(--border-color); } .shortcut { padding: 6px 12px; background-color: var(--bg-card); color: var(--text-gray); border-radius: 8px; font-size: 14px; } .logout-button { display: flex; flex-direction: row; align-items: center; padding: 6px 16px; background: var(--bg-button); border-radius: 8px; border: none; cursor: pointer; transition: all 0.2s ease; color: var(--text-white); font-size: 14px; } .logout-button:hover { background: var(--bg-button-hover); } .loader { border: 2px solid var(--bg-button); border-top: 2px solid var(--purple); border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; margin: 0 auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .input-container { width: 100%; padding: 0; box-sizing: border-box; } .search-memory { width: 100%; box-sizing: border-box; margin-top: 16px; } .settings-input, .settings-select { width: 100%; padding: 12px 16px; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 8px; color: var(--text-white); font-size: 14px; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; transition: border-color 0.2s ease, background-color 0.2s ease; box-sizing: border-box; } .settings-input:focus, .settings-select:focus { outline: none; border-color: var(--purple); background: var(--bg-button); } .settings-input::placeholder { color: var(--text-gray); } .settings-select option { background: var(--bg-card); color: var(--text-white); } .settings-select:hover { border-color: var(--text-gray); } .link-button { background: none; border: none; color: var(--text-gray); cursor: pointer; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 4px; transition: all 0.2s ease; } .link-button:hover { color: var(--text-white); background: var(--bg-button); } .save-button { width: 100%; padding: 12px 24px; background: var(--purple); border: none; border-radius: 8px; color: var(--text-white); font-size: 14px; font-weight: 500; cursor: pointer; transition: background-color 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .save-button:hover:not(:disabled) { background: #6d4ed6; } .save-button:disabled { background: var(--bg-button); cursor: not-allowed; } .save-loader { display: none; align-items: center; justify-content: center; } .mini-loader { border: 2px solid var(--bg-button); border-top: 2px solid var(--text-white); border-radius: 50%; width: 16px; height: 16px; animation: spin 1s linear infinite; } .save-message { margin-top: 8px; padding: 8px 12px; border-radius: 6px; font-size: 13px; text-align: center; } .save-message.success { background: rgba(34, 197, 94, 0.1); color: var(--success-color); border: 1px solid rgba(34, 197, 94, 0.3); } .save-message.error { background: rgba(239, 68, 68, 0.1); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.3); } .threshold-value { color: var(--purple); font-weight: 600; font-size: 14px; } .slider-container { display: flex; flex-direction: column; gap: 8px; } .threshold-slider { width: 100%; height: 6px; background: var(--bg-card); border-radius: 3px; outline: none; appearance: none; -webkit-appearance: none; cursor: pointer; } .threshold-slider::-webkit-slider-thumb { appearance: none; width: 20px; height: 20px; border-radius: 50%; background: var(--purple); cursor: pointer; border: 2px solid var(--bg-dark); box-shadow: 0 0 0 1px var(--purple); } .threshold-slider::-moz-range-thumb { width: 20px; height: 20px; border-radius: 50%; background: var(--purple); cursor: pointer; border: 2px solid var(--bg-dark); box-shadow: 0 0 0 1px var(--purple); } .threshold-slider::-webkit-slider-track { width: 100%; height: 6px; background: var(--bg-card); border-radius: 3px; } .threshold-slider::-moz-range-track { width: 100%; height: 6px; background: var(--bg-card); border-radius: 3px; border: none; } .threshold-slider:focus { outline: none; } .threshold-slider:focus::-webkit-slider-thumb { box-shadow: 0 0 0 2px var(--purple); } .threshold-slider:focus::-moz-range-thumb { box-shadow: 0 0 0 2px var(--purple); } .slider-labels { display: flex; justify-content: space-between; font-size: 12px; color: var(--text-gray); margin-top: 4px; } .settings-input[type="number"] { -moz-appearance: textfield; } .settings-input[type="number"]::-webkit-outer-spin-button, .settings-input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } `; document.head.appendChild(style); } function logout() { chrome.storage.sync.get([StorageKey.API_KEY, StorageKey.ACCESS_TOKEN], function (data) { const headers = getHeaders(data.apiKey, data.access_token); fetch('https://api.mem0.ai/v1/extension/', { method: 'POST', headers: headers, body: JSON.stringify({ event_type: 'extension_logout', }), }).catch(error => { console.error('Error sending logout event:', error); }); }); chrome.storage.sync.remove( [StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN], function () { const sidebar = document.getElementById('mem0-sidebar'); if (sidebar) { sidebar.style.right = '-500px'; } } ); } function openDashboard() { chrome.storage.sync.get([StorageKey.USER_ID], function () { chrome.runtime.sendMessage({ action: SidebarAction.OPEN_DASHBOARD, url: `https://app.mem0.ai/dashboard/requests`, }); }); } // Add function to display memories function displayMemories(memories: Memory[]): void { const memoryCardsContainer = document.querySelector('.memory-cards') as HTMLElement; if (!memoryCardsContainer) { return; } // Clear loading indicator memoryCardsContainer.innerHTML = ''; if (!memories || memories.length === 0) { memoryCardsContainer.innerHTML = '

No memories found

'; return; } // Add memory cards memories.forEach(memory => { // Extract memory content from the new format const memoryContent = memory.memory || ''; // Truncate long text const truncatedContent = memoryContent.length > 120 ? memoryContent.substring(0, 120) + '...' : memoryContent; // Get categories if available const categories = memory.categories || []; const categoryTags = categories.length > 0 ? `
${categories.map(cat => `${cat}`).join('')}
` : ''; const memoryCard = document.createElement('div'); memoryCard.className = 'memory-card'; memoryCard.innerHTML = `

${truncatedContent}

${categoryTags}
`; memoryCardsContainer.appendChild(memoryCard); }); // Add event listener for the copy button document.querySelectorAll('.copy-button').forEach(button => { button.addEventListener('click', function (this: HTMLButtonElement, e) { e.stopPropagation(); const content = decodeURIComponent(this.getAttribute('data-content') || ''); // Copy to clipboard navigator.clipboard .writeText(content) .then(() => { // Visual feedback for copy const originalTitle = this.getAttribute('title') || ''; this.setAttribute('title', 'Copied!'); this.classList.add('copied'); // Reset after a short delay setTimeout(() => { this.setAttribute('title', originalTitle); this.classList.remove('copied'); }, 2000); }) .catch(err => { console.error('Failed to copy: ', err); }); }); }); // Add event listener for the view button document.querySelectorAll('.view-button').forEach(button => { button.addEventListener('click', function (this: HTMLButtonElement, e) { e.stopPropagation(); const memoryId = this.getAttribute('data-id'); if (memoryId) { chrome.storage.sync.get([StorageKey.USER_ID], function (data) { const userId = data.user_id || 'chrome-extension-user'; chrome.runtime.sendMessage({ action: SidebarAction.OPEN_DASHBOARD, url: `https://app.mem0.ai/dashboard/user/${userId}?memoryId=${memoryId}`, }); }); } }); }); } // Add function to display error message function displayErrorMessage(message = 'Error loading memories') { const memoryCardsContainer = document.querySelector('.memory-cards'); if (!memoryCardsContainer) { return; } memoryCardsContainer.innerHTML = `

${message}

`; } // Initialize the listener when the script loads if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeMem0Sidebar); } else { initializeMem0Sidebar(); } })(); ================================================ FILE: src/types/api.ts ================================================ /** message roles (User, Assistant) */ export enum MessageRole { User = 'user', Assistant = 'assistant', } /** Message structure with role and content */ export type ApiMessage = { role: string; content: string; }; /** Request payload for memory API calls */ export type ApiMemoryRequest = { messages: ApiMessage[]; user_id: string; metadata: { provider: string; category: string; page_url?: string; engine?: string; }; source: string; org_id?: string; project_id?: string; }; /** Array of memory search results */ export type MemorySearchResponse = Array<{ id: string; memory: string; text?: string; created_at?: string; user_id?: string; categories?: string[]; }>; /** User authentication data structure */ export type LoginData = Partial<{ apiKey: string; userId: string; user_id: string; access_token: string; }>; /** Default user ID constant */ export const DEFAULT_USER_ID = 'chrome-extension-user'; /** Extension source identifier */ export const SOURCE = 'OPENMEMORY_CHROME_EXTENSION'; ================================================ FILE: src/types/browser.ts ================================================ /** Web navigation event details */ export type OnCommittedDetails = { tabId: number; url: string; processId: number; frameId: number; parentFrameId: number; transitionType: chrome.webNavigation.TransitionType; transitionQualifiers: chrome.webNavigation.TransitionQualifier[]; timeStamp: number; documentId: string; parentDocumentId?: string; documentLifecycle: chrome.extensionTypes.DocumentLifecycle; frameType: chrome.extensionTypes.FrameType; }; /** JSON primitive value types */ export type JsonPrimitive = string | number | boolean | null; /** Recursive JSON value type */ export type JsonValue = JsonPrimitive | JsonValue[] | { [k: string]: JsonValue }; /** JSON object structure */ export type JsonObject = { [k: string]: JsonValue }; /** Browser history state data */ export type HistoryStateData = JsonObject | null; /** Browser history URL type */ export type HistoryUrl = string | URL | null; ================================================ FILE: src/types/dom.ts ================================================ // Global interfaces for DOM extensions declare global { interface Element { value?: string; disabled?: boolean; dataset: DOMStringMap; style: CSSStyleDeclaration; } interface CSSStyleDeclaration { msOverflowStyle?: string; } interface Window { mem0Initialized?: boolean; mem0KeyboardListenersAdded?: boolean; mem0ButtonAdded?: boolean; } } /** Extended HTML element with cleanup methods */ export type ExtendedHTMLElement = HTMLElement & { _cleanupDragEvents?: () => void; }; /** Extended document with mem0-specific properties */ export type ExtendedDocument = Document & { __mem0FocusPrimed?: boolean; __mem0EnterCapture?: boolean; __mem0SubmitCapture?: boolean; }; /** Extended element with additional properties */ export type ExtendedElement = Element & { __mem0Observed?: boolean; nodeType?: number; matches?: (selector: string) => boolean; querySelector?: (selector: string) => Element | null; classList?: DOMTokenList; }; /** Modal size and pagination settings */ export type ModalDimensions = { width: number; height: number; memoriesPerPage: number; }; /** Modal positioning coordinates */ export type ModalPosition = { top: number | null; left: number | null; }; /** Extended mutation observer with timers */ export type MutableMutationObserver = MutationObserver & { memoryStateInterval?: number; debounceTimer?: number; }; ================================================ FILE: src/types/memory.ts ================================================ /** Individual memory structure with id, text, and categories */ export type MemoryItem = { id?: string; text: string; memory?: string; categories?: string[]; removed?: boolean; created_at?: string; user_id?: string; }; /** Simplified memory structure */ export type Memory = Partial<{ id: string; memory: string; categories: string[]; }>; /** Search result item from API */ export type MemorySearchItem = { id: string | number; memory: string; categories?: string[] }; /** API response wrapper for memories */ export type MemoriesResponse = Partial<{ count: number; results: Memory[]; }>; /** Prompt templates and regex patterns */ export type OpenMemoryPrompts = { memory_header_html_strong: string; memory_header_plain_regex: RegExp; memory_header_html_regex: RegExp; }; /** Optional parameters for API calls (org_id, project_id) */ export type OptionalApiParams = Partial<{ org_id: string; project_id: string; }>; ================================================ FILE: src/types/messages.ts ================================================ /** Enum for toast notification types (SUCCESS, ERROR) */ export enum ToastVariant { SUCCESS = 'success', ERROR = 'error', } /** Enum for different message types */ export enum MessageType { GET_SELECTION_CONTEXT = 'mem0:getSelectionContext', SELECTION_CONTEXT = 'mem0:selectionContext', TOAST = 'mem0:toast', } /** Enum for sidebar actions (TOGGLE_SIDEBAR, OPEN_POPUP, etc.) */ export enum SidebarAction { TOGGLE_SIDEBAR = 'toggleSidebar', OPEN_POPUP = 'openPopup', TOGGLE_MEM0 = 'toggleMem0', OPEN_DASHBOARD = 'openDashboard', SIDEBAR_SETTINGS = 'toggleSidebarSettings', OPEN_OPTIONS = 'openOptions', SHOW_LOGIN_POPUP = 'showLoginPopup', } /** Payload for selection context messages */ export type SelectionContextPayload = Partial<{ selection: string; title: string; url: string; }>; /** Response structure for selection context */ export type SelectionContextResponse = Partial<{ type: string; payload: SelectionContextPayload; error: string; }>; /** Message type for getting selection context */ export type GetSelectionContextMessage = { type: MessageType.GET_SELECTION_CONTEXT; }; /** Toast notification message structure */ export type ToastMessage = { type: MessageType.TOAST; payload: { message?: string; variant?: ToastVariant; }; }; /** Union type for selection context messages */ export type SelectionContextMessage = GetSelectionContextMessage | ToastMessage; /** Response callback type */ export type SendResponse = (response: SelectionContextResponse) => void; export type ToggleSidebarMessage = { action: SidebarAction.TOGGLE_SIDEBAR; }; export type OpenPopupMessage = { action: SidebarAction.OPEN_POPUP; }; export type ToggleMem0Message = { action: SidebarAction.TOGGLE_MEM0; enabled: boolean; }; export type OpenDashboardMessage = { action: SidebarAction.OPEN_DASHBOARD; url: string; }; export type SidebarActionMessage = | ToggleSidebarMessage | OpenPopupMessage | ToggleMem0Message | OpenDashboardMessage; ================================================ FILE: src/types/organizations.ts ================================================ /** Organization structure with org_id and name */ export type Organization = { org_id: string; name: string; }; /** Project structure with project_id and name */ export type Project = { project_id: string; name: string; }; ================================================ FILE: src/types/providers.ts ================================================ /** Enum for supported AI providers */ export enum Provider { ContextMenu = 'ContextMenu', DirectURL = 'DirectURL', SearchTracker = 'SearchTracker', Grok = 'Grok', Claude = 'Claude', ChatGPT = 'ChatGPT', Gemini = 'Gemini', Perplexity = 'Perplexity', DeepSeek = 'DeepSeek', Replit = 'Replit', } /** Enum for memory categories (BOOKMARK, NAVIGATION, SEARCH) */ export enum Category { BOOKMARK = 'BOOKMARK', NAVIGATION = 'NAVIGATION', SEARCH = 'SEARCH', } ================================================ FILE: src/types/settings.ts ================================================ /** User preference structure with API keys, memory settings, and thresholds */ export type UserSettings = Partial<{ apiKey: string; accessToken: string; userId: string; memoryEnabled: boolean; selectedOrg: string; selectedProject: string; similarityThreshold: number; topK: number; }>; /** Sidebar-specific settings with organization and project info */ export type SidebarSettings = { user_id?: string; selected_org?: string; selected_org_name?: string; selected_project?: string; selected_project_name?: string; memory_enabled: boolean; auto_inject_enabled: boolean; similarity_threshold: number; top_k: number; track_searches: boolean; }; /** Legacy settings structure for compatibility */ export type Settings = { hasCreds: boolean; apiKey: string | null; accessToken: string | null; userId: string; orgId: string | null; projectId: string | null; memoryEnabled: boolean; }; ================================================ FILE: src/types/storage.ts ================================================ /** Enum for all storage keys used in the extension */ export enum StorageKey { API_KEY = 'apiKey', ACCESS_TOKEN = 'access_token', USER_ID = 'user_id', USER_ID_CAMEL = 'userId', USER_LOGGED_IN = 'userLoggedIn', SELECTED_ORG = 'selected_org', SELECTED_PROJECT = 'selected_project', MEMORY_ENABLED = 'memory_enabled', AUTO_INJECT_ENABLED = 'auto_inject_enabled', SIMILARITY_THRESHOLD = 'similarity_threshold', TOP_K = 'top_k', TRACK_SEARCHES = 'track_searches', } /** Type mapping for storage values (required fields) */ export type StorageItems = { apiKey: string; userId: string; user_id: string; access_token: string; memory_enabled: boolean; selected_org: string; selected_project: string; similarity_threshold: number; top_k: number; }; /** Type mapping for storage values (optional fields) */ export type StorageData = Partial<{ apiKey: string; userId: string; user_id: string; access_token: string; memory_enabled: boolean; selected_org: string; selected_project: string; similarity_threshold: number; top_k: number; }>; ================================================ FILE: src/utils/background_search.ts ================================================ import type { MemorySearchItem } from '../types/memory'; import type { StorageKey } from '../types/storage'; export type SearchStorage = Partial<{ [StorageKey.API_KEY]: string; [StorageKey.USER_ID_CAMEL]: string; [StorageKey.ACCESS_TOKEN]: string; [StorageKey.SELECTED_ORG]: string; [StorageKey.SELECTED_PROJECT]: string; [StorageKey.USER_ID]: string; [StorageKey.SIMILARITY_THRESHOLD]: number; [StorageKey.TOP_K]: number; }>; export type FetchFn = (query: string, opts: { signal?: AbortSignal }) => Promise | T; export interface OrchestratorOptions { fetch: FetchFn; onStart?: (normalizedQuery: string) => void; onSuccess?: ( normalizedQuery: string, result: MemorySearchItem[], meta: { fromCache: boolean } ) => void; onError?: (normalizedQuery: string, err: Error) => void; onFinally?: (normalizedQuery: string) => void; minLength?: number; debounceMs?: number; cacheTTL?: number; useCache?: boolean; refreshOnCache?: boolean; } export interface OrchestratorState { latestText: string; lastCompletedQuery: string; lastResult: MemorySearchItem[] | null; inFlightQuery: string | null; isInFlight: boolean; cacheSize: number; } export interface Orchestrator { setText(text?: string): void; runImmediate(text?: string | null): void; cancel(): void; getState(): OrchestratorState; setOptions( opts: Partial< Pick< OrchestratorOptions, 'minLength' | 'debounceMs' | 'cacheTTL' | 'useCache' | 'refreshOnCache' > > ): void; clearCache(): void; } export function normalizeQuery(s?: string | number | boolean): string { if (!s) { return ''; } return String(s).trim().replace(/\s+/g, ' ').toLowerCase(); } export function createOrchestrator(options: OrchestratorOptions): Orchestrator { const fetchFn = options?.fetch; if (typeof fetchFn !== 'function') { throw new Error('createOrchestrator requires options.fetch(query, { signal })'); } const NOOP = (): void => undefined; const onStart = options.onStart ?? NOOP; const onSuccess = options.onSuccess ?? NOOP; const onError = options.onError ?? NOOP; const onFinally = options.onFinally ?? NOOP; let minLength = typeof options.minLength === 'number' ? options.minLength : 3; let debounceMs = typeof options.debounceMs === 'number' ? options.debounceMs : 75; let cacheTTL = typeof options.cacheTTL === 'number' ? options.cacheTTL : 60_000; let useCache = options.useCache !== false; let refreshOnCache = !!options.refreshOnCache; let latestText = ''; let lastCompletedQuery = ''; let lastResult: MemorySearchItem[] | null = null; let inFlightQuery: string | null = null; let abortController: AbortController | null = null; let timerId: ReturnType | null = null; let seq = 0; const cache = new Map(); function getState(): OrchestratorState { return { latestText, lastCompletedQuery, lastResult, inFlightQuery, isInFlight: !!inFlightQuery, cacheSize: cache.size, }; } function setOptions( newOpts: Partial< Pick< OrchestratorOptions, 'minLength' | 'debounceMs' | 'cacheTTL' | 'useCache' | 'refreshOnCache' > > ) { if (!newOpts) { return; } if (typeof newOpts.minLength === 'number') { minLength = newOpts.minLength; } if (typeof newOpts.debounceMs === 'number') { debounceMs = newOpts.debounceMs; } if (typeof newOpts.cacheTTL === 'number') { cacheTTL = newOpts.cacheTTL; } if (typeof newOpts.useCache === 'boolean') { useCache = newOpts.useCache; } if (typeof newOpts.refreshOnCache === 'boolean') { refreshOnCache = newOpts.refreshOnCache; } } function clearTimer() { if (timerId) { clearTimeout(timerId); timerId = null; } } function clearCache() { cache.clear(); } function getCached(normQuery: string): MemorySearchItem[] | null { if (!useCache) { return null; } const v = cache.get(normQuery); if (!v) { return null; } if (Date.now() - v.ts > cacheTTL) { cache.delete(normQuery); return null; } return v.result; } function setCached(normQuery: string, result: MemorySearchItem[]) { cache.set(normQuery, { ts: Date.now(), result }); } function cancel() { clearTimer(); if (abortController) { try { abortController.abort(); } catch { /* ignore abort errors */ } } inFlightQuery = null; abortController = null; } function run(query?: string | null) { const raw = query !== null && query !== undefined ? String(query) : latestText; const norm = normalizeQuery(raw); if (!norm || norm.length < minLength) { return; } const cached = getCached(norm); if (cached !== null) { onSuccess(norm, cached, { fromCache: true }); if (!refreshOnCache) { return; } } if (inFlightQuery && inFlightQuery === norm) { return; } if (inFlightQuery && inFlightQuery !== norm && abortController) { try { abortController.abort(); } catch { /* ignore abort errors */ } } inFlightQuery = norm; abortController = typeof AbortController !== 'undefined' ? new AbortController() : null; const mySeq = ++seq; onStart(norm); Promise.resolve() .then(() => fetchFn(norm, { signal: abortController ? abortController.signal : undefined })) .then(result => { if (inFlightQuery !== norm || mySeq !== seq) { return; } setCached(norm, result as MemorySearchItem[]); lastCompletedQuery = norm; lastResult = result as MemorySearchItem[]; onSuccess(norm, result as MemorySearchItem[], { fromCache: false }); }) .catch((err: Error) => { const aborted = abortController?.signal.aborted === true || err.name === 'AbortError'; if (mySeq !== seq) { return; } if (!aborted) { onError(norm, err); } }) .finally(() => { if (mySeq !== seq) { return; } inFlightQuery = null; abortController = null; onFinally(norm); }); } function schedule() { clearTimer(); if (!latestText || normalizeQuery(latestText).length < minLength) { return; } timerId = setTimeout(() => { timerId = null; run(latestText); }, debounceMs); } return { setText(text: string | null | undefined) { latestText = text === null || text === undefined ? '' : String(text); schedule(); }, runImmediate(text?: string | null) { if (text !== null && text !== undefined) { latestText = String(text); } clearTimer(); run(latestText); }, cancel, getState, setOptions, clearCache, }; } ================================================ FILE: src/utils/llm_prompts.ts ================================================ export const OPENMEMORY_PROMPTS = { rerank_system_prompt: ` You are OpenMemory Filterer. Your tasks: 1) From the provided candidate memories, select ONLY those that materially help answer the user query. 2) Never invent or paraphrase memories; pick from provided candidates only. 3) If none are relevant, return an empty list. DO NOT BE AFRAID TO EXCLUDE MEMORIES. Selection rules: - Prioritize constraints (medical, safety, legal), then strong stable preferences (likes/dislikes, dietary rules), then recent contextual facts. - Prefer specific over generic; constraints over trivia; use recency as a tiebreaker. - Do NOT select a memory that merely restates the user's present intent (e.g., "wants to eat a dessert"). Select enduring preferences instead (e.g., "likes desserts"). Output JSON ONLY, with exactly these keys: { "selected_memory_ids": ["id1", "id2", ...] } `, // Shared memory header inserted into prompts in various providers memory_header_text: "Here is some of my memories to help answer better (don't respond to these memories but use them to assist in the response):", get memory_header_html_strong() { return `${this.memory_header_text}`; }, memory_marker_prefix: 'Here is some of my memories to help answer better', // Central regexes for stripping the inserted memory header and its content // Plain text variant (end of prompt) – matches the header and everything after it memory_header_plain_regex: /\s*Here is some of my memories to help answer better \(don't respond to these memories but use them to assist in the response\):[\s\S]*$/, // HTML variant used in some editors (e.g., Claude ProseMirror) memory_header_html_regex: /

Here is some of my memories to help answer better \(don't respond to these memories but use them to assist in the response\):<\/strong><\/p>([\s\S]*?)(?=

|$)/, }; ================================================ FILE: src/utils/site_config.ts ================================================ // --- Types --- type InlinePlacement = { strategy: 'inline'; where?: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend'; inlineAlign?: 'start' | 'center' | 'end'; inlineClass?: string; }; type DockPlacement = { strategy: 'dock'; container?: string | Element; side?: 'top' | 'bottom' | 'left' | 'right'; align?: 'start' | 'center' | 'end'; gap?: number; }; type FloatPlacement = { strategy: 'float'; placement?: | 'top-start' | 'top-center' | 'top-end' | 'right-start' | 'right-center' | 'right-end' | 'bottom-start' | 'bottom-center' | 'bottom-end' | 'left-start' | 'left-center' | 'left-end'; gap?: number; }; type Placement = InlinePlacement | DockPlacement | FloatPlacement; interface SiteConfig { editorSelector: string; deriveAnchor: (editor: Element) => Element | null; placement: Placement; fallbackAnchors?: string[]; adjacentTargets?: string[]; autoButtonTextPattern?: RegExp; enableFloatingFallback?: boolean; sendButtonSelector?: string; modelButtonSelector?: string; } interface SiteConfigs { claude: SiteConfig; chatgpt: SiteConfig; grok: SiteConfig; deepseek: SiteConfig; gemini: SiteConfig; perplexity: SiteConfig; replit: SiteConfig; } // --- Config (no globals) --- const SITE_CONFIG = { claude: { editorSelector: 'div[contenteditable="true"], textarea, p[data-placeholder], [contenteditable="true"]', deriveAnchor: editor => editor.closest('form') || editor.parentElement, // Place the icon floating near the editor to avoid container clipping on some layouts placement: { strategy: 'float', placement: 'right-start', gap: 8 }, fallbackAnchors: ['#input-tools-menu-trigger', 'button[aria-label*="Send" i]'], }, chatgpt: { editorSelector: 'textarea, [contenteditable="true"], input[type="text"]', deriveAnchor: editor => { const form = editor.closest('form'); const toolbar = (form && (form.querySelector('[data-testid="composer-trailing-actions"]') || form.querySelector('.composer-trailing-actions') || form.querySelector('.items-center.gap-1\\.5') || form.querySelector('.items-center.gap-2'))) || null; return toolbar || form || editor.parentElement; }, placement: { strategy: 'inline', where: 'beforeend', inlineAlign: 'end' }, adjacentTargets: [ 'button[aria-label="Dictate button"]', 'button[aria-label*="mic" i]', 'button[aria-label*="voice" i]', ], fallbackAnchors: [ 'form [data-testid="composer-trailing-actions"]', 'form .composer-trailing-actions', 'form textarea', 'main form textarea', ], enableFloatingFallback: true, }, grok: { editorSelector: 'textarea, [contenteditable="true"], input[type="text"]', deriveAnchor: editor => { const root = editor.closest('form') || editor.parentElement || document.body; const autoBtn = Array.from(root.querySelectorAll('button,[role="button"]')).find(b => /\bAuto\b/i.test((b.textContent || '').trim()) ); return autoBtn ? autoBtn.parentElement || root : root; }, placement: { strategy: 'inline', where: 'beforeend', inlineAlign: 'end' }, autoButtonTextPattern: /\bAuto\b/i, fallbackAnchors: [ 'button[aria-label*="Send" i]', 'button[data-testid*="send" i]', 'form button[type="submit"]', 'textarea', '[role="textbox"]', ], }, deepseek: { editorSelector: 'textarea, [contenteditable="true"], input[type="text"]', deriveAnchor: editor => editor.closest('form') || editor.parentElement, placement: { strategy: 'inline', where: 'beforeend', inlineAlign: 'end' }, }, gemini: { editorSelector: 'textarea, [contenteditable="true"], input[type="text"]', deriveAnchor: editor => editor.closest('form') || editor.parentElement, placement: { strategy: 'inline', where: 'beforeend', inlineAlign: 'end' }, }, perplexity: { editorSelector: 'textarea, [contenteditable], input[type="text"]', deriveAnchor: editor => editor.closest('form') || editor.parentElement, placement: { strategy: 'inline', where: 'beforeend', inlineAlign: 'end' }, sendButtonSelector: 'button[aria-label="Submit"]', modelButtonSelector: 'button[aria-label="Choose a model"]', }, replit: { editorSelector: 'textarea, [contenteditable="true"], input[type="text"]', deriveAnchor: editor => editor.closest('form') || editor.parentElement || document.body, placement: { strategy: 'float', placement: 'right-center', gap: 12 }, }, } as const satisfies SiteConfigs; // --- Export exactly as requested --- export { SITE_CONFIG }; ================================================ FILE: src/utils/util_functions.ts ================================================ import { StorageKey, type StorageData } from '../types/storage'; type EventType = string; type AdditionalData = Record; type CallbackFunction = (success: boolean) => void; type ExtensionEventPayload = { event_type: EventType; additional_data: { timestamp: string; version: string; user_agent: string; user_id: string; [key: string]: unknown; }; }; type BrowserType = 'Edge' | 'Opera' | 'Chrome' | 'Firefox' | 'Safari' | 'Unknown'; /** * Utility function to send extension events to PostHog via mem0 API * @param eventType - The type of event (e.g., "extension_install", "extension_toggle_button") * @param additionalData - Optional additional data to include with the event * @param callback - Optional callback function called after attempt (receives success boolean) */ export const sendExtensionEvent = ( eventType: EventType, additionalData: AdditionalData = {}, callback: CallbackFunction | null = null ): void => { chrome.storage.sync.get( [StorageKey.API_KEY, StorageKey.ACCESS_TOKEN, StorageKey.USER_ID_CAMEL, StorageKey.USER_ID], (data: StorageData) => { if (!data[StorageKey.API_KEY] && !data[StorageKey.ACCESS_TOKEN]) { if (callback) { callback(false); } return; } const headers: Record = { 'Content-Type': 'application/json', }; if (data[StorageKey.ACCESS_TOKEN]) { headers['Authorization'] = `Bearer ${data[StorageKey.ACCESS_TOKEN]}`; } else if (data[StorageKey.API_KEY]) { headers['Authorization'] = `Token ${data[StorageKey.API_KEY]}`; } const payload: ExtensionEventPayload = { event_type: eventType, additional_data: { timestamp: new Date().toISOString(), version: chrome.runtime.getManifest().version, user_agent: navigator.userAgent, user_id: data[StorageKey.USER_ID_CAMEL] || data[StorageKey.USER_ID] || 'chrome-extension-user', ...additionalData, }, }; console.log('eventType', eventType); console.log('payload', payload); fetch('https://api.mem0.ai/v1/extension/', { method: 'POST', headers: headers, body: JSON.stringify(payload), }) .then(response => { const success = response.ok; if (callback) { callback(success); } }) .catch(error => { console.error(`Error sending ${eventType} event:`, error); if (callback) { callback(false); } }); } ); }; export const getBrowser = (): BrowserType => { const userAgent = navigator.userAgent; if (userAgent.includes('Edg/')) { return 'Edge'; } if (userAgent.includes('OPR/') || userAgent.includes('Opera/')) { return 'Opera'; } if (userAgent.includes('Chrome/')) { return 'Chrome'; } if (userAgent.includes('Firefox/')) { return 'Firefox'; } if (userAgent.includes('Safari/') && !userAgent.includes('Chrome/')) { return 'Safari'; } return 'Unknown'; }; ================================================ FILE: src/utils/util_positioning.ts ================================================ /* eslint-disable no-empty */ type AnchorCandidate = | string | { find: () => Element | null; }; type InlinePlacement = { strategy: 'inline'; where?: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend'; inlineAlign?: 'start' | 'end'; inlineClass?: string; }; type DockPlacement = { strategy: 'dock'; container?: string | Element; side?: 'top' | 'bottom' | 'left' | 'right'; align?: 'start' | 'center' | 'end'; gap?: number; }; type FloatPlacement = { strategy: 'float'; placement?: | 'top-start' | 'top-center' | 'top-end' | 'right-start' | 'right-center' | 'right-end' | 'bottom-start' | 'bottom-center' | 'bottom-end' | 'left-start' | 'left-center' | 'left-end'; gap?: number; }; export type Placement = InlinePlacement | DockPlacement | FloatPlacement; export type ApplyPlacementOptions = { container: HTMLElement; anchor: Element | null; placement?: Placement | null; }; export type FindAnchorOptions = { candidates?: AnchorCandidate[]; timeoutMs?: number; pollMs?: number; }; export type MountResilientOptions = { anchors?: AnchorCandidate[]; timeoutMs?: number; pollMs?: number; placement?: Placement; enableFloatingFallback?: boolean; render?: (shadow: ShadowRoot, host: HTMLElement, anchor: Element | null) => void | (() => void); }; export type MountOnEditorFocusOptions = { editorSelector?: string; deriveAnchor?: (editor: Element) => Element | null; existingHostSelector?: string; placement?: Placement; cacheTtlMs?: number; persistCache?: boolean; fallback?: () => void; learnKey?: string; render?: (shadow: ShadowRoot, host: HTMLElement, anchor: Element | null) => void | (() => void); }; type Stopper = () => void; type ShadowHost = { host: HTMLDivElement; shadow: ShadowRoot; }; type CachedAnchorHint = { sel: string; placement: Placement | null; ts: number; ver: string; }; declare const chrome: | { runtime?: { getManifest: () => { version: string } }; storage?: { session?: { get: (key: string, cb: (items: Record) => void) => void; set: (items: Record, cb?: () => void) => void; }; local?: { get: (key: string, cb: (items: Record) => void) => void; set: (items: Record, cb?: () => void) => void; remove: (key: string | string[], cb?: () => void) => void; }; }; } | undefined; /* utils */ const sleep = (ms: number): Promise => new Promise(r => setTimeout(r, ms)); function watchForRemoval(node: Node, onGone: () => void): MutationObserver { const obs = new MutationObserver(() => { if (!document.contains(node)) { try { obs.disconnect(); } catch {} onGone(); } }); obs.observe(document.documentElement, { childList: true, subtree: true }); return obs; } function watchSpaNavigation(callback: () => void, intervalMs: number = 500): Stopper { const w = globalThis; const loc = w.location; const hist = w.history; let href = loc.href; const i = w.setInterval(() => { if (loc.href !== href) { href = loc.href; callback(); } }, intervalMs); w.addEventListener('popstate', callback); const originalPush = hist.pushState.bind(hist) as History['pushState']; const originalReplace = hist.replaceState.bind(hist) as History['replaceState']; // Reassign and trigger callback on SPA nav hist.pushState = ((...args: Parameters) => { const r = originalPush(...args); callback(); return r; }) as History['pushState']; hist.replaceState = ((...args: Parameters) => { const r = originalReplace(...args); callback(); return r; }) as History['replaceState']; return () => { w.clearInterval(i); w.removeEventListener('popstate', callback); hist.pushState = originalPush; hist.replaceState = originalReplace; }; } /* public API */ export function createShadowRootHost(className: string = 'mem0-root'): ShadowHost { const host = document.createElement('div'); host.className = className; const shadow = host.attachShadow({ mode: 'open' }); return { host, shadow }; } export async function findAnchor( candidates: AnchorCandidate[] = [], timeoutMs: number = 2000, pollMs: number = 250 ): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { for (let i = 0; i < candidates.length; i++) { const cand = candidates[i]; let el: Element | null = null; if (typeof cand === 'string') { el = document.querySelector(cand); } else if (cand && typeof cand.find === 'function') { el = cand.find(); } if (el) { return el; } } await sleep(pollMs); } return null; } export function applyPlacement(opts: ApplyPlacementOptions): Stopper { const { container, anchor, placement } = opts; if (!anchor) { document.body.appendChild(container); return () => { return; }; } const p = placement || ({ strategy: 'inline' } as Placement); switch (p.strategy) { case 'inline': { const where = p.where || 'beforeend'; (anchor as Element).insertAdjacentElement(where, container); if (p.inlineAlign === 'end') { container.style.marginLeft = 'auto'; } if (p.inlineClass) { container.classList.add(p.inlineClass); } return () => { return; }; } case 'dock': { const host = ( p.container ? typeof p.container === 'string' ? anchor.closest(p.container) || (anchor as Element) : p.container : anchor ) as HTMLElement; if (getComputedStyle(host).position === 'static') { host.style.position = 'relative'; } const side = p.side || 'bottom'; const align = p.align || 'start'; const gap = p.gap ?? 8; const cs = container.style; cs.position = 'absolute'; cs.zIndex = '2147483647'; const layout = (): void => { host.getBoundingClientRect(); // force layout if (side === 'top') { cs.top = `${-container.offsetHeight - gap}px`; cs.bottom = ''; } if (side === 'bottom') { cs.top = `${host.offsetHeight + gap}px`; cs.bottom = ''; } if (side === 'left') { cs.left = `${-container.offsetWidth - gap}px`; cs.right = ''; cs.top = ''; } if (side === 'right') { cs.left = `${host.offsetWidth + gap}px`; cs.right = ''; cs.top = ''; } const a = { start: '0px', center: '50%', end: '100%' }[align]; if (side === 'top' || side === 'bottom') { cs.left = a; cs.transform = align === 'center' ? 'translateX(-50%)' : ''; } else { cs.top = a; cs.transform = align === 'center' ? 'translateY(-50%)' : ''; } }; host.appendChild(container); layout(); const ro = new ResizeObserver(layout); ro.observe(host); const on = (): void => layout(); globalThis.addEventListener('scroll', on, true); globalThis.addEventListener('resize', on); return () => { ro.disconnect(); globalThis.removeEventListener('scroll', on, true); globalThis.removeEventListener('resize', on); }; } case 'float': { const gap = p.gap ?? 8; const position = (): void => { const r = (anchor as Element).getBoundingClientRect(); const cs = container.style; cs.position = 'fixed'; cs.zIndex = '2147483647'; const parts = (p.placement || 'right-start').split('-'); const side = parts[0] as 'top' | 'right' | 'bottom' | 'left'; const align = (parts[1] || 'start') as 'start' | 'center' | 'end'; const set = (x: number, y: number): void => { cs.left = `${x}px`; cs.top = `${y}px`; }; const y = { start: r.top, center: r.top + r.height / 2, end: r.bottom }; const x = { start: r.left, center: r.left + r.width / 2, end: r.right }; if (side === 'right') { set(r.right + gap, y[align] ?? r.top); } if (side === 'left') { set(r.left - container.offsetWidth - gap, y[align] ?? r.top); } if (side === 'bottom') { set(x[align] ?? r.left, r.bottom + gap); } if (side === 'top') { set(x[align] ?? r.left, r.top - container.offsetHeight - gap); } }; document.body.appendChild(container); position(); const on = (): void => position(); globalThis.addEventListener('scroll', on, true); globalThis.addEventListener('resize', on); const ro = new ResizeObserver(on); ro.observe(anchor as Element); return () => { globalThis.removeEventListener('scroll', on, true); globalThis.removeEventListener('resize', on); ro.disconnect(); }; } default: { (anchor as Element).appendChild(container); return () => { return; }; } } } /* private helpers */ function validateCachedAnchor(sel: string | undefined, editor: Element | null) { const el = sel ? (document.querySelector(sel) as Element | null) : null; if (!el || !el.isConnected || !document.contains(el)) { return { ok: false as const, reason: 'missing' as const }; } if (editor && !(el === editor || el.contains(editor) || editor.contains(el))) { return { ok: false as const, reason: 'mismatch' as const }; } const r = el.getBoundingClientRect(); if (r.width === 0 && r.height === 0) { return { ok: false as const, reason: 'invisible' as const }; } return { ok: true as const, el }; } function selectorFor(el: Element | null): string { if (!el) { return ''; } if ((el as HTMLElement).id) { return `#${(el as HTMLElement).id}`; } const tag = (el.tagName || '').toLowerCase(); const cls = (el as HTMLElement).className && typeof (el as HTMLElement).className === 'string' ? '.' + (el as HTMLElement).className.trim().split(/\s+/).slice(0, 2).join('.') : ''; const s = tag + cls; const p = el.parentElement; if (p && p.id) { return `#${p.id} > ${s}`; } return s; } function keyFor(opts: { learnKey?: string }): string { const loc = globalThis.location; return 'mem0_anchor_hint:' + (opts.learnKey || `${loc.host}:${loc.pathname}`); } function now(): number { return Date.now(); } function ver(): string { try { return chrome?.runtime?.getManifest().version ?? '0'; } catch { return '0'; } } /* chrome storage helpers (safe, promise-based) */ async function getSession(k: string): Promise { return new Promise(resolve => { try { chrome?.storage?.session?.get(k, (o: Record) => resolve((o?.[k] as T) ?? null) ); } catch { resolve(null); } }); } async function setSession(k: string, v: T): Promise { return new Promise(resolve => { try { const o: Record = {}; o[k] = v as unknown; chrome?.storage?.session?.set(o, resolve); } catch { resolve(); } }); } async function getLocal(k: string): Promise { return new Promise(resolve => { try { chrome?.storage?.local?.get(k, (o: Record) => resolve((o?.[k] as T) ?? null) ); } catch { resolve(null); } }); } async function setLocal(k: string, v: T): Promise { return new Promise(resolve => { try { const o: Record = {}; o[k] = v as unknown; chrome?.storage?.local?.set(o, resolve); } catch { resolve(); } }); } async function delLocal(k: string | string[]): Promise { return new Promise(resolve => { try { chrome?.storage?.local?.remove(k, resolve); } catch { resolve(); } }); } /* cache API */ export async function resolveCachedAnchor( opts: { learnKey?: string }, editor: Element | null, ttlMs: number = 24 * 60 * 60 * 1000 ): Promise<{ el: Element; placement: Placement | null } | null> { const k = keyFor(opts); // fast session cache let hint = (await getSession(k)) || null; if (hint?.sel) { const v = validateCachedAnchor(hint.sel, editor); if (v.ok) { return { el: v.el, placement: hint.placement }; } } // persisted cache hint = (await getLocal(k)) || null; if (hint?.sel && hint.ver === ver() && now() - hint.ts < ttlMs) { const v2 = validateCachedAnchor(hint.sel, editor); if (v2.ok) { return { el: v2.el, placement: hint.placement }; } } else if (hint) { await delLocal(k); } return null; } export async function saveAnchorHint( opts: { learnKey?: string }, anchorEl: Element, placement: Placement | null, persist?: boolean ): Promise { const k = keyFor(opts); const hint: CachedAnchorHint = { sel: selectorFor(anchorEl), placement: placement ?? null, ts: now(), ver: ver(), }; await setSession(k, hint); if (persist) { await setLocal(k, hint); } } /* mount helpers */ export function mountResilient(opts: MountResilientOptions): Stopper { let cleanup: (() => void) | null = null; let stopSpa: Stopper | null = null; let removalObs: MutationObserver | null = null; let host: HTMLElement | null = null; const bootstrap = async (): Promise => { try { if (cleanup) { try { cleanup(); } catch {} cleanup = null; } if (removalObs) { try { removalObs.disconnect(); } catch {} removalObs = null; } if (host?.isConnected) { host.remove(); } host = null; const anchor = await findAnchor( opts.anchors || [], opts.timeoutMs ?? 2000, opts.pollMs ?? 200 ); const { host: h, shadow } = createShadowRootHost('mem0-root'); host = h; let unplace: Stopper = () => { return; }; if (anchor) { unplace = applyPlacement({ container: host, anchor, placement: opts.placement }); } else if (opts.enableFloatingFallback) { Object.assign(host.style, { position: 'fixed', right: '16px', bottom: '16px', zIndex: '2147483647', }); document.body.appendChild(host); } else { return; } const maybeCleanup = typeof opts.render === 'function' ? opts.render(shadow, host, anchor) : null; if (typeof maybeCleanup === 'function') { cleanup = maybeCleanup; } const watchNode = anchor || host; removalObs = watchForRemoval(watchNode, () => { try { unplace(); } catch {} if (cleanup) { try { cleanup(); } catch {} } bootstrap(); }); if (!stopSpa) { stopSpa = watchSpaNavigation(() => bootstrap()); } } catch { setTimeout(() => bootstrap(), 1500); } }; void bootstrap(); return () => { if (cleanup) { cleanup(); } if (stopSpa) { stopSpa(); } if (removalObs) { removalObs.disconnect(); } if (host?.isConnected) { host.remove(); } }; } export function mountOnEditorFocus(opts: MountOnEditorFocusOptions): Stopper { let used = false; const editorSelector = opts.editorSelector || 'textarea, [contenteditable="true"], input[type="text"]'; const deriveAnchor = opts.deriveAnchor || ((editor: Element) => editor.closest('form') || editor.parentElement || null); const guardSelector = opts.existingHostSelector || '#mem0-icon-button, .mem0-root'; const handleIntent = (e: Event): void => { if (used) { return; } try { if (guardSelector && document.querySelector(guardSelector)) { return; } } catch {} const t = e.target as Element | null; if (!t || !(t.matches && t.matches(editorSelector))) { return; } used = true; Promise.resolve() .then(() => resolveCachedAnchor(opts, t, opts.cacheTtlMs)) .then(async hit => { const anchor = hit?.el ?? deriveAnchor(t); const placement = hit?.placement ?? (opts.placement || ({ strategy: 'inline' } as Placement)); if (!anchor && typeof opts.fallback === 'function') { used = false; return opts.fallback(); } if (!anchor) { used = false; return; } const { host, shadow } = createShadowRootHost('mem0-root'); const unplace = applyPlacement({ container: host, anchor, placement }); const cleanup = ( opts.render && typeof opts.render === 'function' ? opts.render(shadow, host, anchor) : null ) as null | (() => void); if (!hit?.el) { try { await saveAnchorHint(opts, anchor, placement, opts.persistCache); } catch {} } const removal = new MutationObserver(() => { if (!document.contains(anchor) || !document.contains(host)) { try { unplace(); } catch {} if (cleanup) { try { cleanup(); } catch {} } try { removal.disconnect(); } catch {} used = false; } }); removal.observe(document.documentElement, { childList: true, subtree: true }); }) .catch(() => { used = false; }); }; globalThis.addEventListener('focusin', handleIntent, true); globalThis.addEventListener('keydown', handleIntent, true); globalThis.addEventListener('pointerdown', handleIntent, true); return function stop() { globalThis.removeEventListener('focusin', handleIntent, true); globalThis.removeEventListener('keydown', handleIntent, true); globalThis.removeEventListener('pointerdown', handleIntent, true); }; } /* bundled export for convenience */ export const OPENMEMORY_UI = { createShadowRootHost, findAnchor, applyPlacement, resolveCachedAnchor, saveAnchorHint, mountResilient, mountOnEditorFocus, }; export default OPENMEMORY_UI; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ES2022", "lib": ["ES2022", "DOM"], "module": "ES2022", "moduleResolution": "Bundler", "verbatimModuleSyntax": true, "strict": true, "noImplicitAny": true, "noUncheckedIndexedAccess": true, "useDefineForClassFields": true, "skipLibCheck": true, "isolatedModules": true, "allowJs": false, "checkJs": false, "outDir": "dist", "rootDir": ".", "types": ["chrome-types"] }, "include": ["**/*.ts", "manifest.json"], "exclude": ["node_modules", "icons/**", "**/*.min.js", "**/dist/**"] } ================================================ FILE: vite.config.ts ================================================ import { defineConfig } from 'vite'; import { crx } from '@crxjs/vite-plugin'; import manifest from './manifest.json'; export default defineConfig({ plugins: [crx({ manifest })], build: { outDir: 'dist', emptyOutDir: true } });