mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-11 22:51:05 +00:00
feat(extension): Add support across gemini & claude, live tool tip, auto complete support (#971)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
86c3ad69d1
commit
2426305a2d
22 changed files with 2148 additions and 781 deletions
|
|
@ -21,11 +21,55 @@ import type {
|
|||
MemoryPayload,
|
||||
} from "../utils/types"
|
||||
|
||||
const PLATFORM_LABELS: Record<string, string> = {
|
||||
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
|
||||
|
||||
browser.runtime.onInstalled.addListener(async (details) => {
|
||||
if (details.reason === "install") {
|
||||
if (details.reason === "install" || details.reason === "update") {
|
||||
await trackEvent("extension_installed", {
|
||||
reason: details.reason,
|
||||
version: browser.runtime.getManifest().version,
|
||||
|
|
@ -107,11 +151,33 @@ 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
|
||||
}
|
||||
|
|
@ -166,7 +232,6 @@ export default defineBackground(() => {
|
|||
response.results?.forEach((result, index) => {
|
||||
memories.push(`${index + 1}. ${result.memory} \n`)
|
||||
})
|
||||
console.log("Memories:", memories)
|
||||
await trackEvent(eventSource)
|
||||
return { success: true, data: memories }
|
||||
} catch (error) {
|
||||
|
|
@ -246,12 +311,12 @@ export default defineBackground(() => {
|
|||
platform: string
|
||||
source: string
|
||||
}
|
||||
console.log("=== PROMPT CAPTURED ===")
|
||||
console.log(messageData)
|
||||
console.log("========================")
|
||||
|
||||
const memoryData: MemoryData = {
|
||||
content: messageData.prompt,
|
||||
url: messageData.source,
|
||||
sourcePlatform: messageData.platform,
|
||||
sourceSurface: "prompt_capture",
|
||||
}
|
||||
|
||||
const result = await saveMemoryToSupermemory(
|
||||
|
|
|
|||
|
|
@ -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 = false
|
||||
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,39 @@ 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}`
|
||||
console.log(
|
||||
"Prompt element dataset:",
|
||||
promptElement.dataset.supermemories,
|
||||
const memoryText = showMemorySuggestion(
|
||||
"chatgpt",
|
||||
promptElement,
|
||||
response.data,
|
||||
)
|
||||
debugChatGPT("memory suggestion rendered", {
|
||||
memoryLength: memoryText.length,
|
||||
})
|
||||
|
||||
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 +244,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)
|
||||
|
|
@ -218,7 +283,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 = `
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
|
|
@ -278,11 +343,16 @@ async function saveMemoriesToSupermemory() {
|
|||
action: MESSAGE_TYPES.SAVE_MEMORY,
|
||||
data: {
|
||||
html: combinedContent,
|
||||
sourcePlatform: "chatgpt",
|
||||
sourceSurface: "memories_dialog",
|
||||
url: window.location.href,
|
||||
},
|
||||
actionSource: "chatgpt_memories_dialog",
|
||||
})
|
||||
|
||||
console.log({ response })
|
||||
debugChatGPT("memory dialog saved", {
|
||||
success: response.success,
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
DOMUtils.showToast("success")
|
||||
|
|
@ -300,272 +370,242 @@ function updateChatGPTIconFeedback(
|
|||
iconElement: HTMLElement,
|
||||
resetAfter = 0,
|
||||
) {
|
||||
if (!iconElement.dataset.originalHtml) {
|
||||
iconElement.dataset.originalHtml = iconElement.innerHTML
|
||||
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
|
||||
}
|
||||
|
||||
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 = `
|
||||
<span>✓</span>
|
||||
<span>${message}</span>
|
||||
`
|
||||
|
||||
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 = `
|
||||
<span style="font-weight: 600; color: #fff;">Included Memories</span>
|
||||
`
|
||||
|
||||
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 = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`
|
||||
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)
|
||||
if (message.toLowerCase().includes("searching")) {
|
||||
setMemoryMarkerStatus(iconElement, "searching")
|
||||
showMarkerPopover(iconElement, message)
|
||||
return
|
||||
}
|
||||
|
||||
iconElement.innerHTML = ""
|
||||
iconElement.appendChild(feedbackDiv)
|
||||
|
||||
if (resetAfter > 0) {
|
||||
setTimeout(() => {
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
|
||||
delete iconElement.dataset.originalHtml
|
||||
}, resetAfter)
|
||||
}
|
||||
setMemoryMarkerStatus(
|
||||
iconElement,
|
||||
message.toLowerCase().includes("error") ? "error" : "none",
|
||||
)
|
||||
showMarkerPopover(iconElement, message, undefined, fallbackReset)
|
||||
}
|
||||
|
||||
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 existingMarkers = Array.from(
|
||||
document.querySelectorAll(
|
||||
`[id*="${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer"]`,
|
||||
),
|
||||
)
|
||||
if (existingMarkers.length > 1) {
|
||||
debugChatGPT("removed duplicate markers", existingMarkers.length)
|
||||
for (const marker of existingMarkers) {
|
||||
marker.remove()
|
||||
}
|
||||
} else if (existingMarkers.length === 1) {
|
||||
debugChatGPT("marker already exists")
|
||||
return
|
||||
}
|
||||
|
||||
const parent = button.parentElement
|
||||
if (!parent) return
|
||||
|
||||
const parentSiblings = parent.parentElement?.children
|
||||
if (!parentSiblings) return
|
||||
|
||||
let hasSpeechButtonSibling = false
|
||||
for (const sibling of parentSiblings) {
|
||||
if (
|
||||
sibling.getAttribute("data-testid") ===
|
||||
"composer-speech-button-container"
|
||||
) {
|
||||
hasSpeechButtonSibling = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasSpeechButtonSibling) return
|
||||
|
||||
const grandParent = parent.parentElement
|
||||
if (!grandParent) return
|
||||
|
||||
const existingIcon = grandParent.querySelector(
|
||||
`#${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer`,
|
||||
)
|
||||
if (existingIcon) {
|
||||
button.setAttribute("data-supermemory-icon-added-before", "true")
|
||||
return
|
||||
}
|
||||
|
||||
const saveChatGPTElement = createChatGPTInputBarElement(async () => {
|
||||
await getRelatedMemoriesForChatGPT(
|
||||
POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_SEARCHED,
|
||||
)
|
||||
})
|
||||
|
||||
saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
|
||||
|
||||
button.setAttribute("data-supermemory-icon-added-before", "true")
|
||||
|
||||
grandParent.insertBefore(saveChatGPTElement, parent)
|
||||
|
||||
setupChatGPTAutoFetch()
|
||||
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),
|
||||
})),
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const saveChatGPTElement = createChatGPTInputBarElement(async () => {
|
||||
await getRelatedMemoriesForChatGPT(
|
||||
POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_SEARCHED,
|
||||
)
|
||||
})
|
||||
|
||||
saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return input.parentElement || document.body
|
||||
}
|
||||
|
||||
function findChatGPTComposerButtons(
|
||||
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 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() {
|
||||
|
|
@ -586,12 +626,29 @@ 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 +661,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 +670,7 @@ async function setupChatGPTAutoFetch() {
|
|||
})
|
||||
|
||||
if (promptTextarea.dataset.supermemories) {
|
||||
delete promptTextarea.dataset.supermemories
|
||||
clearMemorySuggestion("chatgpt", promptTextarea)
|
||||
}
|
||||
}
|
||||
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
|
||||
|
|
@ -631,7 +689,7 @@ function setupChatGPTPromptCapture() {
|
|||
const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false
|
||||
|
||||
if (!autoCapture) {
|
||||
console.log("Auto capture prompts is disabled, skipping prompt capture")
|
||||
debugChatGPT("auto prompt capture disabled")
|
||||
return
|
||||
}
|
||||
const promptTextarea = document.getElementById("prompt-textarea")
|
||||
|
|
@ -641,26 +699,18 @@ 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)
|
||||
|
||||
debugChatGPT("prompt submitted", {
|
||||
source,
|
||||
promptLength: promptContent.length,
|
||||
})
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.CAPTURE_PROMPT,
|
||||
data: {
|
||||
prompt: promptContent,
|
||||
platform: "chatgpt",
|
||||
source: source,
|
||||
source: window.location.href,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
|
|
@ -682,7 +732,7 @@ function setupChatGPTPromptCapture() {
|
|||
})
|
||||
|
||||
if (promptTextarea?.dataset.supermemories) {
|
||||
delete promptTextarea.dataset.supermemories
|
||||
clearMemorySuggestion("chatgpt", promptTextarea)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -705,6 +755,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" &&
|
||||
|
|
|
|||
|
|
@ -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 = false
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -39,6 +58,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() {
|
||||
|
|
@ -58,7 +89,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(() => {
|
||||
addSupermemoryButtonToClaudeMemoryDialog()
|
||||
addSupermemoryIconToClaudeInput()
|
||||
|
|
@ -84,9 +115,11 @@ function setupClaudeRouteChangeDetection() {
|
|||
element.querySelector?.('[role="dialog"]') ||
|
||||
element.querySelector?.('div[contenteditable="true"]') ||
|
||||
element.querySelector?.("textarea") ||
|
||||
element.querySelector?.("button") ||
|
||||
element.matches?.('[role="dialog"]') ||
|
||||
element.matches?.('div[contenteditable="true"]') ||
|
||||
element.matches?.("textarea") ||
|
||||
element.matches?.("button") ||
|
||||
element.textContent?.includes("Manage memory")
|
||||
) {
|
||||
shouldRecheck = true
|
||||
|
|
@ -100,6 +133,7 @@ function setupClaudeRouteChangeDetection() {
|
|||
claudeObserverThrottle = setTimeout(() => {
|
||||
try {
|
||||
claudeObserverThrottle = null
|
||||
debugClaude("DOM changed near composer, rechecking UI")
|
||||
addSupermemoryButtonToClaudeMemoryDialog()
|
||||
addSupermemoryIconToClaudeInput()
|
||||
setupClaudeAutoFetch()
|
||||
|
|
@ -125,39 +159,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}"]`,
|
||||
),
|
||||
)
|
||||
|
||||
targetContainers.forEach((container) => {
|
||||
if (container.hasAttribute("data-supermemory-icon-added")) {
|
||||
return
|
||||
if (existingMarkers.length > 1) {
|
||||
debugClaude("removed duplicate markers", existingMarkers.length)
|
||||
for (const marker of existingMarkers) {
|
||||
marker.remove()
|
||||
}
|
||||
} else if (existingMarkers.length === 1) {
|
||||
debugClaude("marker already exists")
|
||||
return
|
||||
}
|
||||
|
||||
const existingIcon = container.querySelector(
|
||||
`#${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}`,
|
||||
)
|
||||
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)}`
|
||||
|
||||
container.setAttribute("data-supermemory-icon-added", "true")
|
||||
|
||||
container.insertBefore(supermemoryIcon, container.firstChild)
|
||||
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 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
|
|
@ -194,10 +396,12 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
|
|||
}
|
||||
}
|
||||
|
||||
console.log("Claude query extracted:", userQuery)
|
||||
debugClaude("query extracted", {
|
||||
queryLength: userQuery.length,
|
||||
})
|
||||
|
||||
if (!userQuery.trim()) {
|
||||
console.log("No query text found for Claude")
|
||||
debugClaude("memory search skipped because query is empty")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -210,7 +414,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(
|
||||
|
|
@ -228,7 +440,9 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
|
|||
timeoutPromise,
|
||||
])
|
||||
|
||||
console.log("Claude memories response:", response)
|
||||
debugClaude("memory search response", {
|
||||
success: response?.success,
|
||||
})
|
||||
|
||||
if (response?.success && response?.data) {
|
||||
const textareaElement = document.querySelector(
|
||||
|
|
@ -236,24 +450,39 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
|
|||
) as HTMLElement
|
||||
|
||||
if (textareaElement) {
|
||||
textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}`
|
||||
console.log(
|
||||
"Text element dataset:",
|
||||
textareaElement.dataset.supermemories,
|
||||
const memoryText = showMemorySuggestion(
|
||||
"claude",
|
||||
textareaElement,
|
||||
response.data,
|
||||
)
|
||||
debugClaude("memory suggestion rendered", {
|
||||
memoryLength: memoryText.length,
|
||||
})
|
||||
|
||||
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)
|
||||
|
|
@ -262,7 +491,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)
|
||||
|
|
@ -441,7 +676,9 @@ async function saveClaudeMemoriesToSupermemory(memoryDialog: HTMLElement) {
|
|||
actionSource: "claude_memories_dialog",
|
||||
})
|
||||
|
||||
console.log({ response })
|
||||
debugClaude("memory dialog saved", {
|
||||
success: response.success,
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
DOMUtils.showToast("success")
|
||||
|
|
@ -459,220 +696,27 @@ function updateClaudeIconFeedback(
|
|||
iconElement: HTMLElement,
|
||||
resetAfter = 0,
|
||||
) {
|
||||
if (!iconElement.dataset.originalHtml) {
|
||||
iconElement.dataset.originalHtml = iconElement.innerHTML
|
||||
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
|
||||
}
|
||||
|
||||
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 = `
|
||||
<span>✓</span>
|
||||
<span>${message}</span>
|
||||
`
|
||||
|
||||
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 = `
|
||||
<span style="font-weight: 600; color: #fff;">Included Memories</span>
|
||||
`
|
||||
|
||||
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 = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`
|
||||
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)
|
||||
if (message.toLowerCase().includes("searching")) {
|
||||
setMemoryMarkerStatus(iconElement, "searching")
|
||||
showMarkerPopover(iconElement, message)
|
||||
return
|
||||
}
|
||||
|
||||
iconElement.innerHTML = ""
|
||||
iconElement.appendChild(feedbackDiv)
|
||||
|
||||
if (resetAfter > 0) {
|
||||
setTimeout(() => {
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
|
||||
delete iconElement.dataset.originalHtml
|
||||
}, resetAfter)
|
||||
}
|
||||
setMemoryMarkerStatus(
|
||||
iconElement,
|
||||
message.toLowerCase().includes("error") ? "error" : "none",
|
||||
)
|
||||
showMarkerPopover(iconElement, message, undefined, fallbackReset)
|
||||
}
|
||||
|
||||
function setupClaudePromptCapture() {
|
||||
|
|
@ -684,7 +728,7 @@ function setupClaudePromptCapture() {
|
|||
const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false
|
||||
|
||||
if (!autoCapture) {
|
||||
console.log("Auto capture prompts is disabled, skipping prompt capture")
|
||||
debugClaude("auto prompt capture disabled")
|
||||
return
|
||||
}
|
||||
let promptContent = ""
|
||||
|
|
@ -704,19 +748,11 @@ 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)
|
||||
debugClaude("prompt submitted", {
|
||||
source,
|
||||
promptLength: promptContent.length,
|
||||
})
|
||||
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
|
|
@ -724,7 +760,7 @@ function setupClaudePromptCapture() {
|
|||
data: {
|
||||
prompt: promptContent,
|
||||
platform: "claude",
|
||||
source: source,
|
||||
source: window.location.href,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
|
|
@ -746,7 +782,7 @@ function setupClaudePromptCapture() {
|
|||
})
|
||||
|
||||
if (contentEditableDiv?.dataset.supermemories) {
|
||||
delete contentEditableDiv.dataset.supermemories
|
||||
clearMemorySuggestion("claude", contentEditableDiv)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -754,14 +790,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 (target.closest('[data-supermemory-connected-indicator="true"]')) {
|
||||
return
|
||||
}
|
||||
|
||||
if (sendButton) {
|
||||
const sendButton = target.closest("button")
|
||||
|
||||
if (
|
||||
sendButton &&
|
||||
buttonLabel(sendButton as HTMLButtonElement).match(/send|submit/i)
|
||||
) {
|
||||
await captureClaudePromptContent("button click")
|
||||
}
|
||||
},
|
||||
|
|
@ -773,10 +811,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
|
||||
|
|
@ -808,12 +854,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(
|
||||
|
|
@ -826,6 +887,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
|
||||
|
|
@ -834,7 +896,7 @@ async function setupClaudeAutoFetch() {
|
|||
})
|
||||
|
||||
if (textareaElement.dataset.supermemories) {
|
||||
delete textareaElement.dataset.supermemories
|
||||
clearMemorySuggestion("claude", textareaElement)
|
||||
}
|
||||
}
|
||||
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
|
||||
|
|
|
|||
661
apps/browser-extension/entrypoints/content/gemini.ts
Normal file
661
apps/browser-extension/entrypoints/content/gemini.ts
Normal file
|
|
@ -0,0 +1,661 @@
|
|||
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 = false
|
||||
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)
|
||||
for (const marker of existingMarkers) {
|
||||
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 || ""
|
||||
}
|
||||
|
||||
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) {
|
||||
debugGemini("auto prompt capture disabled")
|
||||
return
|
||||
}
|
||||
|
||||
const input = getGeminiPromptInput()
|
||||
const 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: window.location.href,
|
||||
},
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { DOMUtils } from "../../utils/ui-components"
|
|||
import { initializeChatGPT } from "./chatgpt"
|
||||
import { initializeClaude } from "./claude"
|
||||
import { initializeGrok } from "./grok"
|
||||
import { initializeGemini } from "./gemini"
|
||||
import {
|
||||
saveMemory,
|
||||
setupGlobalKeyboardShortcut,
|
||||
|
|
@ -20,13 +21,13 @@ export default defineContentScript({
|
|||
matches: ["<all_urls>"],
|
||||
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) {
|
||||
|
|
@ -52,6 +53,9 @@ export default defineContentScript({
|
|||
if (DOMUtils.isOnDomain(DOMAINS.GROK)) {
|
||||
initializeGrok()
|
||||
}
|
||||
if (DOMUtils.isOnDomain(DOMAINS.GEMINI)) {
|
||||
initializeGemini()
|
||||
}
|
||||
if (DOMUtils.isOnDomain(DOMAINS.T3)) {
|
||||
initializeT3()
|
||||
}
|
||||
|
|
@ -70,6 +74,7 @@ export default defineContentScript({
|
|||
initializeChatGPT()
|
||||
initializeClaude()
|
||||
initializeGrok()
|
||||
initializeGemini()
|
||||
initializeT3()
|
||||
initializeTwitter()
|
||||
|
||||
|
|
|
|||
409
apps/browser-extension/entrypoints/content/memory-suggestion.ts
Normal file
409
apps/browser-extension/entrypoints/content/memory-suggestion.ts
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
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) {
|
||||
const elements = document.querySelectorAll(
|
||||
`[${SUGGESTION_ATTR}="${platform}"]`,
|
||||
)
|
||||
for (const element of elements) {
|
||||
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" }),
|
||||
)
|
||||
}
|
||||
|
|
@ -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<APIResponse> {
|
||||
try {
|
||||
DOMUtils.showToast("loading")
|
||||
|
||||
|
|
@ -64,21 +67,28 @@ 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) {
|
||||
if (response?.success) {
|
||||
DOMUtils.showToast("success")
|
||||
} else {
|
||||
DOMUtils.showToast("error")
|
||||
return response
|
||||
}
|
||||
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 +100,7 @@ export function setupGlobalKeyboardShortcut() {
|
|||
event.key === "m"
|
||||
) {
|
||||
event.preventDefault()
|
||||
await saveMemory()
|
||||
await saveMemory("keyboard_shortcut")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -110,9 +120,6 @@ export function setupStorageListener() {
|
|||
window.location.hostname === "app.supermemory.ai"
|
||||
)
|
||||
) {
|
||||
console.log(
|
||||
"Bearer token and user data is only allowed to be used on localhost or supermemory.ai",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ export function initializeT3() {
|
|||
}
|
||||
|
||||
setTimeout(() => {
|
||||
console.log("Adding supermemory icon to T3 input")
|
||||
addSupermemoryIconToT3Input()
|
||||
setupT3AutoFetch()
|
||||
}, 2000)
|
||||
|
|
@ -55,7 +54,6 @@ function setupT3RouteChangeDetection() {
|
|||
const checkForRouteChange = () => {
|
||||
if (window.location.href !== currentUrl) {
|
||||
currentUrl = window.location.href
|
||||
console.log("T3 route changed, re-adding supermemory icon")
|
||||
setTimeout(() => {
|
||||
addSupermemoryIconToT3Input()
|
||||
setupT3AutoFetch()
|
||||
|
|
@ -183,10 +181,7 @@ async function getRelatedMemoriesForT3(actionSource: string) {
|
|||
}
|
||||
}
|
||||
|
||||
console.log("T3 query extracted:", userQuery)
|
||||
|
||||
if (!userQuery.trim()) {
|
||||
console.log("No query text found for T3")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -217,8 +212,6 @@ async function getRelatedMemoriesForT3(actionSource: string) {
|
|||
timeoutPromise,
|
||||
])
|
||||
|
||||
console.log("T3 memories response:", response)
|
||||
|
||||
if (response?.success && response?.data) {
|
||||
let textareaElement = null
|
||||
const supermemoryContainer = document.querySelector(
|
||||
|
|
@ -337,12 +330,10 @@ function updateT3IconFeedback(
|
|||
`
|
||||
|
||||
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")
|
||||
|
|
@ -493,11 +484,10 @@ function setupT3PromptCapture() {
|
|||
}
|
||||
document.body.setAttribute("data-t3-prompt-capture-setup", "true")
|
||||
|
||||
const captureT3PromptContent = async (source: string) => {
|
||||
const captureT3PromptContent = async (_source: string) => {
|
||||
const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false
|
||||
|
||||
if (!autoCapture) {
|
||||
console.log("Auto capture prompts is disabled, skipping prompt capture")
|
||||
return
|
||||
}
|
||||
let promptContent = ""
|
||||
|
|
@ -538,15 +528,13 @@ function setupT3PromptCapture() {
|
|||
}
|
||||
|
||||
if (promptContent.trim()) {
|
||||
console.log(`T3 prompt submitted via ${source}:`, promptContent)
|
||||
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.CAPTURE_PROMPT,
|
||||
data: {
|
||||
prompt: promptContent,
|
||||
platform: "t3",
|
||||
source: source,
|
||||
source: window.location.href,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -253,6 +258,7 @@ function App() {
|
|||
const [autoCapturePromptsEnabled, setAutoCapturePromptsEnabled] =
|
||||
useState<boolean>(false)
|
||||
const [authInvalidated, setAuthInvalidated] = useState<boolean>(false)
|
||||
const [saveError, setSaveError] = useState<string | null>(null)
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const { data: projects = [], isLoading: loadingProjects } = useProjects({
|
||||
|
|
@ -375,29 +381,70 @@ 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, {
|
||||
action: MESSAGE_TYPES.SAVE_MEMORY,
|
||||
actionSource: "popup",
|
||||
})
|
||||
const tab = tabs[0]
|
||||
let response: { success?: boolean; error?: string } | undefined
|
||||
|
||||
if (response?.success) {
|
||||
await chrome.tabs.sendMessage(tabs[0].id, {
|
||||
action: MESSAGE_TYPES.SHOW_TOAST,
|
||||
state: "success",
|
||||
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_fallback",
|
||||
data: {
|
||||
url: fallbackUrl,
|
||||
title: fallbackTitle,
|
||||
content: `${fallbackTitle}\n\n${fallbackUrl}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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({
|
||||
|
|
@ -413,8 +460,6 @@ function App() {
|
|||
} catch (toastError) {
|
||||
console.error("Failed to show error toast:", toastError)
|
||||
}
|
||||
|
||||
window.close()
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
|
|
@ -592,7 +637,7 @@ function App() {
|
|||
>
|
||||
<img
|
||||
alt="supermemory"
|
||||
src="./icon-48.png"
|
||||
src="./new_logo.png"
|
||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[29px] h-[29px]"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -600,11 +645,9 @@ function App() {
|
|||
<span className="text-[11px] font-medium text-[#737373] leading-normal">
|
||||
Your
|
||||
</span>
|
||||
<img
|
||||
alt="supermemory"
|
||||
src="./logo-fullmark.svg"
|
||||
className="h-[14.5px] w-auto"
|
||||
/>
|
||||
<span className="text-[15px] font-semibold leading-none text-white">
|
||||
supermemory
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -658,7 +701,7 @@ function App() {
|
|||
>
|
||||
<img
|
||||
alt="supermemory"
|
||||
src="./icon-48.png"
|
||||
src="./new_logo.png"
|
||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[29px] h-[29px]"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -672,11 +715,9 @@ function App() {
|
|||
return name.endsWith("s") ? `${name}'` : `${name}'s`
|
||||
})()}
|
||||
</span>
|
||||
<img
|
||||
alt="supermemory"
|
||||
src="./logo-fullmark.svg"
|
||||
className="h-[14.5px] w-auto"
|
||||
/>
|
||||
<span className="text-[15px] font-semibold leading-none text-white">
|
||||
supermemory
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{userSignedIn && (
|
||||
|
|
@ -931,6 +972,11 @@ function App() {
|
|||
|
||||
{saving ? "Saving..." : "Add to supermemory"}
|
||||
</button>
|
||||
{saveError && (
|
||||
<p className="mt-2 text-xs leading-snug text-red-300">
|
||||
{saveError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : activeTab === "imports" ? (
|
||||
|
|
@ -1269,13 +1315,13 @@ function App() {
|
|||
</h2>
|
||||
|
||||
<ul className="list-none p-0 m-0 text-left">
|
||||
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
|
||||
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['-'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
|
||||
Save any page to your supermemory
|
||||
</li>
|
||||
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
|
||||
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['-'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
|
||||
Import all your Twitter / X Bookmarks
|
||||
</li>
|
||||
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
|
||||
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['-'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
|
||||
Import your ChatGPT Memories
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -1300,9 +1346,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"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Default Popup Title</title>
|
||||
<title>supermemory</title>
|
||||
<meta name="manifest.type" content="browser_action" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -1,105 +1,114 @@
|
|||
import { getSupermemoryLoginUrl } from "../../utils/constants"
|
||||
|
||||
const featureCards = [
|
||||
{
|
||||
number: "01",
|
||||
title: "Save any page",
|
||||
description: "Articles, docs, and references from the browser.",
|
||||
},
|
||||
{
|
||||
number: "02",
|
||||
title: "Import X bookmarks",
|
||||
description: "Bring saved posts into your memory library.",
|
||||
},
|
||||
{
|
||||
number: "03",
|
||||
title: "Capture AI chats",
|
||||
description: "Save useful conversations from ChatGPT, Claude, and Gemini.",
|
||||
},
|
||||
{
|
||||
number: "04",
|
||||
title: "Use context anywhere",
|
||||
description: "Search and reuse memories when you need them.",
|
||||
},
|
||||
]
|
||||
|
||||
function Welcome() {
|
||||
return (
|
||||
<div className="min-h-screen font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif] flex items-center justify-center p-8 bg-gradient-to-br from-gray-50 to-white">
|
||||
<div className="max-w-4xl w-full text-center">
|
||||
{/* Header */}
|
||||
<div className="mb-12">
|
||||
<img
|
||||
alt="supermemory"
|
||||
className="h-16 mb-6 mx-auto"
|
||||
src="https://assets.supermemory.ai/brand/wordmark/dark-transparent.svg"
|
||||
/>
|
||||
<p className="text-gray-600 text-lg font-normal max-w-2xl mx-auto">
|
||||
Your AI second brain for saving and organizing everything that
|
||||
matters. Supermemory learns and remembers everything you save, your
|
||||
preferences, and understands you.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative min-h-screen overflow-hidden bg-[#05080D] text-white font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif]">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(180deg, #05080D 0%, #05070A 48%, #060A18 100%)",
|
||||
}}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(105,167,240,0.20)_1px,transparent_1px)] bg-size-[32px_32px] opacity-70 mask-[linear-gradient(to_bottom,transparent_0%,black_12%,black_100%)]" />
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-[55%] bg-[radial-gradient(ellipse_at_bottom,rgba(20,65,255,0.42),transparent_68%)]" />
|
||||
|
||||
{/* Features Section */}
|
||||
<div className="mb-12">
|
||||
<h2 className="text-2xl font-semibold text-black mb-8">
|
||||
What can you do with supermemory ?
|
||||
</h2>
|
||||
<main className="relative mx-auto flex min-h-screen w-full max-w-6xl flex-col px-6 py-6 sm:px-10">
|
||||
<header className="flex items-center border-b border-white/10 pb-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<img alt="" className="size-8 rounded-[4px]" src="./new_logo.png" />
|
||||
<span className="text-lg font-semibold leading-none text-white">
|
||||
supermemory
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 text-center transition-all duration-200 shadow-sm hover:-translate-y-0.5 hover:shadow-md hover:border-gray-300">
|
||||
<div className="text-3xl mb-4 block">💾</div>
|
||||
<h3 className="text-lg font-semibold text-black mb-3">
|
||||
Save Any Page
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 leading-snug">
|
||||
Instantly save web pages, articles, and content to your personal
|
||||
knowledge base
|
||||
</p>
|
||||
</div>
|
||||
<section className="flex flex-1 flex-col items-center justify-center py-10 text-center">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<h1 className="text-4xl font-semibold leading-[1.05] tracking-normal text-white sm:text-6xl">
|
||||
Your browser now has{" "}
|
||||
<span className="text-[#369BFD]">supermemory.</span>
|
||||
</h1>
|
||||
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 text-center transition-all duration-200 shadow-sm hover:-translate-y-0.5 hover:shadow-md hover:border-gray-300">
|
||||
<div className="text-3xl mb-4 block">🐦</div>
|
||||
<h3 className="text-lg font-semibold text-black mb-3">
|
||||
Import Twitter/X Bookmarks
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 leading-snug">
|
||||
Bring all your saved tweets and bookmarks into one organized
|
||||
place
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 text-center transition-all duration-200 shadow-sm hover:-translate-y-0.5 hover:shadow-md hover:border-gray-300">
|
||||
<div className="text-3xl mb-4 block">🤖</div>
|
||||
<h3 className="text-lg font-semibold text-black mb-3">
|
||||
Import ChatGPT Memories
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 leading-snug">
|
||||
Keep your important AI conversations and insights accessible
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 text-center transition-all duration-200 shadow-sm hover:-translate-y-0.5 hover:shadow-md hover:border-gray-300">
|
||||
<div className="text-3xl mb-4 block">🔍</div>
|
||||
<h3 className="text-lg font-semibold text-black mb-3">
|
||||
Your context, everywhere.
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 leading-snug">
|
||||
You can connect chatbots with MCP, chat with your personal
|
||||
assistant, and more.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-col justify-center gap-3 sm:flex-row">
|
||||
<button
|
||||
className="h-12 rounded-xl px-7 text-sm font-semibold text-white transition hover:brightness-110 focus:outline-none focus:ring-2 focus:ring-[#36fdfd]/70"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%)",
|
||||
boxShadow:
|
||||
"1px 1px 2px 0px #1A88FF inset, 0 2px 18px 0 rgba(54, 155, 253, 0.24)",
|
||||
}}
|
||||
onClick={() => {
|
||||
chrome.tabs.create({
|
||||
url: getSupermemoryLoginUrl(),
|
||||
})
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Sign in to connect
|
||||
</button>
|
||||
<button
|
||||
className="h-12 rounded-xl border border-[#369BFD]/25 bg-[#080B0F]/80 px-6 text-sm font-semibold text-[#C7D7F2] transition hover:border-[#369BFD]/50 hover:bg-[#0D121A] focus:outline-none focus:ring-2 focus:ring-[#369BFD]/30"
|
||||
onClick={() => {
|
||||
chrome.tabs.create({
|
||||
url: "https://supermemory.ai",
|
||||
})
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Open supermemory.ai
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mb-8">
|
||||
<button
|
||||
className="min-w-[200px] px-8 py-4 bg-gray-700 text-white border-none rounded-3xl text-base font-semibold cursor-pointer transition-colors duration-200 mb-4 outline-none hover:bg-gray-800 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
onClick={() => {
|
||||
chrome.tabs.create({
|
||||
url: import.meta.env.PROD
|
||||
? "https://app.supermemory.ai/login"
|
||||
: "http://localhost:3000/login",
|
||||
})
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Login to Get started
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-14 grid w-full max-w-5xl gap-3 text-left sm:grid-cols-2 lg:grid-cols-4">
|
||||
{featureCards.map((feature) => (
|
||||
<div
|
||||
className="rounded-lg border border-white/10 bg-white/[0.035] p-4"
|
||||
key={feature.number}
|
||||
>
|
||||
<p className="text-[11px] font-medium text-[#737373]">
|
||||
{feature.number}
|
||||
</p>
|
||||
<h2 className="mt-4 text-sm font-semibold text-white">
|
||||
{feature.title}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-[#A1A1AA]">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t border-gray-200 pt-6 mt-8">
|
||||
<p className="text-sm text-gray-600">
|
||||
Learn more at{" "}
|
||||
<a
|
||||
className="text-blue-500 no-underline hover:underline hover:text-blue-700"
|
||||
href="https://supermemory.ai"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
supermemory.ai
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<footer className="border-t border-white/10 py-5 text-xs text-[#737373]">
|
||||
supermemory stores your extension session locally in Chrome.
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon-16.png" />
|
||||
<link rel="icon" type="image/png" href="/new_logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Welcome to supermemory</title>
|
||||
</head>
|
||||
|
|
@ -10,4 +10,4 @@
|
|||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
|||
BIN
apps/browser-extension/public/new_logo.png
Normal file
BIN
apps/browser-extension/public/new_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
|
|
@ -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
|
||||
|
|
@ -59,6 +71,7 @@ export const DOMAINS = {
|
|||
CHATGPT: ["chatgpt.com", "chat.openai.com"],
|
||||
CLAUDE: ["claude.ai"],
|
||||
GROK: ["grok.com", "x.ai"],
|
||||
GEMINI: ["gemini.google.com"],
|
||||
T3: ["t3.chat"],
|
||||
SUPERMEMORY: ["localhost", "supermemory.ai", "app.supermemory.ai"],
|
||||
} as const
|
||||
|
|
@ -95,6 +108,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
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ export function createRouteDetection(
|
|||
const checkForRouteChange = () => {
|
||||
if (window.location.href !== currentUrl) {
|
||||
currentUrl = window.location.href
|
||||
console.log(`${config.platform} route changed, re-initializing`)
|
||||
setTimeout(config.reinitCallback, 1000)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ export async function captureTwitterTokens(
|
|||
if (authHeader?.value && cookieHeader?.value && csrfHeader?.value) {
|
||||
const tokensAlreadyLogged = await getTokensLogged()
|
||||
if (!tokensAlreadyLogged) {
|
||||
console.log("Twitter auth tokens captured successfully")
|
||||
await setTokensLogged()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -186,8 +186,6 @@ export class TwitterImporter {
|
|||
if (documents.length > 0) {
|
||||
await saveAllTweets(documents)
|
||||
}
|
||||
console.log("Tweets saved")
|
||||
console.log("Documents:", documents)
|
||||
} catch (error) {
|
||||
console.error("Error saving tweets batch:", error)
|
||||
await this.config.onError(error as Error)
|
||||
|
|
@ -201,9 +199,6 @@ export class TwitterImporter {
|
|||
[]
|
||||
const nextCursor = extractNextCursor(instructions)
|
||||
|
||||
console.log("Next cursor:", nextCursor)
|
||||
console.log("Tweets length:", tweets.length)
|
||||
|
||||
if (nextCursor && tweets.length > 0 && !this.config.isFolderImport) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)) // Rate limiting
|
||||
await this.batchImportAll(nextCursor, importedCount, uniqueGroupId)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ export interface MemoryData {
|
|||
url?: string
|
||||
ogImage?: string
|
||||
title?: string
|
||||
sourcePlatform?: string
|
||||
sourcePlatformLabel?: string
|
||||
sourceSurface?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export function createToast(state: ToastState): HTMLElement {
|
|||
break
|
||||
|
||||
case "success": {
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png")
|
||||
const iconUrl = browser.runtime.getURL("/new_logo.png")
|
||||
icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`
|
||||
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 = `
|
||||
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
|
||||
|
|
@ -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 iconFileName = "/new_logo.png"
|
||||
const iconUrl = browser.runtime.getURL(iconFileName)
|
||||
iconButton.innerHTML = `
|
||||
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 50%;" />
|
||||
<img src="${iconUrl}" width="20" height="20" alt="" style="border-radius: 5px; display: block;" />
|
||||
`
|
||||
|
||||
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;
|
||||
`
|
||||
return createConnectedIndicator(onClick)
|
||||
}
|
||||
|
||||
const iconFileName = "/icon-16.png"
|
||||
const iconUrl = browser.runtime.getURL(iconFileName)
|
||||
iconButton.innerHTML = `
|
||||
<img src="${iconUrl}" width="20" height="20" alt="Get Related Memories from supermemory" style="border-radius: 4px;" />
|
||||
`
|
||||
|
||||
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 iconButton
|
||||
export function createGeminiInputBarElement(onClick: () => void): HTMLElement {
|
||||
return createConnectedIndicator(onClick)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -360,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 = `
|
||||
<img src="${iconUrl}" width="20" height="20" alt="Get Related Memories from supermemory" style="border-radius: 4px;" />
|
||||
|
|
@ -433,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 = `
|
||||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||
<h3 style="margin: 0; font-size: 16px; font-weight: 600; color: #ffffff; display: flex; align-items: center; gap: 8px;">
|
||||
|
|
@ -702,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 = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`
|
||||
icon.style.animation = ""
|
||||
text.textContent = "Added to Memory"
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default defineConfig({
|
|||
manifest: {
|
||||
name: "supermemory",
|
||||
homepage_url: "https://supermemory.ai",
|
||||
version: "6.1.4",
|
||||
version: "6.1.3",
|
||||
permissions: ["storage", "activeTab", "webRequest", "tabs"],
|
||||
host_permissions: [
|
||||
"*://x.com/*",
|
||||
|
|
@ -42,11 +42,14 @@ export default defineConfig({
|
|||
"*://*.grok.com/*",
|
||||
"*://x.ai/*",
|
||||
"*://*.x.ai/*",
|
||||
"*://claude.ai/*",
|
||||
"*://gemini.google.com/*",
|
||||
"*://t3.chat/*",
|
||||
"https://*.posthog.com/*",
|
||||
],
|
||||
web_accessible_resources: [
|
||||
{
|
||||
resources: ["icon-16.png", "fonts/*.ttf"],
|
||||
resources: ["new_logo.png", "fonts/*.ttf"],
|
||||
matches: ["<all_urls>"],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -101,6 +101,34 @@ type OgData = {
|
|||
image?: string
|
||||
}
|
||||
|
||||
const EXTENSION_PLATFORM_LABELS: Record<string, string> = {
|
||||
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<string, OgData>()
|
||||
const ogInflight = new Map<string, Promise<OgData | null>>()
|
||||
const ogFailures = new Map<string, number>()
|
||||
|
|
@ -1229,6 +1257,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", {
|
||||
|
|
@ -1236,7 +1265,9 @@ const DocumentCard = memo(
|
|||
day: "numeric",
|
||||
year: "numeric",
|
||||
})
|
||||
return badge ? `${badge} · ${date}` : date
|
||||
return [sourceLabel, badge, date]
|
||||
.filter(Boolean)
|
||||
.join(" - ")
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue