mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-05 08:06:19 +00:00
chrome extension redesign
This commit is contained in:
parent
a8b24ee19c
commit
065316e68b
6 changed files with 728 additions and 10 deletions
|
|
@ -32,6 +32,12 @@ export default defineBackground(() => {
|
|||
contexts: ["selection", "page", "link"],
|
||||
})
|
||||
|
||||
browser.contextMenus.create({
|
||||
id: CONTEXT_MENU_IDS.SEARCH_SUPERMEMORY,
|
||||
title: "search supermemory",
|
||||
contexts: ["selection"],
|
||||
})
|
||||
|
||||
if (details.reason === "install") {
|
||||
await trackEvent("extension_installed", {
|
||||
reason: details.reason,
|
||||
|
|
@ -67,6 +73,22 @@ export default defineBackground(() => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (info.menuItemId === CONTEXT_MENU_IDS.SEARCH_SUPERMEMORY) {
|
||||
if (tab?.id && info.selectionText) {
|
||||
try {
|
||||
await browser.tabs.sendMessage(tab.id, {
|
||||
action: MESSAGE_TYPES.OPEN_SEARCH_PANEL,
|
||||
data: info.selectionText,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to send search message to content script:",
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Send message to current active tab.
|
||||
|
|
@ -296,6 +318,31 @@ export default defineBackground(() => {
|
|||
})()
|
||||
return true
|
||||
}
|
||||
|
||||
if (message.action === MESSAGE_TYPES.SEARCH_SELECTION) {
|
||||
;(async () => {
|
||||
try {
|
||||
const query = message.data as string
|
||||
const responseData = await searchMemories(query)
|
||||
await trackEvent(POSTHOG_EVENT_KEY.SELECTION_SEARCH_TRIGGERED, {
|
||||
query_length: query.length,
|
||||
})
|
||||
sendResponse({ success: true, data: responseData })
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
const isAuthError =
|
||||
errorMessage.includes("Authentication") ||
|
||||
errorMessage.includes("token")
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
isAuthError,
|
||||
})
|
||||
}
|
||||
})()
|
||||
return true
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,9 +2,21 @@ import { DOMAINS, MESSAGE_TYPES } from "../../utils/constants"
|
|||
import { DOMUtils } from "../../utils/ui-components"
|
||||
import { initializeChatGPT } from "./chatgpt"
|
||||
import { initializeClaude } from "./claude"
|
||||
import { saveMemory, setupGlobalKeyboardShortcut, setupStorageListener } from "./shared"
|
||||
import {
|
||||
handleOpenSearchPanel,
|
||||
initializeSelectionSearch,
|
||||
} from "./selection-search"
|
||||
import {
|
||||
saveMemory,
|
||||
setupGlobalKeyboardShortcut,
|
||||
setupStorageListener,
|
||||
} from "./shared"
|
||||
import { initializeT3 } from "./t3"
|
||||
import { handleTwitterNavigation, initializeTwitter, updateTwitterImportUI } from "./twitter"
|
||||
import {
|
||||
handleTwitterNavigation,
|
||||
initializeTwitter,
|
||||
updateTwitterImportUI,
|
||||
} from "./twitter"
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["<all_urls>"],
|
||||
|
|
@ -15,6 +27,8 @@ export default defineContentScript({
|
|||
DOMUtils.showToast(message.state)
|
||||
} else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
|
||||
await saveMemory()
|
||||
} else if (message.action === MESSAGE_TYPES.OPEN_SEARCH_PANEL) {
|
||||
handleOpenSearchPanel(message.data as string)
|
||||
} else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
|
||||
updateTwitterImportUI(message)
|
||||
} else if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
|
||||
|
|
@ -57,6 +71,9 @@ export default defineContentScript({
|
|||
initializeT3()
|
||||
initializeTwitter()
|
||||
|
||||
// Initialize universal selection search
|
||||
initializeSelectionSearch()
|
||||
|
||||
// Start observing for dynamic changes
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", observeForDynamicChanges)
|
||||
|
|
@ -64,4 +81,4 @@ export default defineContentScript({
|
|||
observeForDynamicChanges()
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
647
apps/browser-extension/entrypoints/content/selection-search.ts
Normal file
647
apps/browser-extension/entrypoints/content/selection-search.ts
Normal file
|
|
@ -0,0 +1,647 @@
|
|||
import { ELEMENT_IDS, MESSAGE_TYPES, UI_CONFIG } from "../../utils/constants"
|
||||
|
||||
// State
|
||||
let currentQuery = ""
|
||||
let fabElement: HTMLElement | null = null
|
||||
let panelElement: HTMLElement | null = null
|
||||
let selectedResults: Set<number> = new Set()
|
||||
|
||||
/**
|
||||
* Get the selection rectangle for positioning the FAB
|
||||
*/
|
||||
function getSelectionRect(): DOMRect | null {
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.rangeCount === 0) return null
|
||||
|
||||
const range = selection.getRangeAt(0)
|
||||
return range.getBoundingClientRect()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the selection is inside our extension UI
|
||||
*/
|
||||
function isSelectionInsideExtensionUI(): boolean {
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.rangeCount === 0) return false
|
||||
|
||||
const anchorNode = selection.anchorNode
|
||||
if (!anchorNode) return false
|
||||
|
||||
const element =
|
||||
anchorNode.nodeType === Node.ELEMENT_NODE
|
||||
? (anchorNode as Element)
|
||||
: anchorNode.parentElement
|
||||
|
||||
if (!element) return false
|
||||
|
||||
// Check if selection is inside FAB or panel
|
||||
return (
|
||||
!!element.closest(`#${ELEMENT_IDS.SELECTION_SEARCH_FAB}`) ||
|
||||
!!element.closest(`#${ELEMENT_IDS.SELECTION_SEARCH_PANEL}`)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the floating action button (FAB)
|
||||
*/
|
||||
function createFAB(): HTMLElement {
|
||||
const fab = document.createElement("div")
|
||||
fab.id = ELEMENT_IDS.SELECTION_SEARCH_FAB
|
||||
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png")
|
||||
|
||||
fab.innerHTML = `
|
||||
<img src="${iconUrl}" width="16" height="16" alt="Search" style="border-radius: 2px;" />
|
||||
<span>Search</span>
|
||||
`
|
||||
|
||||
fab.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 2147483646;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: #05070A;
|
||||
color: #ffffff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.15s ease;
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
fab.addEventListener("mouseenter", () => {
|
||||
fab.style.background = "#0F151F"
|
||||
fab.style.borderColor = "rgba(255, 255, 255, 0.2)"
|
||||
})
|
||||
|
||||
fab.addEventListener("mouseleave", () => {
|
||||
fab.style.background = "#05070A"
|
||||
fab.style.borderColor = "rgba(255, 255, 255, 0.1)"
|
||||
})
|
||||
|
||||
fab.addEventListener("click", (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
triggerSearch()
|
||||
})
|
||||
|
||||
return fab
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the FAB near the selection
|
||||
*/
|
||||
function showFAB(rect: DOMRect, text: string) {
|
||||
hideFAB()
|
||||
|
||||
currentQuery = text
|
||||
fabElement = createFAB()
|
||||
|
||||
// Position FAB above the selection, centered
|
||||
const fabWidth = 90 // approximate width
|
||||
let left = rect.left + rect.width / 2 - fabWidth / 2
|
||||
let top = rect.top - 40
|
||||
|
||||
// Ensure FAB stays within viewport
|
||||
if (left < 10) left = 10
|
||||
if (left + fabWidth > window.innerWidth - 10) {
|
||||
left = window.innerWidth - fabWidth - 10
|
||||
}
|
||||
if (top < 10) {
|
||||
// Show below selection if not enough space above
|
||||
top = rect.bottom + 10
|
||||
}
|
||||
|
||||
fabElement.style.left = `${left}px`
|
||||
fabElement.style.top = `${top}px`
|
||||
|
||||
document.body.appendChild(fabElement)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the FAB
|
||||
*/
|
||||
export function hideFAB() {
|
||||
if (fabElement) {
|
||||
fabElement.remove()
|
||||
fabElement = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger search with the current query
|
||||
*/
|
||||
async function triggerSearch() {
|
||||
if (!currentQuery) return
|
||||
|
||||
hideFAB()
|
||||
showPanel(currentQuery, "loading")
|
||||
|
||||
try {
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.SEARCH_SELECTION,
|
||||
data: currentQuery,
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
showPanel(currentQuery, "results", response.data)
|
||||
} else if (response.isAuthError) {
|
||||
showPanel(currentQuery, "auth_error")
|
||||
} else {
|
||||
showPanel(currentQuery, "error", null, response.error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Search failed:", error)
|
||||
showPanel(
|
||||
currentQuery,
|
||||
"error",
|
||||
null,
|
||||
error instanceof Error ? error.message : "Search failed",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and show the search results panel
|
||||
*/
|
||||
function showPanel(
|
||||
query: string,
|
||||
state: "loading" | "results" | "error" | "auth_error",
|
||||
data?: unknown,
|
||||
errorMessage?: string,
|
||||
) {
|
||||
hidePanel()
|
||||
selectedResults.clear()
|
||||
|
||||
panelElement = document.createElement("div")
|
||||
panelElement.id = ELEMENT_IDS.SELECTION_SEARCH_PANEL
|
||||
|
||||
panelElement.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 2147483647;
|
||||
width: 420px;
|
||||
max-width: 90vw;
|
||||
max-height: 70vh;
|
||||
background: #05070A;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
// Header
|
||||
const header = document.createElement("div")
|
||||
header.style.cssText = `
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png")
|
||||
header.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<img src="${iconUrl}" width="20" height="20" alt="supermemory" style="border-radius: 4px;" />
|
||||
<span style="font-size: 14px; font-weight: 600; color: #ffffff;">Search Results</span>
|
||||
</div>
|
||||
<button id="sm-panel-close" style="
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #737373;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
transition: color 0.15s;
|
||||
">×</button>
|
||||
`
|
||||
|
||||
panelElement.appendChild(header)
|
||||
|
||||
// Query display
|
||||
const queryDisplay = document.createElement("div")
|
||||
queryDisplay.style.cssText = `
|
||||
padding: 12px 16px;
|
||||
background: rgba(91, 126, 245, 0.04);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
flex-shrink: 0;
|
||||
`
|
||||
queryDisplay.innerHTML = `
|
||||
<div style="font-size: 12px; color: #737373; margin-bottom: 4px;">Searching for:</div>
|
||||
<div style="font-size: 13px; color: #ffffff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${escapeHtml(query)}</div>
|
||||
`
|
||||
panelElement.appendChild(queryDisplay)
|
||||
|
||||
// Content area
|
||||
const content = document.createElement("div")
|
||||
content.id = "sm-panel-content"
|
||||
content.style.cssText = `
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
min-height: 150px;
|
||||
`
|
||||
|
||||
if (state === "loading") {
|
||||
content.innerHTML = `
|
||||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 150px; gap: 12px;">
|
||||
<div style="width: 24px; height: 24px; border: 2px solid rgba(255,255,255,0.1); border-top-color: #5BD3FB; border-radius: 50%; animation: sm-spin 0.8s linear infinite;"></div>
|
||||
<span style="font-size: 14px; color: #737373;">Searching memories...</span>
|
||||
</div>
|
||||
`
|
||||
} else if (state === "auth_error") {
|
||||
content.innerHTML = `
|
||||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 150px; gap: 16px; text-align: center;">
|
||||
<div style="font-size: 14px; color: #ef4444;">Sign in required</div>
|
||||
<p style="font-size: 13px; color: #737373; margin: 0;">Please sign in to supermemory to search your memories.</p>
|
||||
<button id="sm-sign-in-btn" style="
|
||||
padding: 10px 20px;
|
||||
background: linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
">Sign in</button>
|
||||
</div>
|
||||
`
|
||||
} else if (state === "error") {
|
||||
content.innerHTML = `
|
||||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 150px; gap: 12px; text-align: center;">
|
||||
<div style="color: #ef4444; font-size: 14px;">Search failed</div>
|
||||
<p style="font-size: 13px; color: #737373; margin: 0;">${escapeHtml(errorMessage || "An error occurred")}</p>
|
||||
</div>
|
||||
`
|
||||
} else if (state === "results") {
|
||||
const response = data as {
|
||||
searchResults?: { results?: Array<{ memory?: string; id?: string }> }
|
||||
}
|
||||
const results = response?.searchResults?.results || []
|
||||
|
||||
if (results.length === 0) {
|
||||
content.innerHTML = `
|
||||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 150px; gap: 12px; text-align: center;">
|
||||
<div style="font-size: 14px; color: #737373;">No memories found</div>
|
||||
<p style="font-size: 13px; color: #525966; margin: 0;">Try a different search query.</p>
|
||||
</div>
|
||||
`
|
||||
} else {
|
||||
content.innerHTML = results
|
||||
.map(
|
||||
(result, index) => `
|
||||
<div class="sm-result-item" data-index="${index}" style="
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: rgba(91, 126, 245, 0.04);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
border: 1px solid transparent;
|
||||
">
|
||||
<input type="checkbox" data-index="${index}" style="
|
||||
margin-top: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
accent-color: #5BD3FB;
|
||||
" />
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<p style="font-size: 13px; color: #e5e5e5; margin: 0; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">${escapeHtml(result.memory || "")}</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("")
|
||||
}
|
||||
}
|
||||
|
||||
panelElement.appendChild(content)
|
||||
|
||||
// Footer with copy button (only for results)
|
||||
if (state === "results") {
|
||||
const response = data as {
|
||||
searchResults?: { results?: Array<{ memory?: string }> }
|
||||
}
|
||||
const results = response?.searchResults?.results || []
|
||||
|
||||
if (results.length > 0) {
|
||||
const footer = document.createElement("div")
|
||||
footer.style.cssText = `
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
footer.innerHTML = `
|
||||
<span id="sm-selected-count" style="font-size: 13px; color: #737373;">0 selected</span>
|
||||
<button id="sm-copy-btn" style="
|
||||
padding: 10px 16px;
|
||||
background: linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.15s;
|
||||
" disabled>Copy selected</button>
|
||||
`
|
||||
panelElement.appendChild(footer)
|
||||
}
|
||||
}
|
||||
|
||||
// Add animations style
|
||||
if (!document.getElementById("sm-panel-styles")) {
|
||||
const style = document.createElement("style")
|
||||
style.id = "sm-panel-styles"
|
||||
style.textContent = `
|
||||
@keyframes sm-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.sm-result-item:hover {
|
||||
background: rgba(91, 126, 245, 0.08) !important;
|
||||
}
|
||||
.sm-result-item.selected {
|
||||
border-color: rgba(91, 190, 251, 0.3) !important;
|
||||
background: rgba(91, 126, 245, 0.08) !important;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
|
||||
document.body.appendChild(panelElement)
|
||||
|
||||
// Event listeners
|
||||
setupPanelEventListeners(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners for the panel
|
||||
*/
|
||||
function setupPanelEventListeners(data: unknown) {
|
||||
if (!panelElement) return
|
||||
|
||||
// Close button
|
||||
const closeBtn = panelElement.querySelector("#sm-panel-close")
|
||||
closeBtn?.addEventListener("click", hidePanel)
|
||||
|
||||
// Sign in button
|
||||
const signInBtn = panelElement.querySelector("#sm-sign-in-btn")
|
||||
signInBtn?.addEventListener("click", () => {
|
||||
window.open(
|
||||
import.meta.env.PROD
|
||||
? "https://app.supermemory.ai/login"
|
||||
: "http://localhost:3000/login",
|
||||
"_blank",
|
||||
)
|
||||
})
|
||||
|
||||
// Result item checkboxes
|
||||
const checkboxes = panelElement.querySelectorAll(
|
||||
'.sm-result-item input[type="checkbox"]',
|
||||
)
|
||||
checkboxes.forEach((checkbox) => {
|
||||
checkbox.addEventListener("change", (e) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
const index = Number.parseInt(target.dataset.index || "0", 10)
|
||||
const item = target.closest(".sm-result-item") as HTMLElement
|
||||
|
||||
if (target.checked) {
|
||||
selectedResults.add(index)
|
||||
item?.classList.add("selected")
|
||||
} else {
|
||||
selectedResults.delete(index)
|
||||
item?.classList.remove("selected")
|
||||
}
|
||||
|
||||
updateSelectionUI()
|
||||
})
|
||||
})
|
||||
|
||||
// Result item click (toggle checkbox)
|
||||
const items = panelElement.querySelectorAll(".sm-result-item")
|
||||
items.forEach((item) => {
|
||||
item.addEventListener("click", (e) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.tagName === "INPUT") return
|
||||
|
||||
const checkbox = item.querySelector(
|
||||
'input[type="checkbox"]',
|
||||
) as HTMLInputElement
|
||||
if (checkbox) {
|
||||
checkbox.checked = !checkbox.checked
|
||||
checkbox.dispatchEvent(new Event("change"))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Copy button
|
||||
const copyBtn = panelElement.querySelector("#sm-copy-btn")
|
||||
copyBtn?.addEventListener("click", () => {
|
||||
copySelectedResults(data)
|
||||
})
|
||||
|
||||
// Close on escape
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
hidePanel()
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
|
||||
// Close on outside click
|
||||
const handleOutsideClick = (e: MouseEvent) => {
|
||||
if (panelElement && !panelElement.contains(e.target as Node)) {
|
||||
hidePanel()
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
document.addEventListener("click", handleOutsideClick)
|
||||
}, 100)
|
||||
|
||||
// Cleanup listeners when panel is removed
|
||||
const observer = new MutationObserver(() => {
|
||||
if (!document.contains(panelElement)) {
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
document.removeEventListener("click", handleOutsideClick)
|
||||
observer.disconnect()
|
||||
}
|
||||
})
|
||||
observer.observe(document.body, { childList: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the selection count UI
|
||||
*/
|
||||
function updateSelectionUI() {
|
||||
const countEl = document.getElementById("sm-selected-count")
|
||||
const copyBtn = document.getElementById("sm-copy-btn") as HTMLButtonElement
|
||||
|
||||
if (countEl) {
|
||||
countEl.textContent = `${selectedResults.size} selected`
|
||||
}
|
||||
|
||||
if (copyBtn) {
|
||||
copyBtn.disabled = selectedResults.size === 0
|
||||
copyBtn.style.opacity = selectedResults.size === 0 ? "0.5" : "1"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy selected results to clipboard
|
||||
*/
|
||||
async function copySelectedResults(data: unknown) {
|
||||
const response = data as {
|
||||
searchResults?: { results?: Array<{ memory?: string }> }
|
||||
}
|
||||
const results = response?.searchResults?.results || []
|
||||
|
||||
const selectedMemories = Array.from(selectedResults)
|
||||
.sort((a, b) => a - b)
|
||||
.map((index) => results[index]?.memory)
|
||||
.filter(Boolean)
|
||||
|
||||
if (selectedMemories.length === 0) return
|
||||
|
||||
// Format the copied content
|
||||
const formattedContent = selectedMemories
|
||||
.map((memory, i) => `${i + 1}. ${memory}`)
|
||||
.join("\n\n")
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(formattedContent)
|
||||
|
||||
// Show copied feedback
|
||||
const copyBtn = document.getElementById("sm-copy-btn")
|
||||
if (copyBtn) {
|
||||
const originalText = copyBtn.textContent
|
||||
copyBtn.textContent = "Copied!"
|
||||
setTimeout(() => {
|
||||
if (copyBtn) copyBtn.textContent = originalText
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
// Track event
|
||||
browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.CAPTURE_PROMPT,
|
||||
data: {
|
||||
prompt: `Copied ${selectedMemories.length} memories`,
|
||||
platform: "selection_search",
|
||||
source: "copy_selected",
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to copy to clipboard:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the panel
|
||||
*/
|
||||
export function hidePanel() {
|
||||
if (panelElement) {
|
||||
panelElement.remove()
|
||||
panelElement = null
|
||||
}
|
||||
selectedResults.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
function escapeHtml(text: string): string {
|
||||
const div = document.createElement("div")
|
||||
div.textContent = text
|
||||
return div.innerHTML
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle selection change
|
||||
*/
|
||||
function handleSelectionChange() {
|
||||
const selection = window.getSelection()
|
||||
const text = selection?.toString().trim() || ""
|
||||
|
||||
// Hide FAB if selection is empty or inside extension UI
|
||||
if (
|
||||
!text ||
|
||||
text.length < UI_CONFIG.SELECTION_MIN_LENGTH ||
|
||||
text.length > UI_CONFIG.SELECTION_MAX_LENGTH ||
|
||||
isSelectionInsideExtensionUI()
|
||||
) {
|
||||
hideFAB()
|
||||
return
|
||||
}
|
||||
|
||||
const rect = getSelectionRect()
|
||||
if (rect && rect.width > 0 && rect.height > 0) {
|
||||
showFAB(rect, text)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle message from background to open search panel
|
||||
*/
|
||||
export function handleOpenSearchPanel(query: string) {
|
||||
currentQuery = query
|
||||
hideFAB()
|
||||
triggerSearch()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize selection search functionality
|
||||
*/
|
||||
export function initializeSelectionSearch() {
|
||||
// Listen for mouseup to detect selection
|
||||
document.addEventListener("mouseup", () => {
|
||||
// Small delay to ensure selection is complete
|
||||
setTimeout(handleSelectionChange, 10)
|
||||
})
|
||||
|
||||
// Listen for keyup for keyboard selection
|
||||
document.addEventListener("keyup", (e) => {
|
||||
if (e.shiftKey) {
|
||||
setTimeout(handleSelectionChange, 10)
|
||||
}
|
||||
})
|
||||
|
||||
// Hide FAB when clicking elsewhere
|
||||
document.addEventListener("mousedown", (e) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (
|
||||
fabElement &&
|
||||
!fabElement.contains(target) &&
|
||||
!panelElement?.contains(target)
|
||||
) {
|
||||
// Don't hide immediately to allow FAB click
|
||||
setTimeout(() => {
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.toString().trim().length === 0) {
|
||||
hideFAB()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -311,12 +311,7 @@ function App() {
|
|||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 p-2.5 relative">
|
||||
<img
|
||||
alt="supermemory"
|
||||
className="w-8 h-8 shrink-0"
|
||||
src="./dark-transparent.svg"
|
||||
style={{ width: "80%", height: "45px" }}
|
||||
/>
|
||||
<div className="text-white text-lg font-semibold ml-2">supermemory</div>
|
||||
{userSignedIn && (
|
||||
<button
|
||||
className="bg-none border-none text-base cursor-pointer text-gray-500 p-1 rounded transition-colors duration-200"
|
||||
|
|
|
|||
|
|
@ -146,7 +146,10 @@ export async function searchMemories(query: string): Promise<unknown> {
|
|||
try {
|
||||
const response = await makeAuthenticatedRequest<unknown>("/v4/search", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ q: query, include: { relatedMemories: true } }),
|
||||
body: JSON.stringify({
|
||||
q: query,
|
||||
include: { relatedMemories: true },
|
||||
}),
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ export const ELEMENT_IDS = {
|
|||
CLAUDE_INPUT_BAR_ELEMENT: "sm-claude-input-bar-element",
|
||||
T3_INPUT_BAR_ELEMENT: "sm-t3-input-bar-element",
|
||||
PROJECT_SELECTION_MODAL: "sm-project-selection-modal",
|
||||
SELECTION_SEARCH_FAB: "sm-selection-search-fab",
|
||||
SELECTION_SEARCH_PANEL: "sm-selection-search-panel",
|
||||
} as const
|
||||
|
||||
/**
|
||||
|
|
@ -36,6 +38,8 @@ export const UI_CONFIG = {
|
|||
OBSERVER_THROTTLE_DELAY: 300, // milliseconds between observer callback executions
|
||||
ROUTE_CHECK_INTERVAL: 2000, // milliseconds between route change checks
|
||||
API_REQUEST_TIMEOUT: 10000, // milliseconds for API request timeout
|
||||
SELECTION_MIN_LENGTH: 3, // minimum characters to show FAB
|
||||
SELECTION_MAX_LENGTH: 500, // maximum characters to show FAB
|
||||
} as const
|
||||
|
||||
/**
|
||||
|
|
@ -69,10 +73,13 @@ export const MESSAGE_TYPES = {
|
|||
GET_RELATED_MEMORIES: "sm-get-related-memories",
|
||||
CAPTURE_PROMPT: "sm-capture-prompt",
|
||||
FETCH_PROJECTS: "sm-fetch-projects",
|
||||
SEARCH_SELECTION: "sm-search-selection",
|
||||
OPEN_SEARCH_PANEL: "sm-open-search-panel",
|
||||
} as const
|
||||
|
||||
export const CONTEXT_MENU_IDS = {
|
||||
SAVE_TO_SUPERMEMORY: "sm-save-to-supermemory",
|
||||
SEARCH_SUPERMEMORY: "sm-search-supermemory",
|
||||
} as const
|
||||
|
||||
export const POSTHOG_EVENT_KEY = {
|
||||
|
|
@ -86,4 +93,6 @@ export const POSTHOG_EVENT_KEY = {
|
|||
CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED: "claude_chat_memories_auto_searched",
|
||||
CHATGPT_CHAT_MEMORIES_SEARCHED: "chatgpt_chat_memories_searched",
|
||||
CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED: "chatgpt_chat_memories_auto_searched",
|
||||
SELECTION_SEARCH_TRIGGERED: "selection_search_triggered",
|
||||
SELECTION_SEARCH_COPIED: "selection_search_copied",
|
||||
} as const
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue