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 = 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 = ` Search Search ` 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 = `
supermemory Search Results
` 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 = `
Searching for:
${escapeHtml(query)}
` 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 = `
Searching memories...
` } else if (state === "auth_error") { content.innerHTML = `
Sign in required

Please sign in to supermemory to search your memories.

` } else if (state === "error") { content.innerHTML = `
Search failed

${escapeHtml(errorMessage || "An error occurred")}

` } 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 = `
No memories found

Try a different search query.

` } else { content.innerHTML = results .map( (result, index) => `

${escapeHtml(result.memory || "")}

`, ) .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 = ` 0 selected ` 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) } }) }