From 4d12ecebe2a845e6aefdc65cd3009b6f3cfe8b98 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Tue, 19 May 2026 16:04:50 +0530 Subject: [PATCH 1/5] revamp chrome extension --- .../entrypoints/content/chatgpt.ts | 386 ++++++++-- .../entrypoints/content/claude.ts | 372 ++++++++-- .../entrypoints/content/gemini.ts | 681 ++++++++++++++++++ .../entrypoints/content/index.ts | 11 +- .../entrypoints/content/memory-suggestion.ts | 402 +++++++++++ .../entrypoints/content/shared.ts | 26 +- .../entrypoints/popup/App.tsx | 80 +- .../entrypoints/popup/index.html | 2 +- .../entrypoints/welcome/Welcome.tsx | 176 +++-- apps/browser-extension/utils/constants.ts | 15 + apps/browser-extension/utils/ui-components.ts | 96 +-- apps/browser-extension/wxt.config.ts | 3 + 12 files changed, 1978 insertions(+), 272 deletions(-) create mode 100644 apps/browser-extension/entrypoints/content/gemini.ts create mode 100644 apps/browser-extension/entrypoints/content/memory-suggestion.ts diff --git a/apps/browser-extension/entrypoints/content/chatgpt.ts b/apps/browser-extension/entrypoints/content/chatgpt.ts index 7c3d28aaa..c51bbb3e6 100644 --- a/apps/browser-extension/entrypoints/content/chatgpt.ts +++ b/apps/browser-extension/entrypoints/content/chatgpt.ts @@ -13,18 +13,37 @@ import { createChatGPTInputBarElement, DOMUtils, } from "../../utils/ui-components" +import { + acceptMemorySuggestion, + clearMemorySuggestion, + hasAcceptedSupermemoryContext, + setMemoryMarkerStatus, + showLoadingSuggestion, + showMarkerPopover, + showMemorySuggestion, + syncAcceptedSupermemoryState, +} from "./memory-suggestion" let chatGPTDebounceTimeout: NodeJS.Timeout | null = null let chatGPTRouteObserver: MutationObserver | null = null let chatGPTUrlCheckInterval: NodeJS.Timeout | null = null let chatGPTObserverThrottle: NodeJS.Timeout | null = null +const CHATGPT_DEBUG = true +const CHATGPT_LOG_PREFIX = "[supermemory:chatgpt]" export function initializeChatGPT() { + debugChatGPT("initializeChatGPT called", { + host: window.location.hostname, + href: window.location.href, + }) + if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { + debugChatGPT("not on ChatGPT domain, skipping") return } if (document.body.hasAttribute("data-chatgpt-initialized")) { + debugChatGPT("already initialized") return } @@ -39,6 +58,18 @@ export function initializeChatGPT() { setupChatGPTRouteChangeDetection() document.body.setAttribute("data-chatgpt-initialized", "true") + debugChatGPT("initialized listeners") +} + +function debugChatGPT(message: string, data?: unknown) { + if (!CHATGPT_DEBUG) return + + if (data === undefined) { + console.log(CHATGPT_LOG_PREFIX, message) + return + } + + console.log(CHATGPT_LOG_PREFIX, message, data) } function setupChatGPTRouteChangeDetection() { @@ -58,7 +89,7 @@ function setupChatGPTRouteChangeDetection() { const checkForRouteChange = () => { if (window.location.href !== currentUrl) { currentUrl = window.location.href - console.log("ChatGPT route changed, re-adding supermemory elements") + debugChatGPT("route changed, re-adding supermemory elements", currentUrl) setTimeout(() => { addSupermemoryButtonToMemoriesDialog() addSaveChatGPTElementBeforeComposerBtn() @@ -83,8 +114,10 @@ function setupChatGPTRouteChangeDetection() { if ( element.querySelector?.("#prompt-textarea") || element.querySelector?.("button.composer-btn") || + element.querySelector?.("button") || element.querySelector?.('[role="dialog"]') || element.matches?.("#prompt-textarea") || + element.matches?.("button") || element.id === "prompt-textarea" ) { shouldRecheck = true @@ -98,6 +131,7 @@ function setupChatGPTRouteChangeDetection() { chatGPTObserverThrottle = setTimeout(() => { try { chatGPTObserverThrottle = null + debugChatGPT("DOM changed near composer, rechecking UI") addSupermemoryButtonToMemoriesDialog() addSaveChatGPTElementBeforeComposerBtn() setupChatGPTAutoFetch() @@ -124,6 +158,8 @@ function setupChatGPTRouteChangeDetection() { async function getRelatedMemoriesForChatGPT(actionSource: string) { try { + const isAutoSearch = + actionSource === POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED const userQuery = document.getElementById("prompt-textarea")?.textContent || "" @@ -138,7 +174,15 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) { return } - updateChatGPTIconFeedback("Searching memories...", iconElement) + if (isAutoSearch) { + const promptElement = document.getElementById("prompt-textarea") + if (promptElement) { + showLoadingSuggestion("chatgpt", promptElement) + } + setMemoryMarkerStatus(iconElement, "searching") + } else { + updateChatGPTIconFeedback("Searching memories...", iconElement) + } const timeoutPromise = new Promise((_, reject) => setTimeout( @@ -159,24 +203,40 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) { if (response?.success && response?.data) { const promptElement = document.getElementById("prompt-textarea") if (promptElement) { - promptElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}` + const memoryText = showMemorySuggestion( + "chatgpt", + promptElement, + response.data, + ) console.log( "Prompt element dataset:", - promptElement.dataset.supermemories, + memoryText, ) - iconElement.dataset.memoriesData = response.data + iconElement.dataset.memoriesData = String(response.data) - updateChatGPTIconFeedback("Included Memories", iconElement) + if (isAutoSearch) { + setMemoryMarkerStatus(iconElement, "found") + } else { + updateChatGPTIconFeedback("Included Memories", iconElement) + } } else { console.warn( "ChatGPT prompt element not found after successful memory fetch", ) - updateChatGPTIconFeedback("Memories found", iconElement) + if (isAutoSearch) { + setMemoryMarkerStatus(iconElement, "found") + } else { + updateChatGPTIconFeedback("Memories found", iconElement) + } } } else { console.warn("No memories found or API response invalid") - updateChatGPTIconFeedback("No memories found", iconElement) + if (isAutoSearch) { + setMemoryMarkerStatus(iconElement, "none") + } else { + updateChatGPTIconFeedback("No memories found", iconElement) + } } } catch (error) { console.error("Error getting related memories:", error) @@ -185,7 +245,13 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) { '[id*="sm-chatgpt-input-bar-element-before-composer"]', )[0] as HTMLElement if (icon) { - updateChatGPTIconFeedback("Error fetching memories", icon) + if ( + actionSource === POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED + ) { + setMemoryMarkerStatus(icon, "error") + } else { + updateChatGPTIconFeedback("Error fetching memories", icon) + } } } catch (feedbackError) { console.error("Failed to update error feedback:", feedbackError) @@ -300,6 +366,29 @@ function updateChatGPTIconFeedback( iconElement: HTMLElement, resetAfter = 0, ) { + const memories = iconElement.dataset.memoriesData + const fallbackReset = + resetAfter || (message === "Included Memories" ? 0 : 2200) + + if (message === "Included Memories" || message === "Memories found") { + setMemoryMarkerStatus(iconElement, "found") + showMarkerPopover(iconElement, "Included Memories", memories) + return + } + + if (message.toLowerCase().includes("searching")) { + setMemoryMarkerStatus(iconElement, "searching") + showMarkerPopover(iconElement, message) + return + } + + setMemoryMarkerStatus( + iconElement, + message.toLowerCase().includes("error") ? "error" : "none", + ) + showMarkerPopover(iconElement, message, undefined, fallbackReset) + return + if (!iconElement.dataset.originalHtml) { iconElement.dataset.originalHtml = iconElement.innerHTML } @@ -515,59 +604,220 @@ function updateChatGPTIconFeedback( } function addSaveChatGPTElementBeforeComposerBtn() { - const composerButtons = document.querySelectorAll("button.composer-btn") + const promptInput = getChatGPTPromptInput() + if (!promptInput) { + debugChatGPT("prompt input not found", getChatGPTDomSnapshot()) + return + } - composerButtons.forEach((button) => { - if (button.hasAttribute("data-supermemory-icon-added-before")) { - return - } + const composer = findChatGPTComposerRoot(promptInput) + if (!composer?.querySelector) { + debugChatGPT("composer root not found", describeElement(promptInput)) + return + } - const parent = button.parentElement - if (!parent) return + const existingMarkers = Array.from( + document.querySelectorAll( + `[id*="${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer"]`, + ), + ) + if (existingMarkers.length > 1) { + debugChatGPT("removed duplicate markers", existingMarkers.length) + existingMarkers.forEach((marker) => marker.remove()) + } else if (existingMarkers.length === 1) { + debugChatGPT("marker already exists") + return + } - const parentSiblings = parent.parentElement?.children - if (!parentSiblings) return + const buttons = findChatGPTComposerButtons(promptInput, composer) + debugChatGPT("candidate ChatGPT buttons", { + input: describeElement(promptInput), + composer: describeElement(composer), + buttons: buttons.map((button) => ({ + label: buttonLabel(button), + element: describeElement(button), + })), + }) - let hasSpeechButtonSibling = false - for (const sibling of parentSiblings) { - if ( - sibling.getAttribute("data-testid") === - "composer-speech-button-container" - ) { - hasSpeechButtonSibling = true - break - } - } + const micButton = buttons.find((button) => isChatGPTMicButton(button)) + const voiceButton = buttons.find((button) => isChatGPTVoiceButton(button)) + const sendButton = buttons.find((button) => isChatGPTSendButton(button)) + const anchorButton = micButton || voiceButton || sendButton || buttons[buttons.length - 1] + const anchorSlot = findChatGPTButtonSlot(anchorButton, composer) + const speechContainer = composer.querySelector( + '[data-testid="composer-speech-button-container"]', + ) as HTMLElement | null + const targetContainer = + anchorSlot?.parentElement || + speechContainer?.parentElement || + promptInput.parentElement + + if (!targetContainer) { + debugChatGPT("could not find insertion target", { + anchor: anchorButton ? describeElement(anchorButton) : null, + input: describeElement(promptInput), + }) + return + } - if (!hasSpeechButtonSibling) return + const saveChatGPTElement = createChatGPTInputBarElement(async () => { + await getRelatedMemoriesForChatGPT( + POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_SEARCHED, + ) + }) - const grandParent = parent.parentElement - if (!grandParent) return + saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` - const existingIcon = grandParent.querySelector( - `#${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer`, - ) - if (existingIcon) { - button.setAttribute("data-supermemory-icon-added-before", "true") - return + if (anchorSlot?.parentElement === targetContainer) { + targetContainer.insertBefore(saveChatGPTElement, anchorSlot) + debugChatGPT("inserted marker before anchor button", { + anchorLabel: anchorButton ? buttonLabel(anchorButton) : null, + anchorSlot: describeElement(anchorSlot), + target: describeElement(targetContainer), + }) + } else { + targetContainer.appendChild(saveChatGPTElement) + debugChatGPT("inserted marker into fallback target", { + target: describeElement(targetContainer), + }) + } + + setupChatGPTAutoFetch() +} + +function getChatGPTPromptInput(): HTMLElement | null { + return document.querySelector( + '#prompt-textarea, [data-testid="prompt-textarea"], div[contenteditable="true"]', + ) as HTMLElement | null +} + +function findChatGPTComposerRoot(input: HTMLElement): HTMLElement { + const form = input.closest("form") as HTMLElement | null + if (form) return form + + let current: HTMLElement | null = input + for (let depth = 0; current && depth < 8; depth += 1) { + if (current.querySelectorAll("button").length >= 2) { + return current } + current = current.parentElement + } - const saveChatGPTElement = createChatGPTInputBarElement(async () => { - await getRelatedMemoriesForChatGPT( - POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_SEARCHED, - ) - }) + return input.parentElement || document.body +} - saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` +function findChatGPTComposerButtons( + input: HTMLElement, + composer: HTMLElement, +): HTMLButtonElement[] { + const composerButtons = Array.from(composer.querySelectorAll("button")) + if (composerButtons.length > 0) { + return composerButtons + } - button.setAttribute("data-supermemory-icon-added-before", "true") + const inputRect = input.getBoundingClientRect() + const allButtons = Array.from(document.querySelectorAll("button")) - grandParent.insertBefore(saveChatGPTElement, parent) + return allButtons.filter((button) => { + const rect = button.getBoundingClientRect() + const verticallyNear = + Math.abs(rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2)) < + 120 + const horizontallyNear = + rect.left > inputRect.left - 80 && rect.left < inputRect.right + 260 - setupChatGPTAutoFetch() + return verticallyNear && horizontallyNear }) } +function buttonLabel(button: HTMLButtonElement): string { + return [ + button.id, + button.getAttribute("aria-label"), + button.getAttribute("title"), + button.getAttribute("data-testid"), + button.getAttribute("data-test-id"), + button.textContent, + ] + .filter(Boolean) + .join(" ") +} + +function isChatGPTMicButton(button: HTMLButtonElement): boolean { + return /mic|microphone|dictate/i.test(buttonLabel(button)) +} + +function isChatGPTVoiceButton(button: HTMLButtonElement): boolean { + return /voice|audio|speech/i.test(buttonLabel(button)) +} + +function isChatGPTSendButton(button: HTMLButtonElement): boolean { + const label = buttonLabel(button) + return /composer-submit-button|send|submit/i.test(label) +} + +function findChatGPTButtonSlot( + button: HTMLButtonElement | undefined, + composer: HTMLElement, +): HTMLElement | null { + if (!button) return null + + let current: HTMLElement | null = button + while (current?.parentElement && current.parentElement !== composer) { + const parent: HTMLElement = current.parentElement + const parentStyle = window.getComputedStyle(parent) + const hasSiblingControls = parent.children.length > 1 + const isRow = + parentStyle.display.includes("flex") && + parentStyle.flexDirection !== "column" + + if (hasSiblingControls && isRow) { + return current + } + + current = parent + } + + return current || button +} + +function describeElement(element: Element | null): string | null { + if (!element) return null + + const parts = [element.tagName.toLowerCase()] + if (element.id) parts.push(`#${element.id}`) + if (element.className && typeof element.className === "string") { + parts.push( + `.${element.className + .trim() + .split(/\s+/) + .slice(0, 4) + .join(".")}`, + ) + } + + for (const attr of ["aria-label", "data-testid", "data-test-id", "role"]) { + const value = element.getAttribute(attr) + if (value) parts.push(`[${attr}="${value}"]`) + } + + return parts.join("") +} + +function getChatGPTDomSnapshot() { + return { + promptTextareas: document.querySelectorAll("#prompt-textarea").length, + contenteditables: document.querySelectorAll('[contenteditable="true"]') + .length, + textareas: document.querySelectorAll("textarea").length, + buttons: document.querySelectorAll("button").length, + composerButtons: document.querySelectorAll("button.composer-btn").length, + speechContainers: document.querySelectorAll( + '[data-testid="composer-speech-button-container"]', + ).length, + } +} + async function setupChatGPTAutoFetch() { const autoSearch = (await autoSearchEnabled.getValue()) ?? false @@ -586,12 +836,27 @@ async function setupChatGPTAutoFetch() { promptTextarea.setAttribute("data-supermemory-auto-fetch", "true") const handleInput = () => { + const content = promptTextarea.textContent?.trim() || "" + syncAcceptedSupermemoryState(promptTextarea) + + if (content.length === 0) { + clearMemorySuggestion("chatgpt", promptTextarea) + document + .querySelectorAll('[id*="sm-chatgpt-input-bar-element-before-composer"]') + .forEach((icon) => { + setMemoryMarkerStatus(icon as HTMLElement, "neutral") + }) + } + if (chatGPTDebounceTimeout) { clearTimeout(chatGPTDebounceTimeout) } chatGPTDebounceTimeout = setTimeout(async () => { - const content = promptTextarea.textContent?.trim() || "" + if (hasAcceptedSupermemoryContext(promptTextarea)) { + clearMemorySuggestion("chatgpt", promptTextarea) + return + } if (content.length > 2) { await getRelatedMemoriesForChatGPT( @@ -604,6 +869,7 @@ async function setupChatGPTAutoFetch() { icons.forEach((icon) => { const iconElement = icon as HTMLElement + setMemoryMarkerStatus(iconElement, "neutral") if (iconElement.dataset.originalHtml) { iconElement.innerHTML = iconElement.dataset.originalHtml delete iconElement.dataset.originalHtml @@ -612,7 +878,7 @@ async function setupChatGPTAutoFetch() { }) if (promptTextarea.dataset.supermemories) { - delete promptTextarea.dataset.supermemories + clearMemorySuggestion("chatgpt", promptTextarea) } } }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY) @@ -641,16 +907,6 @@ function setupChatGPTPromptCapture() { promptContent = promptTextarea.textContent || "" } - const storedMemories = promptTextarea?.dataset.supermemories - if ( - storedMemories && - promptTextarea && - !promptContent.includes("Supermemories of user") - ) { - promptTextarea.appendChild(document.createTextNode(storedMemories)) - promptContent = promptTextarea.textContent || "" - } - if (promptTextarea && promptContent.trim()) { console.log(`ChatGPT prompt submitted via ${source}:`, promptContent) @@ -682,7 +938,7 @@ function setupChatGPTPromptCapture() { }) if (promptTextarea?.dataset.supermemories) { - delete promptTextarea.dataset.supermemories + clearMemorySuggestion("chatgpt", promptTextarea) } } @@ -705,6 +961,18 @@ function setupChatGPTPromptCapture() { async (event) => { const target = event.target as HTMLElement + if ( + (target.id === "prompt-textarea" || + target.closest("#prompt-textarea")) && + acceptMemorySuggestion( + event, + "chatgpt", + document.getElementById("prompt-textarea"), + ) + ) { + return + } + if ( target.id === "prompt-textarea" && event.key === "Enter" && diff --git a/apps/browser-extension/entrypoints/content/claude.ts b/apps/browser-extension/entrypoints/content/claude.ts index d124c84a0..60a8fffcc 100644 --- a/apps/browser-extension/entrypoints/content/claude.ts +++ b/apps/browser-extension/entrypoints/content/claude.ts @@ -13,18 +13,37 @@ import { createClaudeInputBarElement, DOMUtils, } from "../../utils/ui-components" +import { + acceptMemorySuggestion, + clearMemorySuggestion, + hasAcceptedSupermemoryContext, + setMemoryMarkerStatus, + showLoadingSuggestion, + showMarkerPopover, + showMemorySuggestion, + syncAcceptedSupermemoryState, +} from "./memory-suggestion" let claudeDebounceTimeout: NodeJS.Timeout | null = null let claudeRouteObserver: MutationObserver | null = null let claudeUrlCheckInterval: NodeJS.Timeout | null = null let claudeObserverThrottle: NodeJS.Timeout | null = null +const CLAUDE_DEBUG = true +const CLAUDE_LOG_PREFIX = "[supermemory:claude]" export function initializeClaude() { + debugClaude("initializeClaude called", { + host: window.location.hostname, + href: window.location.href, + }) + if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) { + debugClaude("not on Claude domain, skipping") return } if (document.body.hasAttribute("data-claude-initialized")) { + debugClaude("already initialized") return } @@ -38,6 +57,18 @@ export function initializeClaude() { setupClaudeRouteChangeDetection() document.body.setAttribute("data-claude-initialized", "true") + debugClaude("initialized listeners") +} + +function debugClaude(message: string, data?: unknown) { + if (!CLAUDE_DEBUG) return + + if (data === undefined) { + console.log(CLAUDE_LOG_PREFIX, message) + return + } + + console.log(CLAUDE_LOG_PREFIX, message, data) } function setupClaudeRouteChangeDetection() { @@ -57,7 +88,7 @@ function setupClaudeRouteChangeDetection() { const checkForRouteChange = () => { if (window.location.href !== currentUrl) { currentUrl = window.location.href - console.log("Claude route changed, re-adding supermemory icon") + debugClaude("route changed, re-adding supermemory icon", currentUrl) setTimeout(() => { addSupermemoryIconToClaudeInput() setupClaudeAutoFetch() @@ -81,8 +112,10 @@ function setupClaudeRouteChangeDetection() { if ( element.querySelector?.('div[contenteditable="true"]') || element.querySelector?.("textarea") || + element.querySelector?.("button") || element.matches?.('div[contenteditable="true"]') || - element.matches?.("textarea") + element.matches?.("textarea") || + element.matches?.("button") ) { shouldRecheck = true } @@ -95,6 +128,7 @@ function setupClaudeRouteChangeDetection() { claudeObserverThrottle = setTimeout(() => { try { claudeObserverThrottle = null + debugClaude("DOM changed near composer, rechecking UI") addSupermemoryIconToClaudeInput() setupClaudeAutoFetch() } catch (error) { @@ -119,39 +153,207 @@ function setupClaudeRouteChangeDetection() { } function addSupermemoryIconToClaudeInput() { - const targetContainers = document.querySelectorAll( - ".relative.flex-1.flex.items-center.gap-2.shrink.min-w-0", + const input = getClaudePromptInput() + if (!input) { + debugClaude("prompt input not found", getClaudeDomSnapshot()) + return + } + + const composer = findComposerRoot(input) + if (!composer?.querySelector) { + debugClaude("composer root not found", describeElement(input)) + return + } + + const existingMarkers = Array.from( + document.querySelectorAll(`[id*="${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}"]`), ) + if (existingMarkers.length > 1) { + debugClaude("removed duplicate markers", existingMarkers.length) + existingMarkers.forEach((marker) => marker.remove()) + } else if (existingMarkers.length === 1) { + debugClaude("marker already exists") + return + } - targetContainers.forEach((container) => { - if (container.hasAttribute("data-supermemory-icon-added")) { - return - } + const buttons = findClaudeComposerButtons(input, composer) + debugClaude("candidate Claude buttons", { + input: describeElement(input), + composer: describeElement(composer), + buttons: buttons.map((button) => ({ + label: buttonLabel(button), + element: describeElement(button), + })), + }) + + const micButton = buttons.find((button) => + isClaudeMicButton(button), + ) + const voiceButton = buttons.find((button) => isClaudeVoiceButton(button)) + const sendButton = buttons.find((button) => isClaudeSendButton(button)) + const anchorButton = micButton || voiceButton || sendButton || buttons[buttons.length - 1] + const anchorSlot = findClaudeButtonSlot(anchorButton, composer) + const targetContainer = anchorSlot?.parentElement || input.parentElement + + if (!targetContainer) { + debugClaude("could not find insertion target", { + anchor: anchorButton ? describeElement(anchorButton) : null, + input: describeElement(input), + }) + return + } - const existingIcon = container.querySelector( - `#${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}`, + const supermemoryIcon = createClaudeInputBarElement(async () => { + await getRelatedMemoriesForClaude( + POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_SEARCHED, ) - if (existingIcon) { - container.setAttribute("data-supermemory-icon-added", "true") - return - } + }) - const supermemoryIcon = createClaudeInputBarElement(async () => { - await getRelatedMemoriesForClaude( - POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_SEARCHED, - ) + supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` + + if (anchorSlot?.parentElement === targetContainer) { + targetContainer.insertBefore(supermemoryIcon, anchorSlot) + debugClaude("inserted marker before anchor button", { + anchorLabel: anchorButton ? buttonLabel(anchorButton) : null, + anchorSlot: describeElement(anchorSlot), + target: describeElement(targetContainer), }) + return + } - supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` + targetContainer.appendChild(supermemoryIcon) + debugClaude("inserted marker into fallback target", { + target: describeElement(targetContainer), + }) +} + +function getClaudePromptInput(): HTMLElement | null { + return document.querySelector( + '.ProseMirror[contenteditable="true"], div[contenteditable="true"], textarea', + ) as HTMLElement | null +} - container.setAttribute("data-supermemory-icon-added", "true") +function findComposerRoot(input: HTMLElement): HTMLElement { + return ( + (input.closest("form") as HTMLElement | null) || + (input.closest('[data-testid*="composer"]') as HTMLElement | null) || + (input.closest('[class*="composer"]') as HTMLElement | null) || + (input.closest(".relative") as HTMLElement | null) || + input.parentElement || + document.body + ) +} - container.insertBefore(supermemoryIcon, container.firstChild) +function buttonLabel(button: HTMLButtonElement): string { + return [ + button.getAttribute("aria-label"), + button.getAttribute("title"), + button.getAttribute("data-testid"), + button.getAttribute("data-test-id"), + button.textContent, + ] + .filter(Boolean) + .join(" ") +} + +function findClaudeComposerButtons( + input: HTMLElement, + composer: HTMLElement, +): HTMLButtonElement[] { + const composerButtons = Array.from(composer.querySelectorAll("button")) + if (composerButtons.length > 0) { + return composerButtons + } + + const inputRect = input.getBoundingClientRect() + const allButtons = Array.from(document.querySelectorAll("button")) + + return allButtons.filter((button) => { + const rect = button.getBoundingClientRect() + const verticallyNear = + Math.abs(rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2)) < + 120 + const horizontallyNear = + rect.left > inputRect.left - 80 && rect.left < inputRect.right + 260 + + return verticallyNear && horizontallyNear }) } +function isClaudeMicButton(button: HTMLButtonElement): boolean { + return /mic|microphone|dictate/i.test(buttonLabel(button)) +} + +function isClaudeVoiceButton(button: HTMLButtonElement): boolean { + return /voice|audio|speech/i.test(buttonLabel(button)) +} + +function isClaudeSendButton(button: HTMLButtonElement): boolean { + return /send|submit/i.test(buttonLabel(button)) +} + +function findClaudeButtonSlot( + button: HTMLButtonElement | undefined, + composer: HTMLElement, +): HTMLElement | null { + if (!button) return null + + let current: HTMLElement | null = button + while (current?.parentElement && current.parentElement !== composer) { + const parent: HTMLElement = current.parentElement + const parentStyle = window.getComputedStyle(parent) + const hasSiblingControls = parent.children.length > 1 + const isRow = + parentStyle.display.includes("flex") && + parentStyle.flexDirection !== "column" + + if (hasSiblingControls && isRow) { + return current + } + + current = parent + } + + return current || button +} + +function describeElement(element: Element | null): string | null { + if (!element) return null + + const parts = [element.tagName.toLowerCase()] + if (element.id) parts.push(`#${element.id}`) + if (element.className && typeof element.className === "string") { + parts.push( + `.${element.className + .trim() + .split(/\s+/) + .slice(0, 4) + .join(".")}`, + ) + } + + for (const attr of ["aria-label", "data-testid", "data-test-id", "role"]) { + const value = element.getAttribute(attr) + if (value) parts.push(`[${attr}="${value}"]`) + } + + return parts.join("") +} + +function getClaudeDomSnapshot() { + return { + proseMirrors: document.querySelectorAll(".ProseMirror").length, + contenteditables: document.querySelectorAll('[contenteditable="true"]') + .length, + textareas: document.querySelectorAll("textarea").length, + buttons: document.querySelectorAll("button").length, + } +} + async function getRelatedMemoriesForClaude(actionSource: string) { try { + const isAutoSearch = + actionSource === POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED let userQuery = "" const supermemoryContainer = document.querySelector( @@ -204,7 +406,15 @@ async function getRelatedMemoriesForClaude(actionSource: string) { return } - updateClaudeIconFeedback("Searching memories...", iconElement) + if (isAutoSearch) { + const input = getClaudePromptInput() + if (input) { + showLoadingSuggestion("claude", input) + } + setMemoryMarkerStatus(iconElement, "searching") + } else { + updateClaudeIconFeedback("Searching memories...", iconElement) + } const timeoutPromise = new Promise((_, reject) => setTimeout( @@ -230,24 +440,40 @@ async function getRelatedMemoriesForClaude(actionSource: string) { ) as HTMLElement if (textareaElement) { - textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}` + const memoryText = showMemorySuggestion( + "claude", + textareaElement, + response.data, + ) console.log( "Text element dataset:", - textareaElement.dataset.supermemories, + memoryText, ) - iconElement.dataset.memoriesData = response.data + iconElement.dataset.memoriesData = String(response.data) - updateClaudeIconFeedback("Included Memories", iconElement) + if (isAutoSearch) { + setMemoryMarkerStatus(iconElement, "found") + } else { + updateClaudeIconFeedback("Included Memories", iconElement) + } } else { console.warn( "Claude input area not found after successful memory fetch", ) - updateClaudeIconFeedback("Memories found", iconElement) + if (isAutoSearch) { + setMemoryMarkerStatus(iconElement, "found") + } else { + updateClaudeIconFeedback("Memories found", iconElement) + } } } else { console.warn("No memories found or API response invalid for Claude") - updateClaudeIconFeedback("No memories found", iconElement) + if (isAutoSearch) { + setMemoryMarkerStatus(iconElement, "none") + } else { + updateClaudeIconFeedback("No memories found", iconElement) + } } } catch (error) { console.error("Error getting related memories for Claude:", error) @@ -256,7 +482,13 @@ async function getRelatedMemoriesForClaude(actionSource: string) { '[id*="sm-claude-input-bar-element"]', ) as HTMLElement if (icon) { - updateClaudeIconFeedback("Error fetching memories", icon) + if ( + actionSource === POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED + ) { + setMemoryMarkerStatus(icon, "error") + } else { + updateClaudeIconFeedback("Error fetching memories", icon) + } } } catch (feedbackError) { console.error("Failed to update Claude error feedback:", feedbackError) @@ -269,6 +501,29 @@ function updateClaudeIconFeedback( iconElement: HTMLElement, resetAfter = 0, ) { + const memories = iconElement.dataset.memoriesData + const fallbackReset = + resetAfter || (message === "Included Memories" ? 0 : 2200) + + if (message === "Included Memories" || message === "Memories found") { + setMemoryMarkerStatus(iconElement, "found") + showMarkerPopover(iconElement, "Included Memories", memories) + return + } + + if (message.toLowerCase().includes("searching")) { + setMemoryMarkerStatus(iconElement, "searching") + showMarkerPopover(iconElement, message) + return + } + + setMemoryMarkerStatus( + iconElement, + message.toLowerCase().includes("error") ? "error" : "none", + ) + showMarkerPopover(iconElement, message, undefined, fallbackReset) + return + if (!iconElement.dataset.originalHtml) { iconElement.dataset.originalHtml = iconElement.innerHTML } @@ -514,17 +769,6 @@ function setupClaudePromptCapture() { } } - const storedMemories = contentEditableDiv?.dataset.supermemories - if ( - storedMemories && - contentEditableDiv && - !promptContent.includes("Supermemories of user") - ) { - contentEditableDiv.appendChild(document.createTextNode(storedMemories)) - promptContent = - contentEditableDiv.textContent || contentEditableDiv.innerText || "" - } - if (promptContent.trim()) { console.log(`Claude prompt submitted via ${source}:`, promptContent) @@ -556,7 +800,7 @@ function setupClaudePromptCapture() { }) if (contentEditableDiv?.dataset.supermemories) { - delete contentEditableDiv.dataset.supermemories + clearMemorySuggestion("claude", contentEditableDiv) } } @@ -564,14 +808,16 @@ function setupClaudePromptCapture() { "click", async (event) => { const target = event.target as HTMLElement - const sendButton = - target.closest( - "button.inline-flex.items-center.justify-center.relative.shrink-0.can-focus.select-none", - ) || - target.closest('button[class*="bg-accent-main-000"]') || - target.closest('button[class*="rounded-lg"]') - - if (sendButton) { + if (target.closest('[data-supermemory-connected-indicator="true"]')) { + return + } + + const sendButton = target.closest("button") + + if ( + sendButton && + buttonLabel(sendButton as HTMLButtonElement).match(/send|submit/i) + ) { await captureClaudePromptContent("button click") } }, @@ -583,10 +829,18 @@ function setupClaudePromptCapture() { async (event) => { const target = event.target as HTMLElement + const activeInput = + (target.closest('div[contenteditable="true"]') as HTMLElement | null) || + (target.matches("textarea") ? (target as HTMLTextAreaElement) : null) + if (acceptMemorySuggestion(event, "claude", activeInput)) { + return + } + if ( (target.matches('div[contenteditable="true"]') || target.matches(".ProseMirror") || target.matches("textarea") || + target.closest('div[contenteditable="true"]') || target.closest(".ProseMirror")) && event.key === "Enter" && !event.shiftKey @@ -618,12 +872,27 @@ async function setupClaudeAutoFetch() { textareaElement.setAttribute("data-supermemory-auto-fetch", "true") const handleInput = () => { + const content = textareaElement.textContent?.trim() || "" + syncAcceptedSupermemoryState(textareaElement) + + if (content.length === 0) { + clearMemorySuggestion("claude", textareaElement) + document + .querySelectorAll('[id*="sm-claude-input-bar-element"]') + .forEach((icon) => { + setMemoryMarkerStatus(icon as HTMLElement, "neutral") + }) + } + if (claudeDebounceTimeout) { clearTimeout(claudeDebounceTimeout) } claudeDebounceTimeout = setTimeout(async () => { - const content = textareaElement.textContent?.trim() || "" + if (hasAcceptedSupermemoryContext(textareaElement)) { + clearMemorySuggestion("claude", textareaElement) + return + } if (content.length > 2) { await getRelatedMemoriesForClaude( @@ -636,6 +905,7 @@ async function setupClaudeAutoFetch() { icons.forEach((icon) => { const iconElement = icon as HTMLElement + setMemoryMarkerStatus(iconElement, "neutral") if (iconElement.dataset.originalHtml) { iconElement.innerHTML = iconElement.dataset.originalHtml delete iconElement.dataset.originalHtml @@ -644,7 +914,7 @@ async function setupClaudeAutoFetch() { }) if (textareaElement.dataset.supermemories) { - delete textareaElement.dataset.supermemories + clearMemorySuggestion("claude", textareaElement) } } }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY) diff --git a/apps/browser-extension/entrypoints/content/gemini.ts b/apps/browser-extension/entrypoints/content/gemini.ts new file mode 100644 index 000000000..64254cbbe --- /dev/null +++ b/apps/browser-extension/entrypoints/content/gemini.ts @@ -0,0 +1,681 @@ +import { + DOMAINS, + ELEMENT_IDS, + MESSAGE_TYPES, + POSTHOG_EVENT_KEY, + UI_CONFIG, +} from "../../utils/constants" +import { + autoCapturePromptsEnabled, + autoSearchEnabled, +} from "../../utils/storage" +import { + createGeminiInputBarElement, + DOMUtils, +} from "../../utils/ui-components" +import { + acceptMemorySuggestion, + clearMemorySuggestion, + hasAcceptedSupermemoryContext, + setMemoryMarkerStatus, + showLoadingSuggestion, + showMarkerPopover, + showMemorySuggestion, + syncAcceptedSupermemoryState, +} from "./memory-suggestion" + +let geminiDebounceTimeout: NodeJS.Timeout | null = null +let geminiRouteObserver: MutationObserver | null = null +let geminiUrlCheckInterval: NodeJS.Timeout | null = null +let geminiObserverThrottle: NodeJS.Timeout | null = null +const GEMINI_DEBUG = true +const GEMINI_LOG_PREFIX = "[supermemory:gemini]" + +type GeminiInput = HTMLElement | HTMLTextAreaElement + +export function initializeGemini() { + debugGemini("initializeGemini called", { + host: window.location.hostname, + href: window.location.href, + }) + + if (!DOMUtils.isOnDomain(DOMAINS.GEMINI)) { + debugGemini("not on Gemini domain, skipping") + return + } + + if (document.body.hasAttribute("data-gemini-initialized")) { + debugGemini("already initialized") + return + } + + setTimeout(() => { + addSupermemoryIconToGeminiInput() + setupGeminiAutoFetch() + }, 2000) + + setupGeminiPromptCapture() + setupGeminiRouteChangeDetection() + + document.body.setAttribute("data-gemini-initialized", "true") + debugGemini("initialized listeners") +} + +function debugGemini(message: string, data?: unknown) { + if (!GEMINI_DEBUG) return + + if (data === undefined) { + console.log(GEMINI_LOG_PREFIX, message) + return + } + + console.log(GEMINI_LOG_PREFIX, message, data) +} + +function setupGeminiRouteChangeDetection() { + if (geminiRouteObserver) { + geminiRouteObserver.disconnect() + } + if (geminiUrlCheckInterval) { + clearInterval(geminiUrlCheckInterval) + } + if (geminiObserverThrottle) { + clearTimeout(geminiObserverThrottle) + geminiObserverThrottle = null + } + + let currentUrl = window.location.href + + const recheckGeminiUI = () => { + addSupermemoryIconToGeminiInput() + setupGeminiAutoFetch() + } + + const checkForRouteChange = () => { + if (window.location.href !== currentUrl) { + currentUrl = window.location.href + debugGemini("route changed, rechecking UI", currentUrl) + setTimeout(recheckGeminiUI, 1000) + } + } + + geminiUrlCheckInterval = setInterval(checkForRouteChange, 2000) + + geminiRouteObserver = new MutationObserver((mutations) => { + if (geminiObserverThrottle) { + return + } + + const shouldRecheck = mutations.some((mutation) => + Array.from(mutation.addedNodes).some((node) => { + if (node.nodeType !== Node.ELEMENT_NODE) { + return false + } + + const element = node as Element + return ( + element.matches?.("rich-textarea, textarea, button") || + element.matches?.('[contenteditable="true"]') || + !!element.querySelector?.( + 'rich-textarea, textarea, button, [contenteditable="true"]', + ) + ) + }), + ) + + if (shouldRecheck) { + geminiObserverThrottle = setTimeout(() => { + geminiObserverThrottle = null + debugGemini("DOM changed near Gemini composer, rechecking UI") + recheckGeminiUI() + }, 300) + } + }) + + try { + geminiRouteObserver.observe(document.body, { + childList: true, + subtree: true, + }) + } catch (error) { + console.error("Failed to set up Gemini route observer:", error) + if (geminiUrlCheckInterval) { + clearInterval(geminiUrlCheckInterval) + } + geminiUrlCheckInterval = setInterval(checkForRouteChange, 1000) + } +} + +function addSupermemoryIconToGeminiInput() { + const input = getGeminiPromptInput() + if (!input) { + debugGemini("prompt input not found", getGeminiDomSnapshot()) + return + } + + const composer = findGeminiComposerRoot(input) + if (!composer?.querySelector) { + debugGemini("composer root not found", describeElement(input)) + return + } + + const existingMarkers = Array.from( + document.querySelectorAll(`[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`), + ) + if (existingMarkers.length > 1) { + debugGemini("removed duplicate markers", existingMarkers.length) + existingMarkers.forEach((marker) => marker.remove()) + } else if (existingMarkers.length === 1) { + debugGemini("marker already exists") + return + } + + const buttons = findGeminiComposerButtons(input, composer) + debugGemini("candidate Gemini buttons", { + input: describeElement(input), + composer: describeElement(composer), + buttons: buttons.map((button) => ({ + label: buttonLabel(button), + element: describeElement(button), + })), + }) + + const micButton = buttons.find((button) => + isGeminiMicButton(button), + ) + const sendButton = buttons.find((button) => isGeminiSendButton(button)) + const anchorButton = micButton || sendButton || buttons[buttons.length - 1] + const anchorSlot = findGeminiButtonSlot(anchorButton, composer) + const targetContainer = + anchorSlot?.parentElement || + (input.closest("rich-textarea") as HTMLElement | null)?.parentElement || + input.parentElement + + if (!targetContainer) { + debugGemini("could not find insertion target", { + anchor: anchorButton ? describeElement(anchorButton) : null, + input: describeElement(input), + }) + return + } + + const supermemoryIcon = createGeminiInputBarElement(async () => { + await getRelatedMemoriesForGemini( + POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_SEARCHED, + ) + }) + + supermemoryIcon.id = `${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` + + if (anchorSlot?.parentElement === targetContainer) { + targetContainer.insertBefore(supermemoryIcon, anchorSlot) + debugGemini("inserted marker before anchor button", { + anchorLabel: anchorButton ? buttonLabel(anchorButton) : null, + anchorSlot: describeElement(anchorSlot), + target: describeElement(targetContainer), + }) + return + } + + targetContainer.appendChild(supermemoryIcon) + debugGemini("inserted marker into fallback target", { + target: describeElement(targetContainer), + }) +} + +function getGeminiPromptInput(): GeminiInput | null { + return document.querySelector( + 'rich-textarea .ql-editor[contenteditable="true"], rich-textarea [contenteditable="true"], .ql-editor[contenteditable="true"], div[contenteditable="true"], textarea', + ) as GeminiInput | null +} + +function findGeminiComposerRoot(input: GeminiInput): HTMLElement { + const form = input.closest("form") as HTMLElement | null + if (form) return form + + let current: HTMLElement | null = input + for (let depth = 0; current && depth < 8; depth += 1) { + if (current.querySelectorAll("button").length >= 2) { + return current + } + current = current.parentElement + } + + return input.parentElement || document.body +} + +function findGeminiComposerButtons( + input: GeminiInput, + composer: HTMLElement, +): HTMLButtonElement[] { + const composerButtons = Array.from(composer.querySelectorAll("button")) + if (composerButtons.length > 0) { + return composerButtons + } + + const inputRect = input.getBoundingClientRect() + const allButtons = Array.from(document.querySelectorAll("button")) + + return allButtons.filter((button) => { + const rect = button.getBoundingClientRect() + const verticallyNear = + Math.abs(rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2)) < + 120 + const horizontallyNear = + rect.left > inputRect.left - 80 && rect.left < inputRect.right + 240 + + return verticallyNear && horizontallyNear + }) +} + +function buttonLabel(button: HTMLButtonElement): string { + return [ + button.getAttribute("aria-label"), + button.getAttribute("title"), + button.getAttribute("data-testid"), + button.getAttribute("data-test-id"), + button.getAttribute("jsname"), + button.textContent, + ] + .filter(Boolean) + .join(" ") +} + +function isGeminiMicButton(button: HTMLButtonElement): boolean { + return /mic|microphone|voice|dictate|audio/i.test(buttonLabel(button)) +} + +function isGeminiSendButton(button: HTMLButtonElement): boolean { + const label = buttonLabel(button) + if (/send|submit/i.test(label)) { + return true + } + + return !!button.querySelector( + 'mat-icon[fonticon="send"], mat-icon[data-mat-icon-name="send"], [data-icon-name="send"]', + ) +} + +function findGeminiButtonSlot( + button: HTMLButtonElement | undefined, + composer: HTMLElement, +): HTMLElement | null { + if (!button) return null + + let current: HTMLElement | null = button + while (current?.parentElement && current.parentElement !== composer) { + const parent: HTMLElement = current.parentElement + const parentStyle = window.getComputedStyle(parent) + const hasSiblingControls = parent.children.length > 1 + const isRow = + parentStyle.display.includes("flex") && + parentStyle.flexDirection !== "column" + + if (hasSiblingControls && isRow) { + return current + } + + current = parent + } + + return current || button +} + +function describeElement(element: Element | null): string | null { + if (!element) return null + + const parts = [element.tagName.toLowerCase()] + if (element.id) parts.push(`#${element.id}`) + if (element.className && typeof element.className === "string") { + parts.push( + `.${element.className + .trim() + .split(/\s+/) + .slice(0, 4) + .join(".")}`, + ) + } + + for (const attr of ["aria-label", "data-testid", "data-test-id", "role"]) { + const value = element.getAttribute(attr) + if (value) parts.push(`[${attr}="${value}"]`) + } + + return parts.join("") +} + +function getGeminiDomSnapshot() { + return { + richTextareas: document.querySelectorAll("rich-textarea").length, + qlEditors: document.querySelectorAll(".ql-editor").length, + contenteditables: document.querySelectorAll('[contenteditable="true"]') + .length, + textareas: document.querySelectorAll("textarea").length, + buttons: document.querySelectorAll("button").length, + } +} + +function getInputText(input: GeminiInput | null): string { + if (!input) return "" + if (input instanceof HTMLTextAreaElement) { + return input.value || "" + } + + return input.innerText || input.textContent || "" +} + +function appendStoredMemories(input: GeminiInput, storedMemories: string) { + if (input instanceof HTMLTextAreaElement) { + const promptContent = input.value || "" + input.value = `${promptContent}${storedMemories}` + input.dispatchEvent(new Event("input", { bubbles: true })) + return input.value + } + + input.appendChild(document.createTextNode(storedMemories)) + input.dispatchEvent( + new InputEvent("input", { bubbles: true, inputType: "insertText" }), + ) + return input.innerText || input.textContent || "" +} + +async function getRelatedMemoriesForGemini(actionSource: string) { + try { + const isAutoSearch = + actionSource === POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_AUTO_SEARCHED + const input = getGeminiPromptInput() + const userQuery = getInputText(input).trim() + debugGemini("manual/auto memory search requested", { + actionSource, + hasInput: !!input, + queryLength: userQuery.length, + }) + + if (!userQuery) { + debugGemini("memory search skipped because query is empty") + return + } + + const iconElement = document.querySelector( + `[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`, + ) as HTMLElement | null + + if (!iconElement) { + console.warn("Gemini icon element not found, cannot update feedback") + return + } + + if (input && isAutoSearch) { + showLoadingSuggestion("gemini", input) + } + setMemoryMarkerStatus(iconElement, "searching") + if (!isAutoSearch) { + updateGeminiIconFeedback("Searching memories...", iconElement) + } + + const timeoutPromise = new Promise((_, reject) => + setTimeout( + () => reject(new Error("Memory search timeout")), + UI_CONFIG.API_REQUEST_TIMEOUT, + ), + ) + + const response = (await Promise.race([ + browser.runtime.sendMessage({ + action: MESSAGE_TYPES.GET_RELATED_MEMORIES, + data: userQuery, + actionSource, + }), + timeoutPromise, + ])) as { success?: boolean; data?: string } + + debugGemini("memory search response", response) + + if (response?.success && response?.data && input) { + const memoryText = showMemorySuggestion("gemini", input, response.data) + iconElement.dataset.memoriesData = String(response.data) + iconElement.dataset.supermemories = memoryText + if (isAutoSearch) { + setMemoryMarkerStatus(iconElement, "found") + } else { + updateGeminiIconFeedback("Included Memories", iconElement) + } + return + } + + if (isAutoSearch) { + setMemoryMarkerStatus(iconElement, "none") + } else { + updateGeminiIconFeedback("No memories found", iconElement, 1800) + } + } catch (error) { + console.error("Error getting related memories for Gemini:", error) + const iconElement = document.querySelector( + `[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`, + ) as HTMLElement | null + if (iconElement) { + if ( + actionSource === POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_AUTO_SEARCHED + ) { + setMemoryMarkerStatus(iconElement, "error") + } else { + updateGeminiIconFeedback("Error fetching memories", iconElement, 1800) + } + } + } +} + +function updateGeminiIconFeedback( + message: string, + iconElement: HTMLElement, + resetAfter = 0, +) { + const memories = iconElement.dataset.memoriesData + const fallbackReset = + resetAfter || (message === "Included Memories" ? 0 : 2200) + + if (message === "Included Memories" || message === "Memories found") { + setMemoryMarkerStatus(iconElement, "found") + showMarkerPopover(iconElement, "Included Memories", memories) + return + } + + if (message.toLowerCase().includes("searching")) { + setMemoryMarkerStatus(iconElement, "searching") + showMarkerPopover(iconElement, message) + return + } + + setMemoryMarkerStatus( + iconElement, + message.toLowerCase().includes("error") ? "error" : "none", + ) + showMarkerPopover(iconElement, message, undefined, fallbackReset) +} + +function setupGeminiPromptCapture() { + if (document.body.hasAttribute("data-gemini-prompt-capture-setup")) { + return + } + + document.body.setAttribute("data-gemini-prompt-capture-setup", "true") + + const captureGeminiPromptContent = async (source: string) => { + const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false + debugGemini("capture requested", { source, autoCapture }) + + if (!autoCapture) { + console.log("Auto capture prompts is disabled, skipping prompt capture") + return + } + + const input = getGeminiPromptInput() + let promptContent = getInputText(input) + debugGemini("capture input state", { + hasInput: !!input, + promptLength: promptContent.length, + hasStoredMemories: !!input?.dataset.supermemories, + }) + + if (promptContent.trim()) { + try { + const response = await browser.runtime.sendMessage({ + action: MESSAGE_TYPES.CAPTURE_PROMPT, + data: { + prompt: promptContent, + platform: "gemini", + source, + }, + }) + debugGemini("capture response", response) + } catch (error) { + console.error("Error sending Gemini prompt to background:", error) + } + } else { + debugGemini("capture skipped because prompt is empty") + } + + const icons = document.querySelectorAll( + `[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`, + ) + + icons.forEach((icon) => { + const iconElement = icon as HTMLElement + iconElement + .querySelector("[data-supermemory-status-badge]") + ?.remove() + delete iconElement.dataset.supermemoryStatus + delete iconElement.dataset.memoriesData + if (iconElement.dataset.originalHtml) { + iconElement.innerHTML = iconElement.dataset.originalHtml + delete iconElement.dataset.originalHtml + } + }) + + if (input?.dataset.supermemories) { + clearMemorySuggestion("gemini", input) + } + } + + document.addEventListener( + "click", + async (event) => { + const target = event.target as HTMLElement + if (target.closest('[data-supermemory-connected-indicator="true"]')) { + return + } + + const sendButton = target.closest("button") + if (sendButton && isGeminiSendButton(sendButton as HTMLButtonElement)) { + debugGemini("send button click detected", { + label: buttonLabel(sendButton as HTMLButtonElement), + element: describeElement(sendButton), + }) + await captureGeminiPromptContent("button click") + } + }, + true, + ) + + document.addEventListener( + "keydown", + async (event) => { + const target = event.target as HTMLElement + + const activeInput = + (target.closest('[contenteditable="true"]') as GeminiInput | null) || + (target.matches("textarea") ? (target as HTMLTextAreaElement) : null) + if (acceptMemorySuggestion(event, "gemini", activeInput)) { + return + } + + if ( + (target.matches("textarea") || + target.matches('[contenteditable="true"]') || + target.closest('[contenteditable="true"]')) && + event.key === "Enter" && + !event.shiftKey + ) { + debugGemini("Enter submit detected", { + target: describeElement(target), + }) + await captureGeminiPromptContent("Enter key") + } + }, + true, + ) +} + +async function setupGeminiAutoFetch() { + const autoSearch = (await autoSearchEnabled.getValue()) ?? false + debugGemini("setup auto fetch", { autoSearch }) + if (!autoSearch) { + return + } + + const input = getGeminiPromptInput() + if (!input || input.hasAttribute("data-supermemory-auto-fetch")) { + debugGemini("auto fetch skipped", { + hasInput: !!input, + alreadyAttached: input?.hasAttribute("data-supermemory-auto-fetch"), + }) + return + } + + input.setAttribute("data-supermemory-auto-fetch", "true") + debugGemini("auto fetch attached", describeElement(input)) + + const handleInput = () => { + const content = getInputText(input).trim() + syncAcceptedSupermemoryState(input) + + if (content.length === 0) { + clearMemorySuggestion("gemini", input) + document + .querySelectorAll(`[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`) + .forEach((icon) => { + setMemoryMarkerStatus(icon as HTMLElement, "neutral") + }) + } + + if (geminiDebounceTimeout) { + clearTimeout(geminiDebounceTimeout) + } + + geminiDebounceTimeout = setTimeout(async () => { + if (hasAcceptedSupermemoryContext(input)) { + clearMemorySuggestion("gemini", input) + return + } + + if (content.length > 2) { + await getRelatedMemoriesForGemini( + POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_AUTO_SEARCHED, + ) + } else if (content.length === 0) { + const icons = document.querySelectorAll( + `[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`, + ) + + icons.forEach((icon) => { + const iconElement = icon as HTMLElement + iconElement + .querySelector("[data-supermemory-status-badge]") + ?.remove() + delete iconElement.dataset.supermemoryStatus + delete iconElement.dataset.memoriesData + if (iconElement.dataset.originalHtml) { + iconElement.innerHTML = iconElement.dataset.originalHtml + delete iconElement.dataset.originalHtml + } + }) + + if (input.dataset.supermemories) { + clearMemorySuggestion("gemini", input) + } + } + }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY) + } + + input.addEventListener("input", handleInput) +} diff --git a/apps/browser-extension/entrypoints/content/index.ts b/apps/browser-extension/entrypoints/content/index.ts index cba33ce28..9d4b3cb28 100644 --- a/apps/browser-extension/entrypoints/content/index.ts +++ b/apps/browser-extension/entrypoints/content/index.ts @@ -2,6 +2,7 @@ import { DOMAINS, MESSAGE_TYPES } from "../../utils/constants" import { DOMUtils } from "../../utils/ui-components" import { initializeChatGPT } from "./chatgpt" import { initializeClaude } from "./claude" +import { initializeGemini } from "./gemini" import { saveMemory, setupGlobalKeyboardShortcut, @@ -19,13 +20,13 @@ export default defineContentScript({ matches: [""], main() { // Setup global event listeners - browser.runtime.onMessage.addListener(async (message) => { + browser.runtime.onMessage.addListener((message) => { if (message.action === MESSAGE_TYPES.SHOW_TOAST) { DOMUtils.showToast(message.state) } else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { - await saveMemory() + return saveMemory(message.actionSource || "content_script") } else if (message.action === MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL) { - await openImportModal() + return openImportModal() } else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { updateTwitterImportUI(message) } else if (message.type === MESSAGE_TYPES.IMPORT_DONE) { @@ -48,6 +49,9 @@ export default defineContentScript({ if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) { initializeClaude() } + if (DOMUtils.isOnDomain(DOMAINS.GEMINI)) { + initializeGemini() + } if (DOMUtils.isOnDomain(DOMAINS.T3)) { initializeT3() } @@ -65,6 +69,7 @@ export default defineContentScript({ // Initialize platform-specific functionality initializeChatGPT() initializeClaude() + initializeGemini() initializeT3() initializeTwitter() diff --git a/apps/browser-extension/entrypoints/content/memory-suggestion.ts b/apps/browser-extension/entrypoints/content/memory-suggestion.ts new file mode 100644 index 000000000..58714c7f7 --- /dev/null +++ b/apps/browser-extension/entrypoints/content/memory-suggestion.ts @@ -0,0 +1,402 @@ +type SuggestionInput = HTMLElement | HTMLTextAreaElement + +const SUGGESTION_ATTR = "data-supermemory-memory-suggestion" +const SUPERMEMORY_PREFIX = "Supermemories of user (only for the reference):" +const SUPERMEMORY_BLUE = "#1A88FF" + +export function buildSupermemoryText(memories: unknown): string { + const memoryText = Array.isArray(memories) + ? memories.join("").trim() + : String(memories || "").trim() + + return `\n\n${SUPERMEMORY_PREFIX} ${memoryText}` +} + +export function showMemorySuggestion( + platform: string, + input: SuggestionInput, + memories: unknown, +): string { + const suggestionText = buildSupermemoryText(memories) + input.dataset.supermemories = suggestionText + delete input.dataset.supermemoriesInjected + + removeMemorySuggestion(platform) + + const anchor = getSuggestionAnchor(input) + if (!anchor) return suggestionText + + const previousPosition = window.getComputedStyle(anchor).position + if (previousPosition === "static") { + anchor.dataset.supermemoryPreviousPosition = "static" + anchor.style.position = "relative" + } + + const suggestion = createSuggestionContainer(platform, input, anchor) + suggestion.dataset.supermemorySuggestionState = "ready" + suggestion.style.gap = "8px" + suggestion.style.alignItems = "center" + + const text = document.createElement("span") + text.style.cssText = ` + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + ` + text.textContent = suggestionText.trim() + + const tabKey = document.createElement("span") + tabKey.style.cssText = ` + display: inline-flex; + align-items: center; + justify-content: center; + height: 20px; + padding: 0 8px; + border-radius: 999px; + background: ${SUPERMEMORY_BLUE}; + color: #FFFFFF; + font-size: 11px; + font-weight: 700; + line-height: 1; + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16) inset, 0 6px 18px rgba(26, 136, 255, 0.24); + flex-shrink: 0; + ` + tabKey.textContent = "Tab" + + suggestion.appendChild(text) + suggestion.appendChild(tabKey) + anchor.appendChild(suggestion) + + return suggestionText +} + +export function showLoadingSuggestion(platform: string, input: SuggestionInput) { + removeMemorySuggestion(platform) + + const anchor = getSuggestionAnchor(input) + if (!anchor) return + + const previousPosition = window.getComputedStyle(anchor).position + if (previousPosition === "static") { + anchor.dataset.supermemoryPreviousPosition = "static" + anchor.style.position = "relative" + } + + ensureSuggestionAnimationStyle() + + const suggestion = createSuggestionContainer(platform, input, anchor) + suggestion.dataset.supermemorySuggestionState = "loading" + suggestion.style.gap = "4px" + suggestion.setAttribute("aria-label", "supermemory searching memories") + + for (let index = 0; index < 3; index += 1) { + const dot = document.createElement("span") + dot.style.cssText = ` + width: 5px; + height: 5px; + border-radius: 999px; + background: ${SUPERMEMORY_BLUE}; + animation: supermemorySuggestionDot 1s ease-in-out infinite; + animation-delay: ${index * 0.14}s; + ` + suggestion.appendChild(dot) + } + + anchor.appendChild(suggestion) +} + +function createSuggestionContainer( + platform: string, + input: SuggestionInput, + anchor: HTMLElement, +): HTMLDivElement { + const suggestion = document.createElement("div") + suggestion.setAttribute(SUGGESTION_ATTR, platform) + const position = getCaretPosition(input, anchor) + const verticalOffset = platform === "gemini" ? -10 : 0 + suggestion.style.cssText = ` + position: absolute; + left: ${position.left + 6}px; + top: ${position.top + verticalOffset}px; + max-width: min(540px, calc(100% - ${position.left + 220}px)); + display: inline-flex; + align-items: center; + height: 22px; + color: rgba(255, 255, 255, 0.34); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + line-height: 1.35; + pointer-events: none; + z-index: 2147483646; + ` + return suggestion +} + +export function removeMemorySuggestion(platform: string) { + document + .querySelectorAll(`[${SUGGESTION_ATTR}="${platform}"]`) + .forEach((element) => element.remove()) +} + +export function acceptMemorySuggestion( + event: KeyboardEvent, + platform: string, + input: SuggestionInput | null, +): boolean { + if (event.key !== "Tab" || !input?.dataset.supermemories) { + return false + } + + event.preventDefault() + event.stopPropagation() + + const text = input.dataset.supermemories + appendTextToInput(input, text) + delete input.dataset.supermemories + input.dataset.supermemoriesInjected = "true" + removeMemorySuggestion(platform) + + return true +} + +export function hasAcceptedSupermemoryContext(input: SuggestionInput | null): boolean { + if (!input) return false + const text = + input instanceof HTMLTextAreaElement + ? input.value + : input.innerText || input.textContent || "" + + return text.includes(SUPERMEMORY_PREFIX) +} + +export function syncAcceptedSupermemoryState(input: SuggestionInput | null) { + if (!input?.dataset.supermemoriesInjected) return + + if (!hasAcceptedSupermemoryContext(input)) { + delete input.dataset.supermemoriesInjected + } +} + +export function clearMemorySuggestion(platform: string, input: SuggestionInput | null) { + removeMemorySuggestion(platform) + if (input?.dataset.supermemories) { + delete input.dataset.supermemories + } + if (input?.dataset.supermemoriesInjected) { + delete input.dataset.supermemoriesInjected + } +} + +export function setMemoryMarkerStatus( + iconElement: HTMLElement | null, + status: "neutral" | "searching" | "found" | "none" | "error", +) { + if (!iconElement) return + + iconElement + .querySelector("[data-supermemory-status-badge]") + ?.remove() + + if (status === "neutral" || status === "none") { + delete iconElement.dataset.supermemoryStatus + return + } + + iconElement.dataset.supermemoryStatus = status + const badge = document.createElement("span") + badge.dataset.supermemoryStatusBadge = "true" + badge.style.cssText = ` + position: absolute; + top: 3px; + right: 3px; + width: ${status === "searching" ? "7px" : "8px"}; + height: ${status === "searching" ? "7px" : "8px"}; + border-radius: 999px; + background: ${status === "found" ? "#36F3D7" : status === "searching" ? SUPERMEMORY_BLUE : status === "error" ? "#EF4444" : "rgba(255, 255, 255, 0.55)"}; + border: 1px solid rgba(5, 7, 10, 0.9); + box-shadow: ${status === "found" ? "0 0 0 2px rgba(54, 243, 215, 0.18)" : "none"}; + pointer-events: none; + ` + iconElement.appendChild(badge) +} + +export function showMarkerPopover( + iconElement: HTMLElement, + message: string, + memories?: string, + resetAfter = 0, +) { + iconElement + .querySelector("[data-supermemory-marker-popover]") + ?.remove() + ensureSuggestionAnimationStyle() + + const popover = document.createElement("div") + popover.dataset.supermemoryMarkerPopover = "true" + popover.style.cssText = ` + position: absolute; + right: 0; + bottom: calc(100% + 10px); + min-width: 168px; + max-width: 280px; + padding: 10px; + border-radius: 12px; + background: rgba(10, 14, 20, 0.96); + border: 1px solid rgba(255, 255, 255, 0.12); + color: #FAFAFA; + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.32); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 12px; + line-height: 1.35; + text-align: left; + z-index: 2147483647; + pointer-events: auto; + ` + + const title = document.createElement("div") + title.style.cssText = ` + display: flex; + align-items: center; + gap: 6px; + font-weight: 700; + margin-bottom: ${memories ? "8px" : "0"}; + ` + + if (message.toLowerCase().includes("searching")) { + const dots = document.createElement("span") + dots.style.cssText = "display: inline-flex; gap: 3px; align-items: center;" + for (let index = 0; index < 3; index += 1) { + const dot = document.createElement("span") + dot.style.cssText = ` + width: 4px; + height: 4px; + border-radius: 999px; + background: ${SUPERMEMORY_BLUE}; + animation: supermemorySuggestionDot 1s ease-in-out infinite; + animation-delay: ${index * 0.14}s; + ` + dots.appendChild(dot) + } + title.appendChild(dots) + } + + const titleText = document.createElement("span") + titleText.textContent = + message === "Included Memories" ? "Included memories" : message + title.appendChild(titleText) + popover.appendChild(title) + + if (memories) { + const list = document.createElement("div") + list.style.cssText = ` + display: flex; + flex-direction: column; + gap: 6px; + max-height: 160px; + overflow-y: auto; + color: rgba(255, 255, 255, 0.76); + ` + + memories + .split(/[,\n]/) + .map((memory) => memory.trim()) + .filter((memory) => memory.length > 0 && memory !== ",") + .slice(0, 5) + .forEach((memory) => { + const item = document.createElement("div") + item.textContent = memory + list.appendChild(item) + }) + + popover.appendChild(list) + } + + iconElement.appendChild(popover) + + if (resetAfter > 0) { + setTimeout(() => { + popover.remove() + }, resetAfter) + } +} + +function ensureSuggestionAnimationStyle() { + if (document.getElementById("supermemory-suggestion-animation-style")) { + return + } + + const style = document.createElement("style") + style.id = "supermemory-suggestion-animation-style" + style.textContent = ` + @keyframes supermemorySuggestionDot { + 0%, 80%, 100% { opacity: 0.3; transform: translateY(0); } + 40% { opacity: 1; transform: translateY(-1px); } + } + ` + document.head.appendChild(style) +} + +function getSuggestionAnchor(input: SuggestionInput): HTMLElement | null { + return ( + (input.closest("form") as HTMLElement | null) || + (input.closest('[role="textbox"]') as HTMLElement | null)?.parentElement || + input.parentElement + ) +} + +function getCaretPosition(input: SuggestionInput, anchor: HTMLElement) { + const anchorRect = anchor.getBoundingClientRect() + + if (!(input instanceof HTMLTextAreaElement)) { + const selection = window.getSelection() + if (selection?.rangeCount) { + const range = selection.getRangeAt(0).cloneRange() + if (input.contains(range.startContainer)) { + range.collapse(true) + let rect = range.getBoundingClientRect() + if (rect.width === 0 && rect.height === 0) { + const marker = document.createElement("span") + marker.textContent = "\u200b" + range.insertNode(marker) + rect = marker.getBoundingClientRect() + marker.remove() + } + + if (rect.width || rect.height) { + return { + left: Math.max(18, rect.right - anchorRect.left + 4), + top: Math.max(10, rect.top - anchorRect.top), + } + } + } + } + } + + const inputRect = input.getBoundingClientRect() + return { + left: Math.max(18, inputRect.left - anchorRect.left + 18), + top: Math.max(10, inputRect.top - anchorRect.top + 8), + } +} + +function appendTextToInput(input: SuggestionInput, text: string) { + if (input instanceof HTMLTextAreaElement) { + input.value = `${input.value}${text}` + input.dispatchEvent(new Event("input", { bubbles: true })) + return + } + + input.focus() + const selection = window.getSelection() + const range = document.createRange() + range.selectNodeContents(input) + range.collapse(false) + range.insertNode(document.createTextNode(text)) + range.collapse(false) + selection?.removeAllRanges() + selection?.addRange(range) + input.dispatchEvent( + new InputEvent("input", { bubbles: true, inputType: "insertText" }), + ) +} diff --git a/apps/browser-extension/entrypoints/content/shared.ts b/apps/browser-extension/entrypoints/content/shared.ts index 68d117a1a..5c56eaa83 100644 --- a/apps/browser-extension/entrypoints/content/shared.ts +++ b/apps/browser-extension/entrypoints/content/shared.ts @@ -1,9 +1,12 @@ import { MESSAGE_TYPES } from "../../utils/constants" import { bearerToken, userData } from "../../utils/storage" +import type { APIResponse } from "../../utils/types" import { DOMUtils } from "../../utils/ui-components" import { default as TurndownService } from "turndown" -export async function saveMemory() { +export async function saveMemory( + actionSource = "content_script", +): Promise { try { DOMUtils.showToast("loading") @@ -64,21 +67,30 @@ export async function saveMemory() { data.markdown = markdown } - const response = await browser.runtime.sendMessage({ + const response = (await browser.runtime.sendMessage({ action: MESSAGE_TYPES.SAVE_MEMORY, data, - actionSource: "context_menu", - }) + actionSource, + })) as APIResponse - console.log("Response from enxtension:", response) - if (response.success) { + console.log("Response from extension:", response) + if (response?.success) { DOMUtils.showToast("success") + return response } else { DOMUtils.showToast("error") + return { + success: false, + error: response?.error || "Failed to save memory", + } } } catch (error) { console.error("Error saving memory:", error) DOMUtils.showToast("error") + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + } } } @@ -90,7 +102,7 @@ export function setupGlobalKeyboardShortcut() { event.key === "m" ) { event.preventDefault() - await saveMemory() + await saveMemory("keyboard_shortcut") } }) } diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx index 498a75a2c..9d9d1eb47 100644 --- a/apps/browser-extension/entrypoints/popup/App.tsx +++ b/apps/browser-extension/entrypoints/popup/App.tsx @@ -2,7 +2,12 @@ import { useQueryClient } from "@tanstack/react-query" import { useEffect, useState } from "react" import "./App.css" import { validateAuthToken } from "../../utils/api" -import { MESSAGE_TYPES, STORAGE_KEYS, UI_CONFIG } from "../../utils/constants" +import { + getSupermemoryLoginUrl, + MESSAGE_TYPES, + STORAGE_KEYS, + UI_CONFIG, +} from "../../utils/constants" import { useDefaultProject, useProjects, @@ -84,6 +89,7 @@ function App() { const [autoCapturePromptsEnabled, setAutoCapturePromptsEnabled] = useState(false) const [authInvalidated, setAuthInvalidated] = useState(false) + const [saveError, setSaveError] = useState(null) const queryClient = useQueryClient() const { data: projects = [], isLoading: loadingProjects } = useProjects({ @@ -189,29 +195,68 @@ function App() { const handleSaveCurrentPage = async () => { setSaving(true) + setSaveError(null) try { const tabs = await chrome.tabs.query({ active: true, currentWindow: true, }) - if (tabs.length > 0 && tabs[0].id) { - const response = await chrome.tabs.sendMessage(tabs[0].id, { + const tab = tabs[0] + let response: { success?: boolean; error?: string } | undefined + + if (tab?.id) { + try { + response = await chrome.tabs.sendMessage(tab.id, { + action: MESSAGE_TYPES.SAVE_MEMORY, + actionSource: "popup", + }) + } catch (contentScriptError) { + console.warn("Content script save failed:", contentScriptError) + } + } + + if (response && !response.success) { + throw new Error(response.error || "Failed to save current page") + } + + if (!response) { + const fallbackUrl = tab?.url || currentUrl + const fallbackTitle = tab?.title || currentTitle || "Current Page" + + if (!fallbackUrl) { + throw new Error("No active page URL found") + } + + response = await chrome.runtime.sendMessage({ action: MESSAGE_TYPES.SAVE_MEMORY, - actionSource: "popup", + actionSource: "popup_fallback", + data: { + url: fallbackUrl, + title: fallbackTitle, + content: `${fallbackTitle}\n\n${fallbackUrl}`, + }, }) + } - if (response?.success) { - await chrome.tabs.sendMessage(tabs[0].id, { - action: MESSAGE_TYPES.SHOW_TOAST, - state: "success", - }) + if (response?.success) { + if (tab?.id) { + await chrome.tabs + .sendMessage(tab.id, { + action: MESSAGE_TYPES.SHOW_TOAST, + state: "success", + }) + .catch(() => undefined) } window.close() + return } + + throw new Error(response?.error || "Failed to save current page") } catch (error) { console.error("Failed to save current page:", error) + setSaveError(error instanceof Error ? error.message : "Could not save page") try { const tabs = await chrome.tabs.query({ @@ -227,8 +272,6 @@ function App() { } catch (toastError) { console.error("Failed to show error toast:", toastError) } - - window.close() } finally { setSaving(false) } @@ -626,6 +669,11 @@ function App() { {saving ? "Saving..." : "Add to supermemory"} + {saveError && ( +

+ {saveError} +

+ )} ) : activeTab === "imports" ? ( @@ -862,13 +910,13 @@ function App() {
    -
  • +
  • Save any page to your supermemory
  • -
  • +
  • Import all your Twitter / X Bookmarks
  • -
  • +
  • Import your ChatGPT Memories
@@ -893,9 +941,7 @@ function App() { className="w-full py-3 px-6 bg-[#2d3f5c] text-white border-none rounded-3xl text-base font-medium cursor-pointer transition-colors duration-200 hover:bg-[#3d5270] disabled:bg-neutral-600 disabled:cursor-not-allowed" onClick={() => { chrome.tabs.create({ - url: import.meta.env.PROD - ? "https://app.supermemory.ai/login" - : "http://localhost:3000/login", + url: getSupermemoryLoginUrl(), }) }} type="button" diff --git a/apps/browser-extension/entrypoints/popup/index.html b/apps/browser-extension/entrypoints/popup/index.html index ed4cb9494..d0cca15e9 100644 --- a/apps/browser-extension/entrypoints/popup/index.html +++ b/apps/browser-extension/entrypoints/popup/index.html @@ -3,7 +3,7 @@ - Default Popup Title + supermemory diff --git a/apps/browser-extension/entrypoints/welcome/Welcome.tsx b/apps/browser-extension/entrypoints/welcome/Welcome.tsx index 9463eba4d..37670d2c4 100644 --- a/apps/browser-extension/entrypoints/welcome/Welcome.tsx +++ b/apps/browser-extension/entrypoints/welcome/Welcome.tsx @@ -1,105 +1,99 @@ +import { getSupermemoryLoginUrl } from "../../utils/constants" + function Welcome() { return ( -
-
- {/* Header */} -
- supermemory -

- Your AI second brain for saving and organizing everything that - matters. Supermemory learns and remembers everything you save, your - preferences, and understands you. -

-
+
+
+
+
- {/* Features Section */} -
-

- What can you do with supermemory ? -

+
+
+
+ + supermemory +
+ + Chrome extension + +
-
-
-
๐Ÿ’พ
-

- Save Any Page -

-

- Instantly save web pages, articles, and content to your personal - knowledge base -

+
+
+
+ + Browser memory connected
-
-
๐Ÿฆ
-

- Import Twitter/X Bookmarks -

-

- Bring all your saved tweets and bookmarks into one organized - place -

-
+

+ Your browser now has{" "} + + supermemory. + +

-
-
๐Ÿค–
-

- Import ChatGPT Memories -

-

- Keep your important AI conversations and insights accessible -

-
+

+ Sign in once, then use the extension to save pages, import + bookmarks, and bring your memory into supported chat apps. +

-
-
๐Ÿ”
-

- Your context, everywhere. -

-

- You can connect chatbots with MCP, chat with your personal - assistant, and more. -

+
+ +
-
- - {/* Actions */} -
- -
+
- {/* Footer */} -
-

- Learn more at{" "} - - supermemory.ai - -

-
-
+
+ supermemory stores your extension session locally in Chrome. +
+
) } diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts index 20977642d..3d1346a71 100644 --- a/apps/browser-extension/utils/constants.ts +++ b/apps/browser-extension/utils/constants.ts @@ -10,6 +10,17 @@ export const API_ENDPOINTS = { : "http://localhost:3000", } as const +export function getSupermemoryLoginUrl(): string { + const baseUrl = API_ENDPOINTS.SUPERMEMORY_WEB + const loginUrl = new URL("/login", baseUrl) + const redirectUrl = new URL("/", baseUrl) + + redirectUrl.searchParams.set("extension-auth-success", "true") + loginUrl.searchParams.set("redirect", redirectUrl.toString()) + + return loginUrl.toString() +} + /** * DOM Element IDs */ @@ -22,6 +33,7 @@ export const ELEMENT_IDS = { SAVE_TWEET_ELEMENT: "sm-save-tweet-element", CHATGPT_INPUT_BAR_ELEMENT: "sm-chatgpt-input-bar-element", CLAUDE_INPUT_BAR_ELEMENT: "sm-claude-input-bar-element", + GEMINI_INPUT_BAR_ELEMENT: "sm-gemini-input-bar-element", T3_INPUT_BAR_ELEMENT: "sm-t3-input-bar-element", PROJECT_SELECTION_MODAL: "sm-project-selection-modal", } as const @@ -58,6 +70,7 @@ export const DOMAINS = { TWITTER: ["x.com", "twitter.com"], CHATGPT: ["chatgpt.com", "chat.openai.com"], CLAUDE: ["claude.ai"], + GEMINI: ["gemini.google.com"], T3: ["t3.chat"], SUPERMEMORY: ["localhost", "supermemory.ai", "app.supermemory.ai"], } as const @@ -94,6 +107,8 @@ export const POSTHOG_EVENT_KEY = { T3_CHAT_MEMORIES_AUTO_SEARCHED: "t3_chat_memories_auto_searched", CLAUDE_CHAT_MEMORIES_SEARCHED: "claude_chat_memories_searched", CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED: "claude_chat_memories_auto_searched", + GEMINI_CHAT_MEMORIES_SEARCHED: "gemini_chat_memories_searched", + GEMINI_CHAT_MEMORIES_AUTO_SEARCHED: "gemini_chat_memories_auto_searched", CHATGPT_CHAT_MEMORIES_SEARCHED: "chatgpt_chat_memories_searched", CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED: "chatgpt_chat_memories_auto_searched", } as const diff --git a/apps/browser-extension/utils/ui-components.ts b/apps/browser-extension/utils/ui-components.ts index 99f96cbbd..3a5ddb9e5 100644 --- a/apps/browser-extension/utils/ui-components.ts +++ b/apps/browser-extension/utils/ui-components.ts @@ -261,31 +261,72 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement { * @returns HTMLElement - The save button element */ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement { - const iconButton = document.createElement("div") + return createConnectedIndicator(onClick) +} + +export function createConnectedIndicator(onClick: () => void): HTMLElement { + const iconButton = document.createElement("button") + iconButton.type = "button" + iconButton.setAttribute("aria-label", "supermemory connected") + iconButton.dataset.supermemoryConnectedIndicator = "true" iconButton.style.cssText = ` display: inline-flex; align-items: center; justify-content: center; - width: auto; - height: 24px; + width: 32px; + height: 32px; + min-width: 32px; cursor: pointer; - transition: opacity 0.2s ease; + transition: opacity 0.2s ease, background-color 0.2s ease, transform 0.2s ease; border-radius: 50%; + border: none; + background: transparent; + padding: 0; + position: relative; + flex-shrink: 0; ` - // Use appropriate icon based on theme const iconFileName = "/icon-16.png" const iconUrl = browser.runtime.getURL(iconFileName) iconButton.innerHTML = ` - Save to Memory + ` + const tooltip = document.createElement("div") + tooltip.textContent = "supermemory connected" + tooltip.style.cssText = ` + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%) translateY(2px); + background: #0A0E14; + color: #FAFAFA; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 8px; + padding: 6px 8px; + font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 12px; + font-weight: 500; + line-height: 1; + white-space: nowrap; + pointer-events: none; + opacity: 0; + transition: opacity 0.16s ease, transform 0.16s ease; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28); + z-index: 2147483647; + ` + iconButton.appendChild(tooltip) + iconButton.addEventListener("mouseenter", () => { - iconButton.style.opacity = "0.8" + iconButton.style.backgroundColor = "rgba(255, 255, 255, 0.08)" + tooltip.style.opacity = "1" + tooltip.style.transform = "translateX(-50%) translateY(0)" }) iconButton.addEventListener("mouseleave", () => { - iconButton.style.opacity = "1" + iconButton.style.backgroundColor = "transparent" + tooltip.style.opacity = "0" + tooltip.style.transform = "translateX(-50%) translateY(2px)" }) iconButton.addEventListener("click", (event) => { @@ -303,42 +344,11 @@ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement { * @returns HTMLElement - The save button element */ export function createClaudeInputBarElement(onClick: () => void): HTMLElement { - const iconButton = document.createElement("div") - iconButton.style.cssText = ` - display: inline-flex; - align-items: center; - justify-content: center; - width: auto; - height: 32px; - cursor: pointer; - transition: all 0.2s ease; - border-radius: 6px; - background: transparent; - ` - - const iconFileName = "/icon-16.png" - const iconUrl = browser.runtime.getURL(iconFileName) - iconButton.innerHTML = ` - Get Related Memories from supermemory - ` - - iconButton.addEventListener("mouseenter", () => { - iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)" - iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)" - }) - - iconButton.addEventListener("mouseleave", () => { - iconButton.style.backgroundColor = "transparent" - iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)" - }) - - iconButton.addEventListener("click", (event) => { - event.stopPropagation() - event.preventDefault() - onClick() - }) + return createConnectedIndicator(onClick) +} - return iconButton +export function createGeminiInputBarElement(onClick: () => void): HTMLElement { + return createConnectedIndicator(onClick) } /** diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts index c810e73f5..acdfe65b0 100644 --- a/apps/browser-extension/wxt.config.ts +++ b/apps/browser-extension/wxt.config.ts @@ -38,6 +38,9 @@ export default defineConfig({ "*://api.supermemory.ai/*", "*://chatgpt.com/*", "*://chat.openai.com/*", + "*://claude.ai/*", + "*://gemini.google.com/*", + "*://t3.chat/*", "https://*.posthog.com/*", ], web_accessible_resources: [ From acdb1d8e66cad994343e72ab30440cc0a6926c1d Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 10:39:32 +0000 Subject: [PATCH 2/5] fix(browser-extension): resolve Biome lint and format issues - Remove dead code after return statements in chatgpt.ts and claude.ts - Replace forEach callbacks that return values with for...of loops - Prefix unused function with underscore (appendStoredMemories) - Apply formatting fixes to all changed files Co-Authored-By: Claude Opus 4.5 --- .../entrypoints/content/chatgpt.ts | 241 +---------------- .../entrypoints/content/claude.ts | 247 +----------------- .../entrypoints/content/gemini.ts | 35 ++- .../entrypoints/content/memory-suggestion.ts | 31 ++- .../entrypoints/content/shared.ts | 11 +- .../entrypoints/popup/App.tsx | 4 +- .../entrypoints/welcome/Welcome.tsx | 6 +- 7 files changed, 70 insertions(+), 505 deletions(-) diff --git a/apps/browser-extension/entrypoints/content/chatgpt.ts b/apps/browser-extension/entrypoints/content/chatgpt.ts index c51bbb3e6..9bda22671 100644 --- a/apps/browser-extension/entrypoints/content/chatgpt.ts +++ b/apps/browser-extension/entrypoints/content/chatgpt.ts @@ -208,10 +208,7 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) { promptElement, response.data, ) - console.log( - "Prompt element dataset:", - memoryText, - ) + console.log("Prompt element dataset:", memoryText) iconElement.dataset.memoriesData = String(response.data) @@ -387,220 +384,6 @@ function updateChatGPTIconFeedback( message.toLowerCase().includes("error") ? "error" : "none", ) showMarkerPopover(iconElement, message, undefined, fallbackReset) - return - - if (!iconElement.dataset.originalHtml) { - iconElement.dataset.originalHtml = iconElement.innerHTML - } - - const feedbackDiv = document.createElement("div") - feedbackDiv.style.cssText = ` - display: flex; - align-items: center; - gap: 6px; - padding: 4px 8px; - background: #513EA9; - border-radius: 12px; - color: white; - font-size: 12px; - font-weight: 500; - cursor: ${message === "Included Memories" ? "pointer" : "default"}; - position: relative; - ` - - feedbackDiv.innerHTML = ` - โœ“ - ${message} - ` - - if (message === "Included Memories" && iconElement.dataset.memoriesData) { - const popup = document.createElement("div") - popup.style.cssText = ` - position: fixed; - bottom: 80px; - left: 50%; - transform: translateX(-50%); - background: #1a1a1a; - color: white; - padding: 0; - border-radius: 12px; - font-size: 13px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; - max-width: 500px; - max-height: 400px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); - z-index: 999999; - display: none; - border: 1px solid #333; - ` - - const header = document.createElement("div") - header.style.cssText = ` - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; - border-bottom: 1px solid #333; - opacity: 0.8; - ` - header.innerHTML = ` - Included Memories - ` - - const content = document.createElement("div") - content.style.cssText = ` - padding: 0; - max-height: 300px; - overflow-y: auto; - ` - - const memoriesText = iconElement.dataset.memoriesData || "" - console.log("Memories text:", memoriesText) - const individualMemories = memoriesText - .split(/[,\n]/) - .map((memory) => memory.trim()) - .filter((memory) => memory.length > 0 && memory !== ",") - console.log("Individual memories:", individualMemories) - - individualMemories.forEach((memory, index) => { - const memoryItem = document.createElement("div") - memoryItem.style.cssText = ` - display: flex; - align-items: center; - gap: 6px; - padding: 10px; - font-size: 13px; - line-height: 1.4; - ` - - const memoryText = document.createElement("div") - memoryText.style.cssText = ` - flex: 1; - color: #e5e5e5; - ` - memoryText.textContent = memory.trim() - - const removeBtn = document.createElement("button") - removeBtn.style.cssText = ` - background: transparent; - color: #9ca3af; - border: none; - padding: 4px; - border-radius: 4px; - cursor: pointer; - flex-shrink: 0; - height: fit-content; - display: flex; - align-items: center; - justify-content: center; - ` - removeBtn.innerHTML = `` - removeBtn.dataset.memoryIndex = index.toString() - - removeBtn.addEventListener("mouseenter", () => { - removeBtn.style.color = "#ef4444" - }) - removeBtn.addEventListener("mouseleave", () => { - removeBtn.style.color = "#9ca3af" - }) - - memoryItem.appendChild(memoryText) - memoryItem.appendChild(removeBtn) - content.appendChild(memoryItem) - }) - - popup.appendChild(header) - popup.appendChild(content) - document.body.appendChild(popup) - - feedbackDiv.addEventListener("mouseenter", () => { - const textSpan = feedbackDiv.querySelector("span:last-child") - if (textSpan) { - textSpan.textContent = "Click to see memories" - } - }) - - feedbackDiv.addEventListener("mouseleave", () => { - const textSpan = feedbackDiv.querySelector("span:last-child") - if (textSpan) { - textSpan.textContent = "Included Memories" - } - }) - - feedbackDiv.addEventListener("click", (e) => { - e.stopPropagation() - popup.style.display = "block" - }) - - document.addEventListener("click", (e) => { - if (!popup.contains(e.target as Node)) { - popup.style.display = "none" - } - }) - - content.querySelectorAll("button[data-memory-index]").forEach((button) => { - const htmlButton = button as HTMLButtonElement - htmlButton.addEventListener("click", () => { - const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10) - const memoryItem = htmlButton.parentElement - - if (memoryItem) { - content.removeChild(memoryItem) - } - - const currentMemories = (iconElement.dataset.memoriesData || "") - .split(/[,\n]/) - .map((memory) => memory.trim()) - .filter((memory) => memory.length > 0 && memory !== ",") - currentMemories.splice(index, 1) - - const updatedMemories = currentMemories.join(" ,") - - iconElement.dataset.memoriesData = updatedMemories - - const promptElement = document.getElementById("prompt-textarea") - if (promptElement) { - promptElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${updatedMemories}` - } - - content - .querySelectorAll("button[data-memory-index]") - .forEach((btn, newIndex) => { - const htmlBtn = btn as HTMLButtonElement - htmlBtn.dataset.memoryIndex = newIndex.toString() - }) - - if (currentMemories.length <= 1) { - if (promptElement?.dataset.supermemories) { - delete promptElement.dataset.supermemories - delete iconElement.dataset.memoriesData - iconElement.innerHTML = iconElement.dataset.originalHtml || "" - delete iconElement.dataset.originalHtml - } - popup.style.display = "none" - if (document.body.contains(popup)) { - document.body.removeChild(popup) - } - } - }) - }) - - setTimeout(() => { - if (document.body.contains(popup)) { - document.body.removeChild(popup) - } - }, 300000) - } - - iconElement.innerHTML = "" - iconElement.appendChild(feedbackDiv) - - if (resetAfter > 0) { - setTimeout(() => { - iconElement.innerHTML = iconElement.dataset.originalHtml || "" - delete iconElement.dataset.originalHtml - }, resetAfter) - } } function addSaveChatGPTElementBeforeComposerBtn() { @@ -623,7 +406,9 @@ function addSaveChatGPTElementBeforeComposerBtn() { ) if (existingMarkers.length > 1) { debugChatGPT("removed duplicate markers", existingMarkers.length) - existingMarkers.forEach((marker) => marker.remove()) + for (const marker of existingMarkers) { + marker.remove() + } } else if (existingMarkers.length === 1) { debugChatGPT("marker already exists") return @@ -642,7 +427,8 @@ function addSaveChatGPTElementBeforeComposerBtn() { const micButton = buttons.find((button) => isChatGPTMicButton(button)) const voiceButton = buttons.find((button) => isChatGPTVoiceButton(button)) const sendButton = buttons.find((button) => isChatGPTSendButton(button)) - const anchorButton = micButton || voiceButton || sendButton || buttons[buttons.length - 1] + const anchorButton = + micButton || voiceButton || sendButton || buttons[buttons.length - 1] const anchorSlot = findChatGPTButtonSlot(anchorButton, composer) const speechContainer = composer.querySelector( '[data-testid="composer-speech-button-container"]', @@ -721,8 +507,9 @@ function findChatGPTComposerButtons( return allButtons.filter((button) => { const rect = button.getBoundingClientRect() const verticallyNear = - Math.abs(rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2)) < - 120 + Math.abs( + rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2), + ) < 120 const horizontallyNear = rect.left > inputRect.left - 80 && rect.left < inputRect.right + 260 @@ -788,11 +575,7 @@ function describeElement(element: Element | null): string | null { if (element.id) parts.push(`#${element.id}`) if (element.className && typeof element.className === "string") { parts.push( - `.${element.className - .trim() - .split(/\s+/) - .slice(0, 4) - .join(".")}`, + `.${element.className.trim().split(/\s+/).slice(0, 4).join(".")}`, ) } @@ -842,7 +625,9 @@ async function setupChatGPTAutoFetch() { if (content.length === 0) { clearMemorySuggestion("chatgpt", promptTextarea) document - .querySelectorAll('[id*="sm-chatgpt-input-bar-element-before-composer"]') + .querySelectorAll( + '[id*="sm-chatgpt-input-bar-element-before-composer"]', + ) .forEach((icon) => { setMemoryMarkerStatus(icon as HTMLElement, "neutral") }) diff --git a/apps/browser-extension/entrypoints/content/claude.ts b/apps/browser-extension/entrypoints/content/claude.ts index 60a8fffcc..cb1a5f389 100644 --- a/apps/browser-extension/entrypoints/content/claude.ts +++ b/apps/browser-extension/entrypoints/content/claude.ts @@ -166,11 +166,15 @@ function addSupermemoryIconToClaudeInput() { } const existingMarkers = Array.from( - document.querySelectorAll(`[id*="${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}"]`), + document.querySelectorAll( + `[id*="${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}"]`, + ), ) if (existingMarkers.length > 1) { debugClaude("removed duplicate markers", existingMarkers.length) - existingMarkers.forEach((marker) => marker.remove()) + for (const marker of existingMarkers) { + marker.remove() + } } else if (existingMarkers.length === 1) { debugClaude("marker already exists") return @@ -186,12 +190,11 @@ function addSupermemoryIconToClaudeInput() { })), }) - const micButton = buttons.find((button) => - isClaudeMicButton(button), - ) + const micButton = buttons.find((button) => isClaudeMicButton(button)) const voiceButton = buttons.find((button) => isClaudeVoiceButton(button)) const sendButton = buttons.find((button) => isClaudeSendButton(button)) - const anchorButton = micButton || voiceButton || sendButton || buttons[buttons.length - 1] + const anchorButton = + micButton || voiceButton || sendButton || buttons[buttons.length - 1] const anchorSlot = findClaudeButtonSlot(anchorButton, composer) const targetContainer = anchorSlot?.parentElement || input.parentElement @@ -271,8 +274,9 @@ function findClaudeComposerButtons( return allButtons.filter((button) => { const rect = button.getBoundingClientRect() const verticallyNear = - Math.abs(rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2)) < - 120 + Math.abs( + rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2), + ) < 120 const horizontallyNear = rect.left > inputRect.left - 80 && rect.left < inputRect.right + 260 @@ -324,11 +328,7 @@ function describeElement(element: Element | null): string | null { if (element.id) parts.push(`#${element.id}`) if (element.className && typeof element.className === "string") { parts.push( - `.${element.className - .trim() - .split(/\s+/) - .slice(0, 4) - .join(".")}`, + `.${element.className.trim().split(/\s+/).slice(0, 4).join(".")}`, ) } @@ -445,10 +445,7 @@ async function getRelatedMemoriesForClaude(actionSource: string) { textareaElement, response.data, ) - console.log( - "Text element dataset:", - memoryText, - ) + console.log("Text element dataset:", memoryText) iconElement.dataset.memoriesData = String(response.data) @@ -522,222 +519,6 @@ function updateClaudeIconFeedback( message.toLowerCase().includes("error") ? "error" : "none", ) showMarkerPopover(iconElement, message, undefined, fallbackReset) - return - - if (!iconElement.dataset.originalHtml) { - iconElement.dataset.originalHtml = iconElement.innerHTML - } - - const feedbackDiv = document.createElement("div") - feedbackDiv.style.cssText = ` - display: flex; - align-items: center; - gap: 6px; - padding: 6px 8px; - background: #513EA9; - border-radius: 6px; - color: white; - font-size: 12px; - font-weight: 500; - cursor: ${message === "Included Memories" ? "pointer" : "default"}; - position: relative; - ` - - feedbackDiv.innerHTML = ` - โœ“ - ${message} - ` - - if (message === "Included Memories" && iconElement.dataset.memoriesData) { - const popup = document.createElement("div") - popup.style.cssText = ` - position: fixed; - bottom: 80px; - left: 50%; - transform: translateX(-50%); - background: #1a1a1a; - color: white; - padding: 0; - border-radius: 12px; - font-size: 13px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; - max-width: 500px; - max-height: 400px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); - z-index: 999999; - display: none; - border: 1px solid #333; - ` - - const header = document.createElement("div") - header.style.cssText = ` - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; - border-bottom: 1px solid #333; - opacity: 0.8; - ` - header.innerHTML = ` - Included Memories - ` - - const content = document.createElement("div") - content.style.cssText = ` - padding: 0; - max-height: 300px; - overflow-y: auto; - ` - - const memoriesText = iconElement.dataset.memoriesData || "" - console.log("Memories text:", memoriesText) - const individualMemories = memoriesText - .split(/[,\n]/) - .map((memory) => memory.trim()) - .filter((memory) => memory.length > 0 && memory !== ",") - console.log("Individual memories:", individualMemories) - - individualMemories.forEach((memory, index) => { - const memoryItem = document.createElement("div") - memoryItem.style.cssText = ` - display: flex; - align-items: center; - gap: 6px; - padding: 10px; - font-size: 13px; - line-height: 1.4; - ` - - const memoryText = document.createElement("div") - memoryText.style.cssText = ` - flex: 1; - color: #e5e5e5; - ` - memoryText.textContent = memory.trim() - - const removeBtn = document.createElement("button") - removeBtn.style.cssText = ` - background: transparent; - color: #9ca3af; - border: none; - padding: 4px; - border-radius: 4px; - cursor: pointer; - flex-shrink: 0; - height: fit-content; - display: flex; - align-items: center; - justify-content: center; - ` - removeBtn.innerHTML = `` - removeBtn.dataset.memoryIndex = index.toString() - - removeBtn.addEventListener("mouseenter", () => { - removeBtn.style.color = "#ef4444" - }) - removeBtn.addEventListener("mouseleave", () => { - removeBtn.style.color = "#9ca3af" - }) - - memoryItem.appendChild(memoryText) - memoryItem.appendChild(removeBtn) - content.appendChild(memoryItem) - }) - - popup.appendChild(header) - popup.appendChild(content) - document.body.appendChild(popup) - - feedbackDiv.addEventListener("mouseenter", () => { - const textSpan = feedbackDiv.querySelector("span:last-child") - if (textSpan) { - textSpan.textContent = "Click to see memories" - } - }) - - feedbackDiv.addEventListener("mouseleave", () => { - const textSpan = feedbackDiv.querySelector("span:last-child") - if (textSpan) { - textSpan.textContent = "Included Memories" - } - }) - - feedbackDiv.addEventListener("click", (e) => { - e.stopPropagation() - popup.style.display = "block" - }) - - document.addEventListener("click", (e) => { - if (!popup.contains(e.target as Node)) { - popup.style.display = "none" - } - }) - - content.querySelectorAll("button[data-memory-index]").forEach((button) => { - const htmlButton = button as HTMLButtonElement - htmlButton.addEventListener("click", () => { - const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10) - const memoryItem = htmlButton.parentElement - - if (memoryItem) { - content.removeChild(memoryItem) - } - - const currentMemories = (iconElement.dataset.memoriesData || "") - .split(/[,\n]/) - .map((memory) => memory.trim()) - .filter((memory) => memory.length > 0 && memory !== ",") - currentMemories.splice(index, 1) - - const updatedMemories = currentMemories.join(" ,") - - iconElement.dataset.memoriesData = updatedMemories - - const textareaElement = document.querySelector( - 'div[contenteditable="true"]', - ) as HTMLElement - if (textareaElement) { - textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${updatedMemories}` - } - - content - .querySelectorAll("button[data-memory-index]") - .forEach((btn, newIndex) => { - const htmlBtn = btn as HTMLButtonElement - htmlBtn.dataset.memoryIndex = newIndex.toString() - }) - - if (currentMemories.length <= 1) { - if (textareaElement?.dataset.supermemories) { - delete textareaElement.dataset.supermemories - delete iconElement.dataset.memoriesData - iconElement.innerHTML = iconElement.dataset.originalHtml || "" - delete iconElement.dataset.originalHtml - } - popup.style.display = "none" - if (document.body.contains(popup)) { - document.body.removeChild(popup) - } - } - }) - }) - - setTimeout(() => { - if (document.body.contains(popup)) { - document.body.removeChild(popup) - } - }, 300000) - } - - iconElement.innerHTML = "" - iconElement.appendChild(feedbackDiv) - - if (resetAfter > 0) { - setTimeout(() => { - iconElement.innerHTML = iconElement.dataset.originalHtml || "" - delete iconElement.dataset.originalHtml - }, resetAfter) - } } function setupClaudePromptCapture() { diff --git a/apps/browser-extension/entrypoints/content/gemini.ts b/apps/browser-extension/entrypoints/content/gemini.ts index 64254cbbe..04c670a20 100644 --- a/apps/browser-extension/entrypoints/content/gemini.ts +++ b/apps/browser-extension/entrypoints/content/gemini.ts @@ -160,11 +160,15 @@ function addSupermemoryIconToGeminiInput() { } const existingMarkers = Array.from( - document.querySelectorAll(`[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`), + document.querySelectorAll( + `[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`, + ), ) if (existingMarkers.length > 1) { debugGemini("removed duplicate markers", existingMarkers.length) - existingMarkers.forEach((marker) => marker.remove()) + for (const marker of existingMarkers) { + marker.remove() + } } else if (existingMarkers.length === 1) { debugGemini("marker already exists") return @@ -180,9 +184,7 @@ function addSupermemoryIconToGeminiInput() { })), }) - const micButton = buttons.find((button) => - isGeminiMicButton(button), - ) + const micButton = buttons.find((button) => isGeminiMicButton(button)) const sendButton = buttons.find((button) => isGeminiSendButton(button)) const anchorButton = micButton || sendButton || buttons[buttons.length - 1] const anchorSlot = findGeminiButtonSlot(anchorButton, composer) @@ -259,8 +261,9 @@ function findGeminiComposerButtons( return allButtons.filter((button) => { const rect = button.getBoundingClientRect() const verticallyNear = - Math.abs(rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2)) < - 120 + Math.abs( + rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2), + ) < 120 const horizontallyNear = rect.left > inputRect.left - 80 && rect.left < inputRect.right + 240 @@ -328,11 +331,7 @@ function describeElement(element: Element | null): string | null { if (element.id) parts.push(`#${element.id}`) if (element.className && typeof element.className === "string") { parts.push( - `.${element.className - .trim() - .split(/\s+/) - .slice(0, 4) - .join(".")}`, + `.${element.className.trim().split(/\s+/).slice(0, 4).join(".")}`, ) } @@ -364,7 +363,7 @@ function getInputText(input: GeminiInput | null): string { return input.innerText || input.textContent || "" } -function appendStoredMemories(input: GeminiInput, storedMemories: string) { +function _appendStoredMemories(input: GeminiInput, storedMemories: string) { if (input instanceof HTMLTextAreaElement) { const promptContent = input.value || "" input.value = `${promptContent}${storedMemories}` @@ -510,7 +509,7 @@ function setupGeminiPromptCapture() { } const input = getGeminiPromptInput() - let promptContent = getInputText(input) + const promptContent = getInputText(input) debugGemini("capture input state", { hasInput: !!input, promptLength: promptContent.length, @@ -541,9 +540,7 @@ function setupGeminiPromptCapture() { icons.forEach((icon) => { const iconElement = icon as HTMLElement - iconElement - .querySelector("[data-supermemory-status-badge]") - ?.remove() + iconElement.querySelector("[data-supermemory-status-badge]")?.remove() delete iconElement.dataset.supermemoryStatus delete iconElement.dataset.memoriesData if (iconElement.dataset.originalHtml) { @@ -659,9 +656,7 @@ async function setupGeminiAutoFetch() { icons.forEach((icon) => { const iconElement = icon as HTMLElement - iconElement - .querySelector("[data-supermemory-status-badge]") - ?.remove() + iconElement.querySelector("[data-supermemory-status-badge]")?.remove() delete iconElement.dataset.supermemoryStatus delete iconElement.dataset.memoriesData if (iconElement.dataset.originalHtml) { diff --git a/apps/browser-extension/entrypoints/content/memory-suggestion.ts b/apps/browser-extension/entrypoints/content/memory-suggestion.ts index 58714c7f7..1722e71ef 100644 --- a/apps/browser-extension/entrypoints/content/memory-suggestion.ts +++ b/apps/browser-extension/entrypoints/content/memory-suggestion.ts @@ -71,7 +71,10 @@ export function showMemorySuggestion( return suggestionText } -export function showLoadingSuggestion(platform: string, input: SuggestionInput) { +export function showLoadingSuggestion( + platform: string, + input: SuggestionInput, +) { removeMemorySuggestion(platform) const anchor = getSuggestionAnchor(input) @@ -134,9 +137,12 @@ function createSuggestionContainer( } export function removeMemorySuggestion(platform: string) { - document - .querySelectorAll(`[${SUGGESTION_ATTR}="${platform}"]`) - .forEach((element) => element.remove()) + const elements = document.querySelectorAll( + `[${SUGGESTION_ATTR}="${platform}"]`, + ) + for (const element of elements) { + element.remove() + } } export function acceptMemorySuggestion( @@ -160,7 +166,9 @@ export function acceptMemorySuggestion( return true } -export function hasAcceptedSupermemoryContext(input: SuggestionInput | null): boolean { +export function hasAcceptedSupermemoryContext( + input: SuggestionInput | null, +): boolean { if (!input) return false const text = input instanceof HTMLTextAreaElement @@ -178,7 +186,10 @@ export function syncAcceptedSupermemoryState(input: SuggestionInput | null) { } } -export function clearMemorySuggestion(platform: string, input: SuggestionInput | null) { +export function clearMemorySuggestion( + platform: string, + input: SuggestionInput | null, +) { removeMemorySuggestion(platform) if (input?.dataset.supermemories) { delete input.dataset.supermemories @@ -194,9 +205,7 @@ export function setMemoryMarkerStatus( ) { if (!iconElement) return - iconElement - .querySelector("[data-supermemory-status-badge]") - ?.remove() + iconElement.querySelector("[data-supermemory-status-badge]")?.remove() if (status === "neutral" || status === "none") { delete iconElement.dataset.supermemoryStatus @@ -227,9 +236,7 @@ export function showMarkerPopover( memories?: string, resetAfter = 0, ) { - iconElement - .querySelector("[data-supermemory-marker-popover]") - ?.remove() + iconElement.querySelector("[data-supermemory-marker-popover]")?.remove() ensureSuggestionAnimationStyle() const popover = document.createElement("div") diff --git a/apps/browser-extension/entrypoints/content/shared.ts b/apps/browser-extension/entrypoints/content/shared.ts index 5c56eaa83..07fdcebde 100644 --- a/apps/browser-extension/entrypoints/content/shared.ts +++ b/apps/browser-extension/entrypoints/content/shared.ts @@ -77,12 +77,11 @@ export async function saveMemory( if (response?.success) { DOMUtils.showToast("success") return response - } else { - DOMUtils.showToast("error") - return { - success: false, - error: response?.error || "Failed to save memory", - } + } + DOMUtils.showToast("error") + return { + success: false, + error: response?.error || "Failed to save memory", } } catch (error) { console.error("Error saving memory:", error) diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx index 9d9d1eb47..696caf87b 100644 --- a/apps/browser-extension/entrypoints/popup/App.tsx +++ b/apps/browser-extension/entrypoints/popup/App.tsx @@ -256,7 +256,9 @@ function App() { throw new Error(response?.error || "Failed to save current page") } catch (error) { console.error("Failed to save current page:", error) - setSaveError(error instanceof Error ? error.message : "Could not save page") + setSaveError( + error instanceof Error ? error.message : "Could not save page", + ) try { const tabs = await chrome.tabs.query({ diff --git a/apps/browser-extension/entrypoints/welcome/Welcome.tsx b/apps/browser-extension/entrypoints/welcome/Welcome.tsx index 37670d2c4..69756c6d6 100644 --- a/apps/browser-extension/entrypoints/welcome/Welcome.tsx +++ b/apps/browser-extension/entrypoints/welcome/Welcome.tsx @@ -16,11 +16,7 @@ function Welcome() {
- + supermemory Date: Wed, 20 May 2026 13:06:14 +0530 Subject: [PATCH 3/5] add new logo --- .../entrypoints/content/chatgpt.ts | 2 +- .../entrypoints/content/twitter.ts | 4 ++-- .../entrypoints/popup/App.tsx | 20 +++++++----------- .../entrypoints/welcome/Welcome.tsx | 10 ++++----- .../entrypoints/welcome/index.html | 4 ++-- apps/browser-extension/public/new_logo.png | Bin 0 -> 33050 bytes apps/browser-extension/utils/ui-components.ts | 14 ++++++------ apps/browser-extension/wxt.config.ts | 2 +- 8 files changed, 25 insertions(+), 31 deletions(-) create mode 100644 apps/browser-extension/public/new_logo.png diff --git a/apps/browser-extension/entrypoints/content/chatgpt.ts b/apps/browser-extension/entrypoints/content/chatgpt.ts index 9bda22671..d6049cab1 100644 --- a/apps/browser-extension/entrypoints/content/chatgpt.ts +++ b/apps/browser-extension/entrypoints/content/chatgpt.ts @@ -281,7 +281,7 @@ function addSupermemoryButtonToMemoriesDialog() { supermemoryButton.id = "supermemory-save-button" supermemoryButton.className = "btn relative btn-primary-outline mr-2" - const iconUrl = browser.runtime.getURL("/icon-16.png") + const iconUrl = browser.runtime.getURL("/new_logo.png") supermemoryButton.innerHTML = `
diff --git a/apps/browser-extension/entrypoints/content/twitter.ts b/apps/browser-extension/entrypoints/content/twitter.ts index ffa138af9..875f4bab3 100644 --- a/apps/browser-extension/entrypoints/content/twitter.ts +++ b/apps/browser-extension/entrypoints/content/twitter.ts @@ -253,7 +253,7 @@ async function showOnboardingToast() { header.style.cssText = "display: flex; align-items: flex-start; gap: 12px; position: relative;" - const iconUrl = browser.runtime.getURL("/icon-16.png") + const iconUrl = browser.runtime.getURL("/new_logo.png") const icon = document.createElement("img") icon.src = iconUrl icon.alt = "Supermemory" @@ -512,7 +512,7 @@ function showOrUpdateImportProgressToast(message: string, isComplete = false) { animation: smSlideInUp 0.3s ease-out; ` - const iconUrl = browser.runtime.getURL("/icon-16.png") + const iconUrl = browser.runtime.getURL("/new_logo.png") const icon = document.createElement("img") icon.src = iconUrl icon.alt = "Supermemory" diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx index 696caf87b..2bb3b3e2e 100644 --- a/apps/browser-extension/entrypoints/popup/App.tsx +++ b/apps/browser-extension/entrypoints/popup/App.tsx @@ -332,7 +332,7 @@ function App() { > supermemory
@@ -340,11 +340,9 @@ function App() { Your - supermemory + + supermemory +
@@ -398,7 +396,7 @@ function App() { > supermemory
@@ -412,11 +410,9 @@ function App() { return name.endsWith("s") ? `${name}'` : `${name}'s` })()} - supermemory + + supermemory +
{userSignedIn && ( diff --git a/apps/browser-extension/entrypoints/welcome/Welcome.tsx b/apps/browser-extension/entrypoints/welcome/Welcome.tsx index 69756c6d6..4ef72b813 100644 --- a/apps/browser-extension/entrypoints/welcome/Welcome.tsx +++ b/apps/browser-extension/entrypoints/welcome/Welcome.tsx @@ -16,12 +16,10 @@ function Welcome() {
- - supermemory + + + supermemory +
Chrome extension diff --git a/apps/browser-extension/entrypoints/welcome/index.html b/apps/browser-extension/entrypoints/welcome/index.html index 92bb26e0d..30ac32140 100644 --- a/apps/browser-extension/entrypoints/welcome/index.html +++ b/apps/browser-extension/entrypoints/welcome/index.html @@ -2,7 +2,7 @@ - + Welcome to supermemory @@ -10,4 +10,4 @@
- \ No newline at end of file + diff --git a/apps/browser-extension/public/new_logo.png b/apps/browser-extension/public/new_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..dbabef62a214c5a6e2a2d36efde6775b2cbd1312 GIT binary patch literal 33050 zcmV)=K!m@EP)0CYD%H z5fQ|KF*Z6VQDNv!K`gOgp_l39%-QSv{hzhhe)lPoR{_APhHP?)>_Znd%x$* z42|wUpS||_+~|OVKezi!-uSuuz3PxJJ?!=G`r2xJ(*;Ki@`)VuaO@Z;w^{}qRQ zf4>7iaor1F{mJF~zxL?$FFoj(4X=397xNC>x;1$D!T*_G^19E??Eg=nUjO`8e*C)q z4m|vdeP8jh%aFhL!dD;p&KDeb_+GDl+bNHH`N5xg=z#|xd#}Cr{pVfpK;;yvZubY^ z$@?C>Wbc=J^mh(;^B13S|3^(9vToJdBbQ%$^@*4Nckx-@zW&-P-gVg}OAq+Y z1^@N9i_SgeA?KfU{O)Id^NYKDvUwhyoU;4AZ-REr&jSAb&FWc<_c*@gX`@1iF-DltX@cTaQ8;h3g^yMFa@4OG6 zb@~@yv-DeE`1AA5KK|hspMUC-i_SkOeeaSp_0u0+tgEj0v97!J7rNovE41RqYqfgi za;;mldIxUf8mw8hQY)8TA8YEh^%`IC^B?Gk-#b^|x!~)%==@Vg9EiJ|d)BePzw{el zcUV?X*kCm;LKJukiZo4Z{3%O9ncD{j!*H7m7#{W{Ig&Z^B> znHAmvZS{3%8F9mg8LeNpR%=$T(6Sq^(d9qAtU2rSFWv2=FCBWnZ=QPe|M#XFtB=(6dkf;`6_K;c55!{$*#UYp?maHf-2X z3f~J0vJy#x!KNyWBxEB&Q!|3yT_3K61u-+Q1zbrTGj|zN~%G|B|gkH0HrGCL{ z(nj z+v3%TOtsZMdmr%8$EUUH-?!}gE56JZs6V>k?BkPPaWaq0Ty7bX7cLgHQKGnwrm1|R z23^xkXapUNMxAVm4mork-NBFKH87gd?ZX+!j_WdhJ6L< zzNfI)EeGP4zxbif{MHv2|M*82zwj47{=wIm&0O=!{SQ9sF1T&AR z3qDd4eUYI%_}hOSnh|Rp>rU(_eD=j_Khz^PAoh`cNU^t#ytjUx&Eha{2xewB=!#$b zw=TQntOs0t;b{kNST+021K#i-e~o#ov}Q|=ve#Y*PrdZ@pa0!^-|NBOy5z!dyy47m z{O2D0q@bDQ{hpgZ44qO^VsT2LHcIAWAsaPyk-d;078IHS*i-RO9F&0N9lX-kS_cZd z>Bkz$`(q^br%B~dG$ZR9C44KwCp8j#FMl`0_jf3EQZj=(hFDQIA z#1nzL;u?O$&F>X$0F>SHdY>CMGz4DWLTz~blBQCl4Td)7Y zWoO@K#q#S^i(g7gOt`=cGLj2Cx!`%6rv9!!q14?)Y)c%16#eL9`$w$>u>S5Jm4?QtVly6#Gl@ zv-b^1?0qr~yY=|MyAKb7sF}6vR`VO@bMF45@16hV^^-e&_=N`^elOk5pXT<8x9_2c zPX2P)e?RTI8?X7?1!tf9m>aLZDkhP;AB80rpcc7l!AK1Y(O^qKEjLQ3!1{)tEgMxg zro#o_!Kbt>*P!gzcyAhc&&vLdZI)Am-0$@Wzk-A=aUhzY4Q1)4LDFp7>hhmln!bC< z(q~_P?bV-o@v9GiG{)Pld8FGV*!qjs@AI?Ezx%!)e((G}{KpjHJ)g)ad1D$SCKvDo zdDW>CP2GZ|)JVXz=!$GzLBhTRN_Uhs81G-~XWd)ZLAU2o1Jr}5QT#?q=u#^4%vtfd z?%H4K2j4&M@mE~&^N&1pzkmOKLwP$~$nv&D@y~7ddD(~GdCj#y|G>|F`n}y}XE*Ts zzb+<|7D%3O(2-)IsbL>I(MGm&K~4cKl9G0O?qn;F*i%4LO7H?u*M_1V8bxh%wIUys z)ZoRFfUApY?V6R%kA85$@3ncu3C}<9@IUh__bpwe`Tos(#VwIt{41`#;q5>D$z}Wh z^hcL=U6Ya%j|q%aCRJ$T$$=&{^2F_DKJ?b@fYPtX8hFo}sF70mP2BZI3JPy(B>1F; z`cIOQX4bFMB^RH*+qGBy?1*2i{K->q>mg`#8$K(px$&UOe{$LLuKeYX`G(H3iYAg2 z38%!w>IBVuBArb2EBU{aa0l=(+VS~SSb@QQv;Qe2c0WnfNaE(0sF5gq%kT*rwxH!{ zFf+4G|NZ?7_qh6(KYHi)etF*S={9_B)dBDq-eJukfG>R%jq%~e02ZgVSTLPe5| zQkQsY61k5GlJx~mLf^~q0-=NM;FZ#^(mIryNQpTQYN8oYH&I8>?&c{qlzO7pl4fUS z^wS?*a-XF1r5C^Iust!{s^*BdsG}b_%g?@HuV4P`M~D3Er{AS7w|)Plr<*{-BpRRO zF6kyF!7%7kL%ON(>@s~*0ez(%yna>Jfq5tDBBi91j0Os#jv6&#gGNm?lnPG*{QQ0M zdzYNG`_F#u!09LDl1fBEt&ulVu*@gv85 zk`w7r?)Hrma`5u0EpmcS+VS6x6}UAkFsPxfwtV+bei|UB_zO6V1n-;j@)g%jUj56T zyyoe9zw-&&%n!Y9-t)S(vyZsphO3{mdiBa_ejqCkSdn-vnNL2U!86g6Iv>*=P^`y} zb-1qmx8<8d>2(uccinZjmtA-56_1~tos~#J6u6NXTp%H*xbu&& zH%iC~B;*}X+78y>PLTt(6?N87SB)C@lnP(!ukaYPcI}$c>NTqm{M&v1Coe0HqBmRy09Tw}n^^*4ajFf_j- z;3q>jTzkbXt8TpEF#mf~C^y;MV?pC{pZktouD$At*Q{N$>OP9{Zj>nYChqzpH4NM! zCumAZsplJ|(Cwf*)?v%kKwo#DJ2QatY3GR8?L!(?{yQub$4yz(=4R-FB4`rYem1&({kq>@zyA9FtxbHIh4k01yWj6G zTXx;!icY%O@9nKr%}yg`I;5qzvsHO ztNyIba&ILUe!TYJWqK-dzHKmzEHaZL%DBy=!P^EZ{l8ropEf@LIQa-7{upW{N7OLI>B4H3Q>z$L>hzS9=AhRt|v#4M=Uy z56!84tOlR{0Q~v?_lo(1?g=S|Dp;`tJ62$OT!BH24%o<)0-xQmVJfdre;P8EH~`nIUh|+08)oknuQWVF z@WWJ^Pm+t@v8?wKVh30i7GuXcY>(?8muptV9}^x2CTtBMc3f|t6`)YPycA~HkgRt8>aysV*%o|nig%nq zCW5215sModR3LMFEq45uCHS3pMeU}Y0GY|J- zdsXa|IRc7L?pDfl2dE;}2=i%Rzrr}c6dw%2v`mLf!#c_Qban;I5BeET5yj)QUp zqH~Ff=ca;m`r092d17<%)t!kT5j9Pwd+7dS-T>?IMIZkTF?8&c75&jUhWprNe&ZZ@>JVVA ze!h;e7<;Zy)*VG2=2>c*$+)OHVzwaNe^J`7Q!|l9{~N`KX-AvW3By?F2zNBZ1a9WC zb?4;5ENe_!2(*5Q%^Q3bD>{lgd>2d+W!Sm3`BjOhhWmzpgvO4(%BcD_r%1v9U%oAr zLCqU>H@&07^8hSrKNA)zrF5S>F}t{#m|XO0UJzVpcIgOCWkHbd(9LtYHMqSe?YpRB zk_&6=HGjmzAEFGt>mD!;<1jP2Ozl&Q8 z*eGr+FpTM;7XV$ty_oD*Q8ah>Yd1j?&m~nx+I?bXa;lk`nR&bob=qFA9w@z0cN}=H z$(~7;oO!%c7=A_AG{h@JLFrTb)3=XPb+?9!0$aG>Z*Tyf|J;7&-iva`!(5!3fexpI=ik{0G}lg_JA#hl zW+}RB;Y8lEv$K=4dHrLW+|KTdm08BM(9X|}cMOobb1pl~9O~u-%ca^EqI6_IJWqG$ zrrG(A^|sx5MAgyV@yhrf&f^3{bxWK3QLl2oLAzI-jG=%jU@%|aY`h-mVo*lIPji6H z>CMrFqBAViXK248@MR9MCa`W~eYb`+>He}XVrjO;#1S#WqS?NAHkeH7{;~Jn{&srX z7KG=Puna&kmAP7H`>ny;ar=B2$r}*y8HZ)Pb&UY6@Ad)mV}LSuz7KHT7{;xv0bJNT z68(ZVMIL8K49nIV=6h7H&ZpPkab#y&W1owu`e&LmIXQEYv2V52xtGYyJ6c>7kMU~ZY)YVpB8OkB{@iJ)Dxs$+}Z3gC|R zwXNd|!4`UQ3>F$bzT9#+ged-&lv@r`+wmT2{1YbU&p0^KBz!XtXh<0ie2femM&}1l z3%h+~oe{pa(4*&(?;yPAOy<$Zni*LGHncl6XcseM2qs$3+l>k0#yW7_uJ}mF8Imr! zd2^Wof?KDrJ{X)kbEBc8Sw@XxydRgMLEjQ9^w2m6^q{p7zv}pI5Y+@7#DOIo&M+<> zya`A!s~}E{koX{=i-C)T;uf|jdcs5VtZ6|TJWm1Z$=Oj!$3rs`gSw%Bpd&*jmSZmN zpYfp+Nk;zmtH4rp%o$$cW8VJsvtQ`9Pu}Ywatx3sD^yf%JMt^gVlN1Kw}3D94=*lZ zhq`Sayt4pqh7%)cZ;~hJ+>Kp!TB3>31S+PCA1=!5nfE;S&~YgZl^He>ErQ66$U z167#tg!*caLggPts(unRo~k?jJ091FgIlGrx45~l=aQq?iA`J&R&*ttc?KqZa>ULR z9$WAoYd*&XL(v7prK&4+eAJbgg{Lo93+7CX?TL5(eNH4+WoEmxUx5~njaV&sLiXD3 zKyqD9E_Sp122q@LmQ* zP;5c*hdRE;pKG%Zogf?QLrG`ix<7%VKjN)vj@B|KBYQ{?JO;w6W&VzDz{>dwL-<0? zmz#h)kd}${;Y>#67l7kMU;GP&3BOJEnd?{mw;?4}X3W*MgEY}_Zw#R*&DubM-lXjn zEIN(scnp(jyuu)PgRI|!;{VWlUa7}D_ThThJNDNT|LCzAjYi|WpkRtGbEQ9Bv2hpm z9UPg~)ralqQPAwx!TPKimjN@{G1}0VS{iF{V~jRDxxlmH4$6|>;zqw5cYb2TT!;W| zK@OYsWoJDQDsn={z+uNdGQ$wm`q1Hn1M1Ke8@hoM>lX4nedtP$?d>|w6pr~Z)zX+K zPXH^C5o+Sh#p{Nl=q+y_h$Oq*DqpB8)^mHz}7T|FhrM zsNr>}772II4Ad2p9EPfO49D^y=Dp#z71)WGkRr20@b(+Zh3^bDf{5d;m50(B%`I>4 zn(I6mrtSC`7e1(+I-XyGaWuY?`)(*W92083d z9Liya$1-k~w^NWaD>O*ec^0vW$%g|9Q&1;px;XrdekF- zT}|T7#B_$uhv2>c%Inbo{vDnKybe($nSL&oI1uorWuGZf?0&^D?32A__>?ir-Lmlb zVjT0%$FQDP9@`w$v?Zu1u^iL+!P;tevq6Y{+aeBmSz&!pBC$LqL1O^xOMXR;0nwX| z_0G2-=1gE3;fk*Kb@L(McrS)_^B%)=`vDonJh;Gm;w%tzKoGA$BpNGWvE!>9x_vbA z>hgA$UAiSTCbG-UOZ4_bUZ95zUVDmhL@tJ4{5oJ;2w_1QQVwD#nqUmidg689hAhnS0Ib5 zAO?KqEs*sXh?4U*?i4NyYEVW z7KH+7nN~~TWWYI4MwdNh1XB+kcSZcnTNkjs(-plt736Rj@{Hy5P=znNT_Dk{4rVhh zo`3YElc~oll+K~!P;)rv>adY(@g+j|p(B~Gu2TlFj~VjL?uah|?aKmL3E_!g6YJ2n z@Yqq(thB$~9HSsdoUPB31+9$BbW37f#g{!^f#9%>4V}PbSOSc~WX}3+$VWb;LmJsU zbb5M9&wR#{^qgn@g(fB@Fm7IZ@Pqc$M?Q3b{MzI_kqh3%?|lniYLPR%Z7q7!%$V%8 z*kP3wLBWtEA9x%JZ>8O;7nFg**7alnobcgF&%(fOPIEpC6@5(SfB+$rbB!D0B9Mhs z$AhjoSx-6$(a_^j^+m#50%3ti$u(0r8Cs>DcVBFdmZ7=#vBut}){+cj;cgpG7)B|P zHpxd`Wt^HpU5I!EveJk!cE;Ddb=wx65*d(F$Hip1$ge#w+V`p2|AkL0rdxP@=tJ(U z5B&R!^{|KDU-27M?=uD8&T{t*UZQ3}EJn~sg9{BK1KNZDA*HfnXld5i#k$H{4R3#A zEZPxi^P8W1gpk3&g}QqQUu5$>#5mZ`#ct}s3Uvo0`avp15Jpm6BSST^9JnXj;- z@|A8%w~)T}*)r5oV-ifT^4=L|60bm38sTKE85CvFaCOIR3U5@+LzF)2yUMskz*O#y7JmHmaVI>t)l8Gb*##7dFrGYCDy<4UJ`?yhQusM$}T;Rew(*$ zC!Cjo7+D6r?a4Jw(qkU|KppO;(~7| zOOG*O3gYDdFu<3M^JGcguDthb#ms5v-= z8C+rQ)cGRe7m8rZrS>b*rmya2Y}#W>TKbV=@|_f<6;FU%#ttKM^n}fC7byAo)EYj; zq^GrO*Xtu6|Dulk)bSB<@)!9rY>Wr* zW#9miE6QRb82ngc?Y4z;aaKm|V;DHJsIns}Y1_@>s;EJ&MEuH}GL|+FA z)_fPjIRuNY+QkHX z*eAZKnVAi}iHY3e=GUR$e9QxN_+bac>rm$I&)zCCX6wR-b}ktX000mGNklN3|*&zsS7>i@^#Xx-5L*(1>cC_64n8DS4wvFLfWnJu2?ABG#$p*;Aw(4{H zQV&(eG6xvnbMeIa4uFF3+|c9aO+jY?$xho6HBqNRR-{?-E4<;($Il&gew&7KCWmZlY@8=1 zl=y5$Yk6!C1y3gFs|f*2Y7XWzhX8Y^e8I(H)T##?%l5_up~28PCP-W;YQMf!lRFy@ z9KaCNgGL7{7>i8j_}OMuKl64q-9lh|*AbbJkH;FLJON1D4w93yFl|m;t*i8iiJW{vP3Gl_SG4H2*_lNb4j}7L$D?nJ2uu9p6AX$ve#wP? zIFEJs&XKW4TqsU7((ZT`bR5Gv_Z`w9&#Mldi@*I;a_)8Z-HM3CV4|#t;3F@n#m3YM z3+9<|u)!B@8(zIgb$Cyp!n(>Ax^^wELq{B^kA3`Dty@3Kf=jc!uFS@UGHtuL+h!nM zhko;c`ozb19lFQvYAJjH7k-Yre~H(|-Jd%nv2*a~9UpnTe#`+~uDbA}Pp1ntm$0MT zoaXNUEq%h*9nFK-$DmjSbexOz7_2Zs>@sHCt71SMd*D-jt*TyhL@e`x88mwsV^trn zR}Jk8#poSx8&ijI0*0eObK4Gn3gkrr-Y%m|HsiYO5F1KP@~xIkg5{Et$F|OpvevF! zuR}lbB^~j}6SRTiAHoI0ob9~Xc-X`4qeDN$>(ImaWf^z z2N*TO7&65HSF;7!H@Eo!L?>dDJ`t%?nJ=R-3}$5fXvL03Zj9L?kK=xr#d%pNY%wA`o+YUx1qT|#tOyxfeXQnedxehL{ zZ4FWAa@yn_X$6m93Y@vZhefmly5%eQ=}^25eO~J~Nb6?`_>L?;4SWzvzsTI6w4Gx2!9E=xAhJa-mCMZsHpL zT*fgRr$h8h2!;jr)ngWxCmALefAoiEWXFklFnI7CEpwoM^$l8OQ~KjOoL^>faZ~Qc zu%BOt4D($d`x)6fquR|EALnv=f}|pF#AMzGUKKw9U`Zr&sLWPAD3jdQH2yU&Ck6^n zq6;OJyxZkTaO9E4>#&a>r;D0yW)@E=D@FCw@#lp{L4D>s1vkmt+aBD zv>LjGgTPk=#UWsUJ$(o>bVTB8qQ&dbp)b}W9>MES{D_o8KvB2Ag=A_d3W8V!&F1;Z zNtXXBI#`%82ybn{;M(hK&nPOWR}J9~u}}B}&n0|=k`sE%h}0wFT6PXl6b=vR{BW{w zXXRhVclpc)0we&$nYpkfY?y;^$XLMkz_So+S;+^!&P;hYzOBbIf5JhOA_}UH#L>L$>@UGgB z<6>O_S4_mSkG-KZj~Ed1Dhx&p?jA4?&=SJGYjqw-Hr5W-3+WKS4)->R4TO;|ATZLRz*`@ieH1prtFl)Q3W#p`F|OI`muobm)^u zyj=I(V>jhlHkV@0p!Ff3=yQbwfLw!{&t%@w7CJXN5;|I+hJD-}%N!kHO0J%*JD3_6 zXki=EGG@fU*ZKnV1V^PJ(+!4(99k4Kvvl?Ws+azp)-0&i)P*y0CY*FxDy? z+9)I^G>?%-|y~1CSTL2S9Q|cCrj%2|uhP6GBiNLNGNf zm>OG}_ZVIRj{`g#h$wK8Td~Z#1mwa`onE|Sn4g(hr!Rc=EBcRPPK*O^J%`{%4uM~d z{3SV_1PnnVnj1?lehR2n<`4*e&8n69&rg3rM}6Y+x_-HI z-7?9CU^QQmH*AoG1y>`Pis_qn#BAXIdE7(v;urm;rl+QrefMYMGHA_H{4IQj2IlcX z76D^394&ZRWl0&?j0t7KETk5}`k)kDb_{6kl(D;yWslkO)&twKgPDhB_csqbK!)}| z#R)qR7%X(AIJke26*;1k6AK*zZJ>95fYLYC%y_vQLlMYAu?s`-VMWX{zUMK9eGLqK zU(1_|@i-B)T%3CM90~=&pmVPz6uRw@dZD`9RH=*IyBSnUP(J|@_QWCZli<{^9(F%^1RsKU63j{)yuKnDi$J3nJaRD5s)NGCm$BD5?Wu9m?rR9S zQrXFJODFgo{P-}igv2EbHrWOgx%vRv570M<-7&1kAD?XBs>et0IY6JV;ftT(dwuw! zFA4%VWReI+RzS5;+v(ED#seyX)3OmxnX&^RdpD zmj`!w=g*R3^y(S*fnrN)<3Oxgvrr_LG1mR8vk$s#-hdB3$3YH#=i?aJ`cBTB z-TArz$Ef2wSmo6*P+3oB2Q+Unz{SDr(gU4+ZWivakgxm-gfVQslsw8f+%sXuiEnMW z=EAtF$2bq`(M4|N4RUu2U|VoO{C$AAVxu&)V%ZHk?BCv{?_B)fy7tB#2Vogsj^lNR z?*#s16fQ)DmMNj#ncff!fQF#*I`o1kY45%NEWQ&&!DGx6MBy2c!mykPn5&-NxN)VK z0W^ft>bP`?aVur&8y^g|GW-dUJyyVLlQbuX&|7C)qn1O`T1W7p!DENE=$vaNrfn^G z7dbJ$A+`9}mhlhZ)5Z-TN$7-ZgpD{kQ2G`=s|_9QC}@4`@t$XNogL?m^|l%I+Zb57 z{$*W@Q1~W{q7$+MTQ&-ha+@`;3WGzqD$7@6BZZY6Ivexx9E#Gq3U(`OwzOKl?0Ow@ z_$PG9cYYLKim&4U_;&?91b!0qKOLew>)x*$Vu=DnD?A}z^J-p)UOs*uk_!N2iXc;H z*?dRE-8$?VX9^oaCTI(2F{XLLdh5a|oI5UJVsZQ!3nRSayHC(gZ#mn>fywTIUeSM~ z+=AdK&?w?Y8Ce}){Lmp=pL6GL*&uH2&=NO0e#lW~B-0?3vboP?soR;RWZKLRG)b7`*hiN$FD>FrP-S9b;ySxyt5(?hjM&^nN?h{ z(X7%w`|vvSV*hlgybk3k3>S@y%AgAYJ%{M)UUa5~IHXqKN(UMSk83>PiI3Ljzw{b? z^`tlGt0%rOaO_EM(sAH~lMm5};3RPJ$#2mqr@T!kpZqqRa`M~3KkL+!-Vr$E#DCT) zC%z+a@`?YVijz)wr%pQIUjru||8FXImrgtmeu!hy1>UWbj(xXIG`{*Co!H~OI^nDT zt{3jN7kiV`vJR~e1ry3~C?x+lH^;o#H)z}Gi%#^j-Y_rxh0d_9L)%e0^))b$k0mBP zp0F@pSU8bOd%fglXkDXiQYV7O>x#`tx_k_U!R_)<0SH0N*Pw0q!gy-D>h zys!4oz6XP>kcaK5hdz8y zJJjlOi;9bI1w!e9(V?d$>A9`kmz!kl(UIH6hExX|cefF>; z^xaE-7=Kj4uR&{h3i!9h{_$uW0$gccyFQPQi53lk7VK+Z!|Tv1{;%%BPlvqWx!d@l z=8f?5917#UnG4M~r!od6XX(RG5k5G1#W0H{Q2pmnO?$nzh!-n~000mGNklOi_UY4CIwD8^=1e(ujh z-DICqj3k47Yw;eO;UINfQlSF8`Jsmo>cxX+4mc$I0e6DgSzaJ9}rLRdx7{eOgxW2OFYvG#PD<6;2i{p7E$-z=TEf%{(sSZF=naB z*+mfWyESt3r9)4NG2~+`>i}s)T-(hXR#-N0DfyV$UclVi*q5YC!Yy#F<_*;~sCxXZ z@9VACvsV~$fGxLQ!20>9cI!(3kEyt@F!ZZDPXL@lhqDX|rg*u7fj&BH5uWkfm{<%G zyvO!^j72jGthQY&n1l^ohKG_ zcd(Y99JvVNyFeDNGywl>9?&A3Wz@l!i!{M6%U<{*UWfMm6ZxNU%y9?gAQ0JkExU0w zxhv2*e}XwqfM6as_@N#iS31!;9uh-VO4pE04eN*0{MJJ5=9;^?%X<@h!k%HP^K(At zmCa$ZU+~B_^t@%S017wzq3RpDh~Yj~LS4&s3Uhofk$qT`Y{vHZmRQYG{bN6THB`<=$;2C=H3!knRz3}h!qWzxE zZ)g8j`#t|@dj7so<30STdhR}dqvt;NZ}jZv{I#C>oTum?pZyg5!?T{OXFTgK^^AY~ z3+SJNCxJf;{Qa~4M1T2ze_uCVe;unZqo#2tl9${C8BYNO^8t9L@3c*wyLmdB4ZQZX zf1|(s+yBXurhvKvIPSQ!k%>)*hy`^FNa!6SWE8fWXSlFcYM&ScpPsj3zHHsLfD4qT zT{iW+p#pKgiyU3-iaGfb#c6%yij)lms6ro?+5rO%6%D4gPI4csOcw$+z69fpk!!@C zOBo#Pmb)^KK0Z8y*Rj0eFM07l=%sV;_j>96^YD^q=w&ZK2YqS9OP`^a8ZSc+-T!6C z2RuVBfxZN~|I44D{a^lfddVxE4t+ZGZ}rkwK20w-qqdq9r4`Yz%^=LX;>ffGaTwJ7=9rf#nIFx!kF+Xr9R&gSY6 z)(Xo!rR|g94XpMb1HJ9@^bvstbY_^{x-JAMD~MeAEk?#9cedbYcXZXfz2Xz~p43sU9Ou@`^GwRjBz2zq*X@7a!7*i_>sn z8nyUa(zjgUsi2AjL07r2VdkED+?7N57uw?H&fOXo$S0APOIy4EEYTna=1_ObjK|H-@mI z@9QFCqZJv3R(N+{IkA_17~uQ;xUn&UbJC>$d`iL-oA#+03yirE(C= z(1$Z1&VnokkFN5t3Y!&%6~My>jCo4CdM}Ioe6g-g_s_-bQ4mS4H)OJ$?_qlCBmJ3PY zh=WB4GLH`g54vUHd3r35Un%^}G?q|aGj$)!hD8^-ZI*oqjLMr<*tdeIGH2&9Mie}Y z;iB}9Lw)xTuzD`S$_KR{pv{d_2Oz0~-}E%Keht7WQyAx+{R3Tj#SQWM1|I;!V`kgw zXBzuvxuVnCW+2+dWMj~$S9ZLK(d)M$lbK`YE}uOhXf1ab^JTp~i5#{rp)9rZ$*grY z?Lm!TC^hN6$T$>v%$L8AWBXiqoB+e{1fT;F{Sga~JB`uM!-RgzQNqVKP?GSda8+aW zCgDP^g>FY`%wmAa1q~gJo1)?5&Vcpjs(9yQ-6M}EZnUhqj4HcOeJJeb17ceb{FzSl z_odEjx7~NvKKnkA1H#WA80^}sm+NaMU!a-wIsVfAx*5rbAmS3Z#t2FfQ;%g@;W2}+ zV|IQJ56xH#in%bIp(A12QhbcbB{Nr}9}HdaHyLPyXIC2NhU zGe4;37-zmt{-Y4?VAyC%Tn6S^x}c_QLEcH0(dsxJEK(@xY~D4guj43mNv8GAk6GsH zp{Ue)Lwtgg1g#VejEgHkF`ByQU4W)O2vrw6KBl(W2QdzeeLSB^|JVO|6cc7Niel}$ z4LbS6^K|WXE8?GU@h{&D|4lU?f{4}S(Z?WH$42d?id<0zwa&a@e><2)J{X-tMci2l zv_&anW_hkmIRg@dbh(ySq0Bmj&NdiYIEFrV^?{PcbdX_a@VNDT9Sa$4*tpQFaMVr< z3s#*8ZHCd z0utrmZdGZ1+r!!1mMl`&xfZo)EqYJbvSFQn&Tsz!7h3do4u%C&gPJLYgy14`@oa#P z;>TEv)q3mfQ-NFb_4FBDR3hFl?aXk%t|p(@r^0t5(fw)oN)aSiMGCw_foZ zX~)Ag_S!Kk?|4-Xo`l23#s7-*HCL~YaW&Mq3VPMmD^;)pdi&yP;`g{_xmK)P%REUc z8uNwb{w|X`TSm*-aIwRM#D#`L>lym@JbGI0BEa5)<{*aZ0hr&{Mq|EyOUQMcRDur| zp=fT?)ln;4@KN|g3?6D*@Me(bG*WYM1t@Ue&7!#BH%aN^Vh!H`?-)^_=oT%S(w=+n z#*VuH7#N-R@jY)pQm3A9rdDtWR<2U~Q!f1>n3WvUF^tL&;|5P|wi`ZJpZ?UD+WR?& zYoF&Hu6>{Taqas&<1px9dj9jk-p0rD{JjhI+vlU&Z=a8JI23xQUa$|?cZd(`1^a^M zSA0k>c)syLehK+Oei`|J9-sNlf3c*VAjFdlh%589%COlm@52!_fF0Va;lmvE!qsaW z0#CRqg3>=IV};FLUhht6LU3+~TrC958zFEpza_yC(drl$*g=*@GQTjYcTg_f{FrI+ zM^%`cx%x2JCH&(;2&m&3F0x^v`$v@({>QZpe_@@WIJt?(?+Z*T`o6?8?*jGgrWa4? z*Y@1qLo;Bs;8(3!txtUL7@hUai{c;iTCtKt09NrdSi7Dt$BYwk@Q(b5ODnGf1g`Oe zVrV8t`oDkkIPJ7#N@tz%LoEeo_V|I$Tnf(kzRo-Y3V+s8^8o%#^B{C*TK7Gjx%7KF zYw2Y=>rAKte`cVQtvmBFof-Hpe&5wketad_x{(n3>Z_$dCzu%4d0s(%F{**SZdL_) zeMsdJI$tV^S{#~)8yC&_Z~)i<{HqN%7Sw}Tw`IYpWWeDVksUgZZ$4}=q2gjWZ1AQX zG1~+8eh%it0MqyF`2Ncuh1G&t|4lI9ck%TUoL@f-e<}4ldBrMTO?+oI{LXIpeccDa z1#0-!$OplEJP}B=QWq#GM2@K005{*Al2)!*qkn(Pp*r)w&e!r4v+-{j`LApGmB@#{ z2QXq1!1IYiN|1=nAqd-m-jgTIoBw$){r2xXfHI)dpwVq0(Wx#}&|LieTdI)lntgGy z(2j>I3dnV?U3UeqAuIg1#Qb-L!41o#>u!{;2c`jZ_VXc# zCj$An_&uJBa>zT69-a(y6Pr>XK2~NlS)Y9WCw2PQ&eF2w90KP6>LKt!bDUXvc#Pn) z6nsa`M-Jq(=Y8*{_k8G?`okwYoF0n!iT$msE_l^X6l}%^MaSD7{+0s-bszYKU*9IE zC?@{}WAh0~UfHtM}SMrjSi>3KEy9E7_sS9UR&0ct%KFt3IYh zmB%dK0eS(3HfO*Ps&Y$ECsdPmd{3lMTMSE1xWbzn8_w>#kS&|~&Nv6`k>zSL4RD9I z-i2?>DSi&Xn(hGj9ef3^A2(8XE_@gLwG{rfH%M1sCtU?huf3iF!Qt>vZsJFm&Wo{Y zUO_Ee&txKZCNt{_trHwl^Q`+?PT&_m@mXDPCa*&r#AW2-zq9N^;DhQz;MgrO9m;Wi z*!I^yc;`|2(7zp}kG}g;I{dvy>xlOq%^mq@9rb~u^{EehTA%vh zr**XH2R@@yzOpoaV&f-3ElL+=6d%(FN?r8DiO%-;EoG^UZuK#AvRA+bvu5>b9rfN1 z>%240)3Rl=x{-rc4}o9He1IL_2f*>;5VVr{8R!u9+k@{N|8&UH^c#=4UmXJKaTOyc zsiz)ad{Sg*9|A_9w{r0pyY&jW_p8M(sn-#s^ig~~&al7mls_dLTxBXnwLmLYM>7Tv z#jvqr&fzHLO}+xfX(~Qj?OGPBER9&~BaiyBj{MYDz*lwTQQ%X@L66f>M;+hcMCb|N zM1ATg<0L3J@+5R8!xtQV)G6?%=;%+KqE8=nsy+k#?9r#_bDus{$AJI%?5X`nIZmMb3TyBDbAvz0oRSx{UCUopAhd>3{x#$CCU zd^ZkM-W2!=5TVM2F$T?XdogS6npHaS)1TG1&%RK8H5(oRe@TxgL09n3!3960F8t8h z2i*79@1@th{qObI|M$WDC}y%R#@rf2gN2-2q5;-HkvDo%Y%+ppa_}j5y3!JBXa9PD z#^K!`2ePsM)Qz3c3uJuFr?9mo z(&CFhenp2zmR$IT3oi~pD+OifC_ZGmY8~^T59@-Z z=kY2us~h|}1o+9R{O_Ck3%KKZd>=R$y|JEght#}rulwCY2fXoVddMU0!&oXqn)CYS zV0;N!w7U1QB6^s~N;}*8tU?`ap{%n~qKfcK6r&6ZQ)HW<0d)2W9$(Y? zWGMP9Rp0u=2)``HOuB6pX0Y)Jba!6fxh6{Dpr$hc^ zaxg8(Cd}2ua~#4s9s9{6bpAJa9pXEI|Mdz#4gAH}-$B-OuVH@O@wj!YkXb^@L)`zN z_ttCw`B{3@V}HF26N4sJ7RChy7x^G2P+C^%^F!0~S%NQqg)cz|d>CD4ijPGj1TJHq zK7+9_Aq}r%PixCKg{xObsC=a!xu9y=@d2nRQ#%#v&_&89%oTe3=^e=^v5DTzEwh-` zdr=u$isG}Ks_bsYvuqmnDhJI2T^sQe%bbYme!|D&n>SST z_=xzj9h!xOOZ{m~90J0*0A1szfmx|i->q#cQ!Q6Q5Xo)KS(=Nib-WIp{JGESJLm8^ zv{F0`3gQs(yNmc&EEuaVdUzkWD13P9hwveI#UansBY%rSpyb&x&JYqDj%uw$eO&GYCx2>S$TX@ANxO5K?YpruMw6!B_ zOZoE(lwpmI*a6mE3M|-e+}^z70TzsF*BGahy?CPV#FSFdRw-2ZbssUdY3*xXjNm2n0We(HaR`{koF{>0QgI<} zz|ntBNAbZxa~z9ho2(dsaYYWci}8F2zHsP=bP=yZ_4fqvB;Y^{p9C>}mVz>N96)H~ zXo}6b?)iXw>O}|rogVm*JsE-w(8G8r2r}1kvs^LO3RNbzt`ml=6nVp$+#CNM2$FMf!jTzLvk|Tgvnw3V8w%(N~#<0GOkp>$siqeyc zMTAcV_BphfWx?51Rxb5?8#?>RdiR3{-#smC3wQ;?Ui**>vd5LF!I;5EU#}28^z)UP zmf?bOf%*G@X@&2?fmZRCUs^jjM2AG2!aAC{Ozu#YhpS}j+{4edtmBXTgf942c^z8D z_W}Q|z)u4I#<)HSJpTN`w_NkI-Fg4|!|$t?zvbC__+x%OMi8$+jH39!o92$jtyS47 z;GzV%%BaFNU$)+;dL~ram|SP;b(RY}ZM^aF)w&E1%>XWl$1IGM9X3)7^tQEXGAaWy zr!AzL<8ZqPWu!W_D7Zd0HqV~P`Oa)x6pD)xq^=+nK84RLM+>rCs72Ryd_5p7_Ew^A zB8I7mAO6U_F8HjFi7ya5{bYo_Ridx=-f&I3n2ad0R&t@44`gI?!9zvg0I@NVQ*Bi; z9fD5yMj!gFTf0i9e(p26^juztR!jaJfw9mbaH0E2;CupvGIg$%{`w>Cr3ETtgH21=n4MJqn+Q-WfY$y@6Qcz;4_!>s@AVn_V}KWlKRW zq@34&%(=aq&{xl{L-fcfw+cMya4Zt^xFi;n>v>PyuQZ*nix4EBKxT1T)gn#wmx~l? zKE=k^C(C-F7yYacz*`pjIWqlHwV4O@QPts3#JKyrn0yGLprk1NT6j@>t>ohOYfx)v zeCV!#`7tw)9KFJ7_&x`|6L1Lr^CKV9cg{RdE0)h{IZp!r@eO{FEdO~XzwdiY59g)q zDWNR)Kz8$wNMH0%&(H%OdY@7Teo5$xU#1OeSmEaBadd5Ll<*?0@ON?ClcKyOnS#r*XWXaY7 zOmPre#UZG?3$en5NH+~*XwHWq5mmV49%ZDAlHg&_az4i&^$A_{t)*JAVirjLV;rm2 zNcHywK8SHJvm(c+33wW@)PFpk2R-7xdc|9wqu==LhsZ-@rT8zLD5M+=rpk0Onx31> z*wL^SnyM$f2k2qDVZCX^eA)V{GbNx^h9_nk@F5%6Lg$P7n99eyFx&Q85=xOo;mZ;g zKJ^Qf*@%o+mN#8&Wdh|uWb`~CqJS%3sQtv@@N$?>@TH&aznz%BKI7;;H!Q;PlB&~ z{xkaSc^AfCsPO?X{0IBu&nUH$4`TG>LL-J$)?D#P2>uH~#)BVuU%i~yp+`NoI|LNI zdT$&99=7$-KAtXLptg(f)j)`nO3Sc8=r=PLI+3u~PbhXdazR?`X} z6!@^?Z^}rjewN+8%Ft|G^|1{;p-=D(9Z!3&@en=;0fsP*LjZ3Y{|-`2iacnZTfo7H zoq=VWqSmZ8tY=Ky0_32`t(CtMeCeYf)OXH4SF2WR&`N$Pv}%>)Kj7!rp*R4HAH(|M zcz`0%h)ey8L8c@0_j}Mi_0l)~y&n9?^4B+Wmq;e59L3J@%noG0LoAlui!7fibv$;F zKda9*grLRWmco@7mJ94zbl72NR?&C99O}5(r^1TheL;w6K;4+O39!+C-Fj{QfR=|l zZD~V}^^Stba&d;g%@j>?9G1WHd4#1BL3UvlAN0tgc-=1mZ$EEm_*kZI z>iP=ac11t;FF9CF=o0>ZT`| zM4qYI$wkP*XZ_lh`ugWTt?!b3QiDQIPA#^2T1C(^xy^O5gdi zwYeKTTj*fEu5#hUdU3`(`;gdP{qB9JpHWqSe<|B`U<`KB#O*5WPD6r{1YR|v|IJajgg}`9L{%r z!*^d7zxnbVAiToA#tnh-m_)EKOmH48%lQ@?eUlAYDV(xGPmcB+z7DPCNwAs&u!h6n zL*PF_>Ky1He|!iMl@N!3m?P#>p8$ReOd;R@0r${r5BUc@>M;+Z3r+&{5Co6f)F3d; ziVmIR#ppEZRL(UPYC(NbhHG3bdtB22!p4QxYsn4IBBH0oP~&lnm1!5BVukNKY|03PTs_-b0LHedh<9FA+t3(sCAyc zi_Wl2Y2l4MI(8J^_Q=I!(|auEh&LK#`2e_hd?$@O@FSS7k&>t2vt;V`a2H<~7rf-T zT5_<$`I&nBm`~`lYHp^6oM&%qb;6N{>!Q<_YW1oOTEijm6Jch(;!m(TrxH_gAvk|O z0X!EU0MDz!hv3)wPVkQR?W4y(;nB=1(X|7`CMBkfoXx3x;DuiC!4?!HO?U@_w*iJ` zZmhQg&^Feg^n#WSVXkbUw!`5AX9mn~jpuE%VWX*Gerv(w85@t+7gLz3k7;IEGuk1# zJGcwLi_4wRTT)i0?R?vDzch=k1sNR_e??z-_XinWDZEw*K7~Egrv=3_LDfu6NE5)t zgD7vcy?VgV(?q?iTot+mty}7IqHLWT_bUtd;t7JGo5H8KLvacCP&f(9=M0z^p0ogkN)?g zB^SQYb(d3u4-yvylpe$L?N#V(xoH&d;gtdVE=Avq9fP)>>Q2*>$P;_75UV2}05o&; zD7J@Il1h<^`NDqmC&OmE93MmQ)|IR1%vd}PA z$aL`X!TWV#dXgfcIQj!Xum%?hecVs>E+Di^)J1`dQ-q)lPgt7!L3&%{Ta^)|`kRgq zLCIAzP4;7MrtA1l@E;%kpe{c1oOlwf;~uO?o8rnqa*_opcojROZ-X%uK0}3hLwpjUE02ie>ln5r;+qz2&-ILA z1e`SYV+W({4uGwh3!oKdqXAKG^+q<#u?!0zIhk4mWrNLmf|O=q(UmUP?LU;^Wmq3@ z7yPU%oyvky*mL}B$KQqJvBK8rCMU9{rzF23_-^UDrVGdK)FoK-;B!Af$N&Hk07*na zRFy&H3BYiE5XcpXgVA}OI5`yh19`i_N^-u5$4bgfEEsc4F0q~aS#`FXYvJ6}$oB1u)v}OXco|OxT7q<-SjKwdBFp~0pLLm~w|}Nzj`#mXJOSc0 zXr%Ga)3HXw%|%h+A~V7kadB9X;*mhuDzz_s%*Q#5;ny{VZz~tO$U7M^hb;5!)~wVC zpZ>JIbJhj?&Oq{0!0=zP_aD^jzoavS^Myon8itD{4g!J?!F}#~H@)p&o~1wjQw~8E zqhsiLxIe0*qxj9+RB7R>zgNA$dc%H(dBc3c+-vP+>I|y2Qumz;EHn}m%TxX^+=32O zY3HrNGZ3u~6$yqgRwgT1%uol9ty{yW33OXGrfw=sqSL@;ne_~SyR;%XL?Nt%E(wsvnR=bZmOGAV!tLF6zeK19y(t&2z_$koPbF1(%_+X?eS87w`O$YF%+Z?kPFRYh`Ex|JI z#YYrGZ-e)o1MsQFf_E?Lts@A_ZB2#bqL1$Y9DsNQY9zlh#4C_H0PmfPen`u@JMuV| zCERI49pbQ&TZl=Ki|00k!cKp3PoT)H3g6PH4gL#|ICsP zg1;lQl5YtmZNhslCmeUaUbyd}y7I~!v33nx#}yKCTv+u&2L=gkLD3f8eg-<`$#vWq zxa_Vos_Nofu2olQ)k33h)fdWk6=uOJ9U>u9!^xT&TZJbp^5#l|>BlW1SJFf8T#CSc zSJ}%1C<3nNr4)X)tNVBnbLhvmOw3K_YjF|BcR?KGFTwHp!%B|$wPLc5L9V`uwER?J z)}V@J!G;b#5M|6R7551?laHA(^*B9pwVA1KzVpGXnY&!JrXYbTui;7Xm7|Wu=*!Pq#oa6d?-#h_I$neRYy_$H#i|crE+I>MhL8s zuk%rSgrDX+S~fg;Q^Va&Sxd8hA2ZfhrTOrIp@q8ogv^Cj^uCS-whhHtM%Z2(NwR(= zJNUv3uGRqxDS7fe>{5$CUS4Tv!@tVz7eiPV#gg#{mg!}?D5zTe=KXtsasaZtn8_PK zb^DJyxBS-2@75cjGaFn~06K%*+K-qTvQXz;eBF;+h$Pn6qTP2B@co`bG!my$F{6Q9^uj#bVf8FVKZ+a z+s8of`O3M%yYQ3eFhG%Kb<9T((^+3XLn}E1{$msVy9BG(WUb~9tcm|~O_u-dqqXZ> z9rxAq^wJl6R99Vf1A}G7oZ;j#0~WF`A?3J+IJnH!vSir0K0)F_$c|!-Rvd$!d#QeG zP8h0OXt{=gOBuW9vTs^u4QySqb6spR$S3SugLeiV$Ef2vKO=XGKHtZ-mP@DirX#tx zTmwT`w&&<)TS)ZSn0nI<_w*ND7k}f|A0p9#_vEZuo&p;x`r^-; zq4<3m+I};K@!eb9{KUsL$e`6N4pro8E_Z!08Dtb=Dtr=ilfm4FJXtDXU(1u=%b)y| zE?in(hkOA1x61s_J+9&q_@7$Prg;s5)Ra+ujGHfk$zP`xgYdislu4B_Ui0xid+qNxVGAOxZdCANh#o}ep zu$_j@Md;dCA4N`QYvjV{Jj`2XC^ip{7@5Ke7x=2r@Gc%F;imxM!o`Ihp!j_VX1Z4g z)Abw#V+Ptk0sKByJ4Xb@4C;${&!tcoT#-1SjBk%VAM&-W%46Cpa@9CS@_c}4)~rnZ zo#2S~yiezT^PKX+t^^i~Lx@-M~mssTN)%G}*mhs7r#PAR< zaHUx`SE>8;_W&HndV7VnFkGvxQdHe+ie~<`MQb0w|Hs{*Lf_EAML*-$A7t}BB#Ms
>f(#qS=@f|7it@=)hn8@4kK`{+j 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 tQ~duA00960N4~q^00006Nkl` textElement.textContent = "Added to Memory" break @@ -184,7 +184,7 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement { font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; ` - const iconUrl = browser.runtime.getURL("/icon-16.png") + const iconUrl = browser.runtime.getURL("/new_logo.png") button.style.backgroundImage = `url("${iconUrl}")` button.style.backgroundRepeat = "no-repeat" @@ -232,7 +232,7 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement { z-index: 1000; ` - const iconFileName = "/icon-16.png" + const iconFileName = "/new_logo.png" const iconUrl = browser.runtime.getURL(iconFileName) iconButton.innerHTML = ` Save to Memory @@ -286,7 +286,7 @@ export function createConnectedIndicator(onClick: () => void): HTMLElement { flex-shrink: 0; ` - const iconFileName = "/icon-16.png" + const iconFileName = "/new_logo.png" const iconUrl = browser.runtime.getURL(iconFileName) iconButton.innerHTML = ` @@ -370,7 +370,7 @@ export function createT3InputBarElement(onClick: () => void): HTMLElement { background: transparent; ` - const iconFileName = "/icon-16.png" + const iconFileName = "/new_logo.png" const iconUrl = browser.runtime.getURL(iconFileName) iconButton.innerHTML = ` Get Related Memories from supermemory @@ -443,7 +443,7 @@ export function createProjectSelectionModal( margin-bottom: 20px; ` - const iconUrl = browser.runtime.getURL("/icon-16.png") + const iconUrl = browser.runtime.getURL("/new_logo.png") header.innerHTML = `

@@ -712,7 +712,7 @@ export const DOMUtils = { if (icon && text) { if (state === "success") { - const iconUrl = browser.runtime.getURL("/icon-16.png") + const iconUrl = browser.runtime.getURL("/new_logo.png") icon.innerHTML = `Success` icon.style.animation = "" text.textContent = "Added to Memory" diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts index acdfe65b0..f3514b97c 100644 --- a/apps/browser-extension/wxt.config.ts +++ b/apps/browser-extension/wxt.config.ts @@ -45,7 +45,7 @@ export default defineConfig({ ], web_accessible_resources: [ { - resources: ["icon-16.png", "fonts/*.ttf"], + resources: ["new_logo.png", "fonts/*.ttf"], matches: [""], }, ], From dd3ea0195961089ca7207eafdf665ce4ca29f163 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Wed, 20 May 2026 14:42:31 +0530 Subject: [PATCH 4/5] add ref on nova from extension --- .../entrypoints/background.ts | 63 +++++++++++++++++++ .../entrypoints/content/chatgpt.ts | 5 +- .../entrypoints/content/claude.ts | 2 +- .../entrypoints/content/gemini.ts | 2 +- .../entrypoints/content/t3.ts | 2 +- apps/browser-extension/utils/types.ts | 3 + apps/web/components/memories-grid.tsx | 29 ++++++++- 7 files changed, 101 insertions(+), 5 deletions(-) diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts index e6dba3eb6..86f1b0644 100644 --- a/apps/browser-extension/entrypoints/background.ts +++ b/apps/browser-extension/entrypoints/background.ts @@ -21,6 +21,45 @@ import type { MemoryPayload, } from "../utils/types" +const PLATFORM_LABELS: Record = { + chatgpt: "ChatGPT", + claude: "Claude", + gemini: "Gemini", + t3: "T3 Chat", + twitter: "X / Twitter", +} + +function normalizePlatform(value?: string): string | undefined { + if (!value) return undefined + return value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") +} + +function inferPlatformFromActionSource(actionSource: string): string | undefined { + const source = actionSource.toLowerCase() + if (source.includes("chatgpt")) return "chatgpt" + if (source.includes("claude")) return "claude" + if (source.includes("gemini")) return "gemini" + if (source.includes("t3")) return "t3" + if (source.includes("twitter") || source.includes("x_")) return "twitter" + return undefined +} + +function inferPlatformFromUrl(url?: string): string | undefined { + if (!url) return undefined + try { + const hostname = new URL(url).hostname + if (hostname === "chatgpt.com" || hostname === "chat.openai.com") { + return "chatgpt" + } + if (hostname === "claude.ai") return "claude" + if (hostname === "gemini.google.com") return "gemini" + if (hostname === "t3.chat") return "t3" + if (hostname === "x.com" || hostname === "twitter.com") return "twitter" + } catch { + return undefined + } +} + export default defineBackground(() => { let twitterImporter: TwitterImporter | null = null @@ -107,11 +146,32 @@ export default defineBackground(() => { content = data?.url || "" } + const platform = normalizePlatform(data.sourcePlatform) + || inferPlatformFromUrl(data.url) + || inferPlatformFromActionSource(actionSource) + const platformLabel = platform + ? data.sourcePlatformLabel || PLATFORM_LABELS[platform] || platform + : undefined + const metadata: MemoryPayload["metadata"] = { sm_source: "consumer", + sm_origin: "browser_extension", + sm_origin_action: actionSource, website_url: data.url, } + if (platform) { + metadata.sm_origin_platform = platform + } + + if (platformLabel) { + metadata.sm_origin_platform_label = platformLabel + } + + if (data.sourceSurface) { + metadata.sm_origin_surface = data.sourceSurface + } + if (data.ogImage) { metadata.website_og_image = data.ogImage } @@ -242,6 +302,9 @@ export default defineBackground(() => { const memoryData: MemoryData = { content: messageData.prompt, + url: messageData.source, + sourcePlatform: messageData.platform, + sourceSurface: "prompt_capture", } const result = await saveMemoryToSupermemory( diff --git a/apps/browser-extension/entrypoints/content/chatgpt.ts b/apps/browser-extension/entrypoints/content/chatgpt.ts index d6049cab1..0ec50d926 100644 --- a/apps/browser-extension/entrypoints/content/chatgpt.ts +++ b/apps/browser-extension/entrypoints/content/chatgpt.ts @@ -341,6 +341,9 @@ async function saveMemoriesToSupermemory() { action: MESSAGE_TYPES.SAVE_MEMORY, data: { html: combinedContent, + sourcePlatform: "chatgpt", + sourceSurface: "memories_dialog", + url: window.location.href, }, actionSource: "chatgpt_memories_dialog", }) @@ -701,7 +704,7 @@ function setupChatGPTPromptCapture() { data: { prompt: promptContent, platform: "chatgpt", - source: source, + source: window.location.href, }, }) } catch (error) { diff --git a/apps/browser-extension/entrypoints/content/claude.ts b/apps/browser-extension/entrypoints/content/claude.ts index cb1a5f389..c88fe70a6 100644 --- a/apps/browser-extension/entrypoints/content/claude.ts +++ b/apps/browser-extension/entrypoints/content/claude.ts @@ -559,7 +559,7 @@ function setupClaudePromptCapture() { data: { prompt: promptContent, platform: "claude", - source: source, + source: window.location.href, }, }) } catch (error) { diff --git a/apps/browser-extension/entrypoints/content/gemini.ts b/apps/browser-extension/entrypoints/content/gemini.ts index 04c670a20..cf763c108 100644 --- a/apps/browser-extension/entrypoints/content/gemini.ts +++ b/apps/browser-extension/entrypoints/content/gemini.ts @@ -523,7 +523,7 @@ function setupGeminiPromptCapture() { data: { prompt: promptContent, platform: "gemini", - source, + source: window.location.href, }, }) debugGemini("capture response", response) diff --git a/apps/browser-extension/entrypoints/content/t3.ts b/apps/browser-extension/entrypoints/content/t3.ts index c7bdb09a1..13ce233d9 100644 --- a/apps/browser-extension/entrypoints/content/t3.ts +++ b/apps/browser-extension/entrypoints/content/t3.ts @@ -546,7 +546,7 @@ function setupT3PromptCapture() { data: { prompt: promptContent, platform: "t3", - source: source, + source: window.location.href, }, }) } catch (error) { diff --git a/apps/browser-extension/utils/types.ts b/apps/browser-extension/utils/types.ts index 8cec22411..e17f4ff9f 100644 --- a/apps/browser-extension/utils/types.ts +++ b/apps/browser-extension/utils/types.ts @@ -38,6 +38,9 @@ export interface MemoryData { url?: string ogImage?: string title?: string + sourcePlatform?: string + sourcePlatformLabel?: string + sourceSurface?: string } /** diff --git a/apps/web/components/memories-grid.tsx b/apps/web/components/memories-grid.tsx index d2f93d544..d110d2e87 100644 --- a/apps/web/components/memories-grid.tsx +++ b/apps/web/components/memories-grid.tsx @@ -90,6 +90,32 @@ type OgData = { image?: string } +const EXTENSION_PLATFORM_LABELS: Record = { + chatgpt: "ChatGPT", + claude: "Claude", + gemini: "Gemini", + t3: "T3 Chat", + twitter: "X / Twitter", +} + +function getExtensionSourceLabel(document: DocumentWithMemories): string | null { + const metadata = document.metadata + if (!metadata || typeof metadata !== "object") return null + + const label = metadata.sm_origin_platform_label + if (typeof label === "string" && label.trim()) { + return label.trim() + } + + const platform = metadata.sm_origin_platform + if (typeof platform === "string" && platform.trim()) { + const normalized = platform.trim().toLowerCase() + return EXTENSION_PLATFORM_LABELS[normalized] || platform.trim() + } + + return null +} + const ogCache = new Map() const ogInflight = new Map>() const ogFailures = new Map() @@ -1147,6 +1173,7 @@ const DocumentCard = memo( pluginDocument?.kind === "claude-code-doc" ? claudeCodeTokenBadge(document) : null + const sourceLabel = getExtensionSourceLabel(document) const date = new Date( document.createdAt, ).toLocaleDateString("en-US", { @@ -1154,7 +1181,7 @@ const DocumentCard = memo( day: "numeric", year: "numeric", }) - return badge ? `${badge} ยท ${date}` : date + return [sourceLabel, badge, date].filter(Boolean).join(" - ") })()}

From 2cc26c15c235adb10aad566cc681e25dbe431aa9 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 09:15:05 +0000 Subject: [PATCH 5/5] fix: resolve Biome formatting issues in background.ts and memories-grid.tsx Co-Authored-By: Claude Opus 4.5 --- apps/browser-extension/entrypoints/background.ts | 16 +++++++++++----- apps/web/components/memories-grid.tsx | 8 ++++++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts index 86f1b0644..2066cf161 100644 --- a/apps/browser-extension/entrypoints/background.ts +++ b/apps/browser-extension/entrypoints/background.ts @@ -31,10 +31,15 @@ const PLATFORM_LABELS: Record = { function normalizePlatform(value?: string): string | undefined { if (!value) return undefined - return value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") + return value + .toLowerCase() + .replace(/[^a-z0-9]+/g, "_") + .replace(/^_+|_+$/g, "") } -function inferPlatformFromActionSource(actionSource: string): string | undefined { +function inferPlatformFromActionSource( + actionSource: string, +): string | undefined { const source = actionSource.toLowerCase() if (source.includes("chatgpt")) return "chatgpt" if (source.includes("claude")) return "claude" @@ -146,9 +151,10 @@ export default defineBackground(() => { content = data?.url || "" } - const platform = normalizePlatform(data.sourcePlatform) - || inferPlatformFromUrl(data.url) - || inferPlatformFromActionSource(actionSource) + const platform = + normalizePlatform(data.sourcePlatform) || + inferPlatformFromUrl(data.url) || + inferPlatformFromActionSource(actionSource) const platformLabel = platform ? data.sourcePlatformLabel || PLATFORM_LABELS[platform] || platform : undefined diff --git a/apps/web/components/memories-grid.tsx b/apps/web/components/memories-grid.tsx index d110d2e87..9c77e0f41 100644 --- a/apps/web/components/memories-grid.tsx +++ b/apps/web/components/memories-grid.tsx @@ -98,7 +98,9 @@ const EXTENSION_PLATFORM_LABELS: Record = { twitter: "X / Twitter", } -function getExtensionSourceLabel(document: DocumentWithMemories): string | null { +function getExtensionSourceLabel( + document: DocumentWithMemories, +): string | null { const metadata = document.metadata if (!metadata || typeof metadata !== "object") return null @@ -1181,7 +1183,9 @@ const DocumentCard = memo( day: "numeric", year: "numeric", }) - return [sourceLabel, badge, date].filter(Boolean).join(" - ") + return [sourceLabel, badge, date] + .filter(Boolean) + .join(" - ") })()}