From e97d642217c3afee355168308b74f1cb36f31b9b Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommmu Date: Wed, 27 Aug 2025 17:54:33 -0700 Subject: [PATCH] extension: updated minor changes --- .../entrypoints/background.ts | 118 +++---- apps/browser-extension/entrypoints/content.ts | 300 ++++++++--------- .../entrypoints/popup/App.css | 40 ++- .../entrypoints/popup/App.tsx | 222 ++++++------- .../entrypoints/popup/main.tsx | 12 +- .../entrypoints/welcome/Welcome.tsx | 58 +--- .../entrypoints/welcome/main.tsx | 12 +- .../entrypoints/welcome/welcome.css | 73 ++--- apps/browser-extension/utils/api.ts | 92 +++--- apps/browser-extension/utils/constants.ts | 18 +- apps/browser-extension/utils/twitter-auth.ts | 52 +-- .../browser-extension/utils/twitter-import.ts | 115 +++---- apps/browser-extension/utils/twitter-utils.ts | 304 +++++++++--------- apps/browser-extension/utils/types.ts | 130 ++++---- apps/browser-extension/utils/ui-components.ts | 202 ++++++------ apps/browser-extension/wxt.config.ts | 12 +- 16 files changed, 848 insertions(+), 912 deletions(-) diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts index 5f8c24ab..eabe8c50 100644 --- a/apps/browser-extension/entrypoints/background.ts +++ b/apps/browser-extension/entrypoints/background.ts @@ -1,55 +1,55 @@ -import { getDefaultProject, saveMemory, searchMemories } from "../utils/api"; +import { getDefaultProject, saveMemory, searchMemories } from "../utils/api" import { CONTAINER_TAGS, CONTEXT_MENU_IDS, MESSAGE_TYPES, -} from "../utils/constants"; -import { captureTwitterTokens } from "../utils/twitter-auth"; +} from "../utils/constants" +import { captureTwitterTokens } from "../utils/twitter-auth" import { type TwitterImportConfig, TwitterImporter, -} from "../utils/twitter-import"; +} from "../utils/twitter-import" import type { ExtensionMessage, MemoryData, MemoryPayload, -} from "../utils/types"; +} from "../utils/types" interface SearchResponse { results: Array<{ chunks: Array<{ - content: string; - }>; - }>; + content: string + }> + }> } export default defineBackground(() => { - let twitterImporter: TwitterImporter | null = null; + let twitterImporter: TwitterImporter | null = null browser.runtime.onInstalled.addListener((details) => { browser.contextMenus.create({ id: CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY, title: "Save to supermemory", contexts: ["selection", "page", "link"], - }); + }) // Open welcome tab on first install if (details.reason === "install") { browser.tabs.create({ url: browser.runtime.getURL("/welcome.html"), - }); + }) } - }); + }) // Intercept Twitter requests to capture authentication headers. browser.webRequest.onBeforeSendHeaders.addListener( (details) => { - captureTwitterTokens(details); - return {}; + captureTwitterTokens(details) + return {} }, { urls: ["*://x.com/*", "*://twitter.com/*"] }, ["requestHeaders", "extraHeaders"], - ); + ) // Handle context menu clicks. browser.contextMenus.onClicked.addListener(async (info, tab) => { @@ -58,27 +58,27 @@ export default defineBackground(() => { try { await browser.tabs.sendMessage(tab.id, { action: MESSAGE_TYPES.SAVE_MEMORY, - }); + }) } catch (error) { - console.error("Failed to send message to content script:", error); + console.error("Failed to send message to content script:", error) } } } - }); + }) // Send message to current active tab. const sendMessageToCurrentTab = async (message: string) => { const tabs = await browser.tabs.query({ active: true, currentWindow: true, - }); + }) if (tabs.length > 0 && tabs[0].id) { await browser.tabs.sendMessage(tabs[0].id, { type: MESSAGE_TYPES.IMPORT_UPDATE, importedMessage: message, - }); + }) } - }; + } /** * Send import completion message @@ -87,14 +87,14 @@ export default defineBackground(() => { const tabs = await browser.tabs.query({ active: true, currentWindow: true, - }); + }) if (tabs.length > 0 && tabs[0].id) { await browser.tabs.sendMessage(tabs[0].id, { type: MESSAGE_TYPES.IMPORT_DONE, totalImported, - }); + }) } - }; + } /** * Save memory to supermemory API @@ -103,48 +103,48 @@ export default defineBackground(() => { data: MemoryData, ): Promise<{ success: boolean; data?: unknown; error?: string }> => { try { - let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT; + let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT try { - const defaultProject = await getDefaultProject(); + const defaultProject = await getDefaultProject() if (defaultProject?.containerTag) { - containerTag = defaultProject.containerTag; + containerTag = defaultProject.containerTag } } catch (error) { - console.warn("Failed to get default project, using fallback:", error); + console.warn("Failed to get default project, using fallback:", error) } const payload: MemoryPayload = { containerTags: [containerTag], content: `${data.highlightedText}\n\n${data.html}\n\n${data?.url}`, metadata: { sm_source: "consumer" }, - }; + } - const responseData = await saveMemory(payload); - return { success: true, data: responseData }; + const responseData = await saveMemory(payload) + return { success: true, data: responseData } } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Unknown error", - }; + } } - }; + } const getRelatedMemories = async ( data: string, ): Promise<{ success: boolean; data?: unknown; error?: string }> => { try { - const responseData = await searchMemories(data); + const responseData = await searchMemories(data) const content = (responseData as SearchResponse).results[0].chunks[0] - .content; - console.log("Content:", content); - return { success: true, data: content }; + .content + console.log("Content:", content) + return { success: true, data: content } } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Unknown error", - }; + } } - }; + } /** * Handle extension messages @@ -157,48 +157,48 @@ export default defineBackground(() => { onProgress: sendMessageToCurrentTab, onComplete: sendImportDoneMessage, onError: async (error: Error) => { - await sendMessageToCurrentTab(`Error: ${error.message}`); + await sendMessageToCurrentTab(`Error: ${error.message}`) }, - }; + } - twitterImporter = new TwitterImporter(importConfig); - twitterImporter.startImport().catch(console.error); - sendResponse({ success: true }); - return true; + twitterImporter = new TwitterImporter(importConfig) + twitterImporter.startImport().catch(console.error) + sendResponse({ success: true }) + return true } // Handle regular memory save request if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { - (async () => { + ;(async () => { try { const result = await saveMemoryToSupermemory( message.data as MemoryData, - ); - sendResponse(result); + ) + sendResponse(result) } catch (error) { sendResponse({ success: false, error: error instanceof Error ? error.message : "Unknown error", - }); + }) } - })(); - return true; + })() + return true } if (message.action === MESSAGE_TYPES.GET_RELATED_MEMORIES) { - (async () => { + ;(async () => { try { - const result = await getRelatedMemories(message.data as string); - sendResponse(result); + const result = await getRelatedMemories(message.data as string) + sendResponse(result) } catch (error) { sendResponse({ success: false, error: error instanceof Error ? error.message : "Unknown error", - }); + }) } - })(); - return true; + })() + return true } }, - ); -}); + ) +}) diff --git a/apps/browser-extension/entrypoints/content.ts b/apps/browser-extension/entrypoints/content.ts index afca4f1f..385beaec 100644 --- a/apps/browser-extension/entrypoints/content.ts +++ b/apps/browser-extension/entrypoints/content.ts @@ -1,85 +1,85 @@ -import { DOMAINS, ELEMENT_IDS, MESSAGE_TYPES } from "../utils/constants"; +import { DOMAINS, ELEMENT_IDS, MESSAGE_TYPES } from "../utils/constants" import { createChatGPTInputBarElement, createSaveTweetElement, createTwitterImportButton, createTwitterImportUI, DOMUtils, -} from "../utils/ui-components"; +} from "../utils/ui-components" export default defineContentScript({ matches: [""], main() { - let twitterImportUI: HTMLElement | null = null; - let isTwitterImportOpen = false; + let twitterImportUI: HTMLElement | null = null + let isTwitterImportOpen = false browser.runtime.onMessage.addListener(async (message) => { if (message.action === MESSAGE_TYPES.SHOW_TOAST) { - DOMUtils.showToast(message.state); + DOMUtils.showToast(message.state) } else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { - await saveMemory(); + await saveMemory() } else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { - updateTwitterImportUI(message); + updateTwitterImportUI(message) } else if (message.type === MESSAGE_TYPES.IMPORT_DONE) { - updateTwitterImportUI(message); + updateTwitterImportUI(message) } - }); + }) const observeForMemoriesDialog = () => { const observer = new MutationObserver(() => { if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { - addSupermemoryButtonToMemoriesDialog(); - addSaveChatGPTElementBeforeComposerBtn(); + addSupermemoryButtonToMemoriesDialog() + addSaveChatGPTElementBeforeComposerBtn() } if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - addTwitterImportButton(); + addTwitterImportButton() //addSaveTweetElement(); } - }); + }) observer.observe(document.body, { childList: true, subtree: true, - }); + }) if ( window.location.hostname === "chatgpt.com" || window.location.hostname === "chat.openai.com" ) { - addSupermemoryButtonToMemoriesDialog(); - addSaveChatGPTElementBeforeComposerBtn(); + addSupermemoryButtonToMemoriesDialog() + addSaveChatGPTElementBeforeComposerBtn() } if ( window.location.hostname === "x.com" || window.location.hostname === "twitter.com" ) { - addTwitterImportButton(); + addTwitterImportButton() //addSaveTweetElement(); } - }; + } if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) { setTimeout(() => { - addTwitterImportButton(); // Wait 2 seconds for page to load + addTwitterImportButton() // Wait 2 seconds for page to load //addSaveTweetElement(); - }, 2000); + }, 2000) } if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", observeForMemoriesDialog); + document.addEventListener("DOMContentLoaded", observeForMemoriesDialog) } else { - observeForMemoriesDialog(); + observeForMemoriesDialog() } async function saveMemory() { try { - DOMUtils.showToast("loading"); + DOMUtils.showToast("loading") - const highlightedText = window.getSelection()?.toString() || ""; + const highlightedText = window.getSelection()?.toString() || "" - const url = window.location.href; + const url = window.location.href - const html = document.documentElement.outerHTML; + const html = document.documentElement.outerHTML const response = await browser.runtime.sendMessage({ action: MESSAGE_TYPES.SAVE_MEMORY, @@ -88,75 +88,75 @@ export default defineContentScript({ highlightedText, url, }, - }); + }) - console.log("Response from enxtension:", response); + console.log("Response from enxtension:", response) if (response.success) { - DOMUtils.showToast("success"); + DOMUtils.showToast("success") } else { - DOMUtils.showToast("error"); + DOMUtils.showToast("error") } } catch (error) { - console.error("Error saving memory:", error); - DOMUtils.showToast("error"); + console.error("Error saving memory:", error) + DOMUtils.showToast("error") } } async function getRelatedMemories() { try { const userQuery = - document.getElementById("prompt-textarea")?.textContent || ""; + document.getElementById("prompt-textarea")?.textContent || "" const response = await browser.runtime.sendMessage({ action: MESSAGE_TYPES.GET_RELATED_MEMORIES, data: userQuery, - }); + }) if (response.success && response.data) { - const promptElement = document.getElementById("prompt-textarea"); + const promptElement = document.getElementById("prompt-textarea") if (promptElement) { - const currentContent = promptElement.innerHTML; - promptElement.innerHTML = `${currentContent}
Supermemories: ${response.data}`; + const currentContent = promptElement.innerHTML + promptElement.innerHTML = `${currentContent}
Supermemories: ${response.data}` } } } catch (error) { - console.error("Error getting related memories:", error); + console.error("Error getting related memories:", error) } } function addSupermemoryButtonToMemoriesDialog() { - const dialogs = document.querySelectorAll('[role="dialog"]'); - let memoriesDialog: HTMLElement | null = null; + const dialogs = document.querySelectorAll('[role="dialog"]') + let memoriesDialog: HTMLElement | null = null for (const dialog of dialogs) { - const headerText = dialog.querySelector("h2"); + const headerText = dialog.querySelector("h2") if (headerText?.textContent?.includes("Saved memories")) { - memoriesDialog = dialog as HTMLElement; - break; + memoriesDialog = dialog as HTMLElement + break } } - if (!memoriesDialog) return; + if (!memoriesDialog) return - if (memoriesDialog.querySelector("#supermemory-save-button")) return; + if (memoriesDialog.querySelector("#supermemory-save-button")) return const deleteAllContainer = memoriesDialog.querySelector( ".mt-5.flex.justify-end", - ); - if (!deleteAllContainer) return; + ) + if (!deleteAllContainer) return - const supermemoryButton = document.createElement("button"); - supermemoryButton.id = "supermemory-save-button"; - supermemoryButton.className = "btn relative btn-primary-outline mr-2"; + const supermemoryButton = document.createElement("button") + 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("/icon-16.png") supermemoryButton.innerHTML = `
supermemory Save to supermemory
- `; + ` supermemoryButton.style.cssText = ` background: #1C2026 !important; @@ -168,101 +168,101 @@ export default defineContentScript({ font-size: 14px !important; margin-right: 8px !important; cursor: pointer !important; - `; + ` supermemoryButton.addEventListener("mouseenter", () => { - supermemoryButton.style.backgroundColor = "#2B2E33"; - }); + supermemoryButton.style.backgroundColor = "#2B2E33" + }) supermemoryButton.addEventListener("mouseleave", () => { - supermemoryButton.style.backgroundColor = "#1C2026"; - }); + supermemoryButton.style.backgroundColor = "#1C2026" + }) supermemoryButton.addEventListener("click", async () => { - await saveMemoriesToSupermemory(); - }); + await saveMemoriesToSupermemory() + }) deleteAllContainer.insertBefore( supermemoryButton, deleteAllContainer.firstChild, - ); + ) } async function saveMemoriesToSupermemory() { try { - DOMUtils.showToast("loading"); + DOMUtils.showToast("loading") const memoriesTable = document.querySelector( '[role="dialog"] table tbody', - ); + ) if (!memoriesTable) { - DOMUtils.showToast("error"); - return; + DOMUtils.showToast("error") + return } - const memoryRows = memoriesTable.querySelectorAll("tr"); - const memories: string[] = []; + const memoryRows = memoriesTable.querySelectorAll("tr") + const memories: string[] = [] memoryRows.forEach((row) => { - const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap"); + const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap") if (memoryCell?.textContent) { - memories.push(memoryCell.textContent.trim()); + memories.push(memoryCell.textContent.trim()) } - }); + }) - console.log("Memories:", memories); + console.log("Memories:", memories) if (memories.length === 0) { - DOMUtils.showToast("error"); - return; + DOMUtils.showToast("error") + return } - const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}`; + const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}` const response = await browser.runtime.sendMessage({ action: "saveMemory", data: { html: combinedContent, }, - }); + }) if (response.success) { - DOMUtils.showToast("success"); + DOMUtils.showToast("success") } else { - DOMUtils.showToast("error"); + DOMUtils.showToast("error") } } catch (error) { - console.error("Error saving memories to supermemory:", error); - DOMUtils.showToast("error"); + console.error("Error saving memories to supermemory:", error) + DOMUtils.showToast("error") } } function addTwitterImportButton() { if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - return; + return } if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { - return; + return } const button = createTwitterImportButton(() => { - showTwitterImportUI(); - }); + showTwitterImportUI() + }) - document.body.appendChild(button); + document.body.appendChild(button) } function showTwitterImportUI() { if (twitterImportUI) { - twitterImportUI.remove(); + twitterImportUI.remove() } - isTwitterImportOpen = true; + isTwitterImportOpen = true // Check if user is authenticated browser.storage.local.get(["bearerToken"], ({ bearerToken }) => { - const isAuthenticated = !!bearerToken; + const isAuthenticated = !!bearerToken twitterImportUI = createTwitterImportUI( hideTwitterImportUI, @@ -270,35 +270,35 @@ export default defineContentScript({ try { await browser.runtime.sendMessage({ type: MESSAGE_TYPES.BATCH_IMPORT_ALL, - }); + }) } catch (error) { - console.error("Error starting import:", error); + console.error("Error starting import:", error) } }, isAuthenticated, - ); + ) - document.body.appendChild(twitterImportUI); - }); + document.body.appendChild(twitterImportUI) + }) } function hideTwitterImportUI() { if (twitterImportUI) { - twitterImportUI.remove(); - twitterImportUI = null; + twitterImportUI.remove() + twitterImportUI = null } - isTwitterImportOpen = false; + isTwitterImportOpen = false } function updateTwitterImportUI(message: { - type: string; - importedMessage?: string; - totalImported?: number; + type: string + importedMessage?: string + totalImported?: number }) { - if (!isTwitterImportOpen || !twitterImportUI) return; + if (!isTwitterImportOpen || !twitterImportUI) return - const statusDiv = twitterImportUI.querySelector("#twitter-import-status"); - const button = twitterImportUI.querySelector("#twitter-import-button"); + const statusDiv = twitterImportUI.querySelector("#twitter-import-status") + const button = twitterImportUI.querySelector("#twitter-import-button") if (message.type === "import-update") { if (statusDiv) { @@ -307,11 +307,11 @@ export default defineContentScript({
${message.importedMessage} - `; + ` } if (button) { - (button as HTMLButtonElement).disabled = true; - (button as HTMLButtonElement).textContent = "Importing..."; + ;(button as HTMLButtonElement).disabled = true + ;(button as HTMLButtonElement).textContent = "Importing..." } } @@ -322,100 +322,100 @@ export default defineContentScript({ Successfully imported ${message.totalImported} tweets! - `; + ` } setTimeout(() => { - hideTwitterImportUI(); - }, 3000); + hideTwitterImportUI() + }, 3000) } } function addSaveChatGPTElementBeforeComposerBtn() { if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { - return; + return } - const composerButtons = document.querySelectorAll("button.composer-btn"); + const composerButtons = document.querySelectorAll("button.composer-btn") composerButtons.forEach((button) => { if (button.hasAttribute("data-supermemory-icon-added-before")) { - return; + return } - const parent = button.parentElement; - if (!parent) return; + const parent = button.parentElement + if (!parent) return - const parentSiblings = parent.parentElement?.children; - if (!parentSiblings) return; + const parentSiblings = parent.parentElement?.children + if (!parentSiblings) return - let hasSpeechButtonSibling = false; + let hasSpeechButtonSibling = false for (const sibling of parentSiblings) { if ( sibling.getAttribute("data-testid") === "composer-speech-button-container" ) { - hasSpeechButtonSibling = true; - break; + hasSpeechButtonSibling = true + break } } - if (!hasSpeechButtonSibling) return; + if (!hasSpeechButtonSibling) return - const grandParent = parent.parentElement; - if (!grandParent) 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; + button.setAttribute("data-supermemory-icon-added-before", "true") + return } const saveChatGPTElement = createChatGPTInputBarElement(async () => { - await getRelatedMemories(); - }); + await getRelatedMemories() + }) - saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; + 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"); + button.setAttribute("data-supermemory-icon-added-before", "true") - grandParent.insertBefore(saveChatGPTElement, parent); - }); + grandParent.insertBefore(saveChatGPTElement, parent) + }) } // TODO: Add Tweet Capture Functionality function _addSaveTweetElement() { if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - return; + return } const targetDivs = document.querySelectorAll( "div.css-175oi2r.r-18u37iz.r-1h0z5md.r-1wron08", - ); + ) targetDivs.forEach((targetDiv) => { if (targetDiv.hasAttribute("data-supermemory-icon-added")) { - return; + return } - const previousElement = targetDiv.previousElementSibling; + const previousElement = targetDiv.previousElementSibling if (previousElement?.id?.startsWith(ELEMENT_IDS.SAVE_TWEET_ELEMENT)) { - targetDiv.setAttribute("data-supermemory-icon-added", "true"); - return; + targetDiv.setAttribute("data-supermemory-icon-added", "true") + return } const saveTweetElement = createSaveTweetElement(async () => { - await saveMemory(); - }); + await saveMemory() + }) - saveTweetElement.id = `${ELEMENT_IDS.SAVE_TWEET_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; + saveTweetElement.id = `${ELEMENT_IDS.SAVE_TWEET_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` - targetDiv.setAttribute("data-supermemory-icon-added", "true"); + targetDiv.setAttribute("data-supermemory-icon-added", "true") - targetDiv.parentNode?.insertBefore(saveTweetElement, targetDiv); - }); + targetDiv.parentNode?.insertBefore(saveTweetElement, targetDiv) + }) } document.addEventListener("keydown", async (event) => { @@ -424,16 +424,16 @@ export default defineContentScript({ event.shiftKey && event.key === "m" ) { - event.preventDefault(); - await saveMemory(); + event.preventDefault() + await saveMemory() } - }); + }) window.addEventListener("message", (event) => { if (event.source !== window) { - return; + return } - const bearerToken = event.data.token; + const bearerToken = event.data.token if (bearerToken) { if ( @@ -445,12 +445,12 @@ export default defineContentScript({ ) { console.log( "Bearer token is only allowed to be used on localhost or supermemory.ai", - ); - return; + ) + return } - chrome.storage.local.set({ bearerToken }, () => {}); + chrome.storage.local.set({ bearerToken }, () => {}) } - }); + }) }, -}); +}) diff --git a/apps/browser-extension/entrypoints/popup/App.css b/apps/browser-extension/entrypoints/popup/App.css index dc8eb590..c6e7c59b 100644 --- a/apps/browser-extension/entrypoints/popup/App.css +++ b/apps/browser-extension/entrypoints/popup/App.css @@ -1,48 +1,50 @@ /* Custom Font Definitions */ @font-face { - font-family: 'Space Grotesk'; + font-family: "Space Grotesk"; font-style: normal; font-weight: 300; font-display: swap; - src: url('/fonts/SpaceGrotesk-Light.ttf') format('truetype'); + src: url("/fonts/SpaceGrotesk-Light.ttf") format("truetype"); } @font-face { - font-family: 'Space Grotesk'; + font-family: "Space Grotesk"; font-style: normal; font-weight: 400; font-display: swap; - src: url('/fonts/SpaceGrotesk-Regular.ttf') format('truetype'); + src: url("/fonts/SpaceGrotesk-Regular.ttf") format("truetype"); } @font-face { - font-family: 'Space Grotesk'; + font-family: "Space Grotesk"; font-style: normal; font-weight: 500; font-display: swap; - src: url('/fonts/SpaceGrotesk-Medium.ttf') format('truetype'); + src: url("/fonts/SpaceGrotesk-Medium.ttf") format("truetype"); } @font-face { - font-family: 'Space Grotesk'; + font-family: "Space Grotesk"; font-style: normal; font-weight: 600; font-display: swap; - src: url('/fonts/SpaceGrotesk-SemiBold.ttf') format('truetype'); + src: url("/fonts/SpaceGrotesk-SemiBold.ttf") format("truetype"); } @font-face { - font-family: 'Space Grotesk'; + font-family: "Space Grotesk"; font-style: normal; font-weight: 700; font-display: swap; - src: url('/fonts/SpaceGrotesk-Bold.ttf') format('truetype'); + src: url("/fonts/SpaceGrotesk-Bold.ttf") format("truetype"); } .popup-container { width: 320px; padding: 0; - font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: + "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + sans-serif; background: #ffffff; border-radius: 8px; position: relative; @@ -81,7 +83,9 @@ color: #6c757d; padding: 4px; border-radius: 4px; - transition: color 0.2s ease, background-color 0.2s ease; + transition: + color 0.2s ease, + background-color 0.2s ease; } .header-sign-out:hover { @@ -403,7 +407,9 @@ font-size: 13px; font-weight: 400; cursor: pointer; - transition: background-color 0.2s ease, color 0.2s ease; + transition: + background-color 0.2s ease, + color 0.2s ease; } .secondary-btn:hover { @@ -505,7 +511,9 @@ background-color: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef; - transition: background-color 0.2s ease, border-color 0.2s ease; + transition: + background-color 0.2s ease, + border-color 0.2s ease; } .project-selector-btn:hover .project-selector-content { @@ -599,7 +607,9 @@ cursor: pointer; padding: 4px 8px; border-radius: 4px; - transition: color 0.2s ease, background-color 0.2s ease; + transition: + color 0.2s ease, + background-color 0.2s ease; outline: none; } diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx index d5cb3014..b9e33d4b 100644 --- a/apps/browser-extension/entrypoints/popup/App.tsx +++ b/apps/browser-extension/entrypoints/popup/App.tsx @@ -1,181 +1,180 @@ -import { useEffect, useState } from "react"; -import "./App.css"; +import { useEffect, useState } from "react" +import "./App.css" import { getDefaultProject, getProjects, setDefaultProject, -} from "../../utils/api"; -import type { Project } from "../../utils/types"; +} from "../../utils/api" +import type { Project } from "../../utils/types" function App() { - const [userSignedIn, setUserSignedIn] = useState(false); - const [loading, setLoading] = useState(true); - const [projects, setProjects] = useState([]); + const [userSignedIn, setUserSignedIn] = useState(false) + const [loading, setLoading] = useState(true) + const [projects, setProjects] = useState([]) const [defaultProject, setDefaultProjectState] = useState( null, - ); - const [loadingProjects, setLoadingProjects] = useState(false); - const [showProjectSelector, setShowProjectSelector] = - useState(false); - const [currentUrl, setCurrentUrl] = useState(""); - const [currentTitle, setCurrentTitle] = useState(""); - const [saving, setSaving] = useState(false); - const [activeTab, setActiveTab] = useState<"save" | "imports">("save"); + ) + const [loadingProjects, setLoadingProjects] = useState(false) + const [showProjectSelector, setShowProjectSelector] = useState(false) + const [currentUrl, setCurrentUrl] = useState("") + const [currentTitle, setCurrentTitle] = useState("") + const [saving, setSaving] = useState(false) + const [activeTab, setActiveTab] = useState<"save" | "imports">("save") useEffect(() => { const checkAuthStatus = async () => { try { - const result = await chrome.storage.local.get(["bearerToken"]); - const isSignedIn = !!result.bearerToken; - setUserSignedIn(isSignedIn); + const result = await chrome.storage.local.get(["bearerToken"]) + const isSignedIn = !!result.bearerToken + setUserSignedIn(isSignedIn) if (isSignedIn) { try { - const defaultProj = await getDefaultProject(); - setDefaultProjectState(defaultProj); + const defaultProj = await getDefaultProject() + setDefaultProjectState(defaultProj) } catch (error) { - console.error("Error loading default project:", error); + console.error("Error loading default project:", error) } } } catch (error) { - console.error("Error checking auth status:", error); - setUserSignedIn(false); + console.error("Error checking auth status:", error) + setUserSignedIn(false) } finally { - setLoading(false); + setLoading(false) } - }; + } const getCurrentTab = async () => { try { const tabs = await chrome.tabs.query({ active: true, currentWindow: true, - }); + }) if (tabs.length > 0 && tabs[0].url && tabs[0].title) { - setCurrentUrl(tabs[0].url); - setCurrentTitle(tabs[0].title); + setCurrentUrl(tabs[0].url) + setCurrentTitle(tabs[0].title) } } catch (error) { - console.error("Error getting current tab:", error); + console.error("Error getting current tab:", error) } - }; + } - checkAuthStatus(); - getCurrentTab(); - }, []); + checkAuthStatus() + getCurrentTab() + }, []) const loadProjects = async () => { - setLoadingProjects(true); + setLoadingProjects(true) try { - const projectsList = await getProjects(); - setProjects(projectsList); - console.log("Projects:", projectsList); - console.log("Default project:", defaultProject); + const projectsList = await getProjects() + setProjects(projectsList) + console.log("Projects:", projectsList) + console.log("Default project:", defaultProject) // If no default project is set and projects are available, set first as default if (!defaultProject && projectsList.length > 0) { - const firstProject = projectsList[0]; - await setDefaultProject(firstProject); - setDefaultProjectState(firstProject); + const firstProject = projectsList[0] + await setDefaultProject(firstProject) + setDefaultProjectState(firstProject) } } catch (error) { - console.error("Error loading projects:", error); + console.error("Error loading projects:", error) } finally { - setLoadingProjects(false); + setLoadingProjects(false) } - }; + } const handleProjectSelect = async (project: Project) => { try { - await setDefaultProject(project); - setDefaultProjectState(project); - setShowProjectSelector(false); + await setDefaultProject(project) + setDefaultProjectState(project) + setShowProjectSelector(false) } catch (error) { - console.error("Error setting default project:", error); + console.error("Error setting default project:", error) } - }; + } const handleShowProjectSelector = () => { - console.log("handleShowProjectSelector, projects.length:", projects.length); + console.log("handleShowProjectSelector, projects.length:", projects.length) if (projects.length === 0) { - loadProjects(); + loadProjects() } - setShowProjectSelector(true); - }; + setShowProjectSelector(true) + } const handleSaveCurrentPage = async () => { - setSaving(true); + setSaving(true) try { const tabs = await chrome.tabs.query({ active: true, currentWindow: true, - }); + }) if (tabs.length > 0 && tabs[0].id) { await chrome.tabs.sendMessage(tabs[0].id, { action: "saveMemory", - }); + }) } } catch (error) { - console.error("Failed to save current page:", error); + console.error("Failed to save current page:", error) } finally { - setSaving(false); + setSaving(false) } - }; + } const handleSignOut = async () => { try { - await chrome.storage.local.remove(["bearerToken"]); - setUserSignedIn(false); - setDefaultProjectState(null); - setProjects([]); + await chrome.storage.local.remove(["bearerToken"]) + setUserSignedIn(false) + setDefaultProjectState(null) + setProjects([]) } catch (error) { - console.error("Error signing out:", error); + console.error("Error signing out:", error) } - }; + } if (loading) { return (
- supermemory + supermemory

supermemory

Loading...
- ); + ) } return (
supermemory {userSignedIn && ( )} @@ -186,16 +185,16 @@ function App() { {/* Tab Navigation */}
@@ -217,9 +216,9 @@ function App() { {/* Project Selection */}
@@ -267,22 +266,23 @@ function App() {
@@ -337,10 +337,10 @@ function App() {
{projects.map((project) => (

)}
- ); + ) } -export default App; +export default App diff --git a/apps/browser-extension/entrypoints/popup/main.tsx b/apps/browser-extension/entrypoints/popup/main.tsx index e0f969f0..bfa6ad76 100644 --- a/apps/browser-extension/entrypoints/popup/main.tsx +++ b/apps/browser-extension/entrypoints/popup/main.tsx @@ -1,13 +1,13 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App.js"; -import "./style.css"; +import React from "react" +import ReactDOM from "react-dom/client" +import App from "./App.js" +import "./style.css" -const rootElement = document.getElementById("root"); +const rootElement = document.getElementById("root") if (rootElement) { ReactDOM.createRoot(rootElement).render( , - ); + ) } diff --git a/apps/browser-extension/entrypoints/welcome/Welcome.tsx b/apps/browser-extension/entrypoints/welcome/Welcome.tsx index 6b1e4eae..b6f4fab2 100644 --- a/apps/browser-extension/entrypoints/welcome/Welcome.tsx +++ b/apps/browser-extension/entrypoints/welcome/Welcome.tsx @@ -5,9 +5,9 @@ function Welcome() { {/* Header */}
supermemory

Your AI-powered second brain for saving and organizing everything @@ -56,61 +56,21 @@ function Welcome() {

- {/* Getting Started */} -
-

- Get Started in 2 Simple Steps -

- -
-
-
1
-
-

Login to Your Account

-

Connect your supermemory account to start saving content

-
-
- -
-
2
-
-

Start Saving

-

Click the extension icon on any page to save it instantly

-
-
-
-
- {/* Actions */}
- -
-

- Need help getting started?{" "} - -

-
{/* Footer */} @@ -118,10 +78,10 @@ function Welcome() {

Learn more at{" "} supermemory.ai @@ -129,7 +89,7 @@ function Welcome() {

- ); + ) } -export default Welcome; +export default Welcome diff --git a/apps/browser-extension/entrypoints/welcome/main.tsx b/apps/browser-extension/entrypoints/welcome/main.tsx index a9233deb..0b54490a 100644 --- a/apps/browser-extension/entrypoints/welcome/main.tsx +++ b/apps/browser-extension/entrypoints/welcome/main.tsx @@ -1,13 +1,13 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import Welcome from "./Welcome"; -import "./welcome.css"; +import React from "react" +import ReactDOM from "react-dom/client" +import Welcome from "./Welcome" +import "./welcome.css" -const rootElement = document.getElementById("root"); +const rootElement = document.getElementById("root") if (rootElement) { ReactDOM.createRoot(rootElement).render( , - ); + ) } diff --git a/apps/browser-extension/entrypoints/welcome/welcome.css b/apps/browser-extension/entrypoints/welcome/welcome.css index 3362fffe..d2c8d43d 100644 --- a/apps/browser-extension/entrypoints/welcome/welcome.css +++ b/apps/browser-extension/entrypoints/welcome/welcome.css @@ -1,42 +1,42 @@ /* Custom Font Definitions */ @font-face { - font-family: 'Space Grotesk'; + font-family: "Space Grotesk"; font-style: normal; font-weight: 300; font-display: swap; - src: url('/fonts/SpaceGrotesk-Light.ttf') format('truetype'); + src: url("/fonts/SpaceGrotesk-Light.ttf") format("truetype"); } @font-face { - font-family: 'Space Grotesk'; + font-family: "Space Grotesk"; font-style: normal; font-weight: 400; font-display: swap; - src: url('/fonts/SpaceGrotesk-Regular.ttf') format('truetype'); + src: url("/fonts/SpaceGrotesk-Regular.ttf") format("truetype"); } @font-face { - font-family: 'Space Grotesk'; + font-family: "Space Grotesk"; font-style: normal; font-weight: 500; font-display: swap; - src: url('/fonts/SpaceGrotesk-Medium.ttf') format('truetype'); + src: url("/fonts/SpaceGrotesk-Medium.ttf") format("truetype"); } @font-face { - font-family: 'Space Grotesk'; + font-family: "Space Grotesk"; font-style: normal; font-weight: 600; font-display: swap; - src: url('/fonts/SpaceGrotesk-SemiBold.ttf') format('truetype'); + src: url("/fonts/SpaceGrotesk-SemiBold.ttf") format("truetype"); } @font-face { - font-family: 'Space Grotesk'; + font-family: "Space Grotesk"; font-style: normal; font-weight: 700; font-display: swap; - src: url('/fonts/SpaceGrotesk-Bold.ttf') format('truetype'); + src: url("/fonts/SpaceGrotesk-Bold.ttf") format("truetype"); } /* Welcome Page Styles */ @@ -47,7 +47,9 @@ } body { - font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: + "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + sans-serif; background: #ffffff; color: #000000; line-height: 1.5; @@ -147,18 +149,6 @@ body { line-height: 1.4; } -/* Getting Started Section */ -.welcome-getting-started { - margin-bottom: 48px; -} - -.getting-started-title { - font-size: 24px; - font-weight: 600; - color: #000000; - margin-bottom: 32px; -} - .steps { display: flex; justify-content: center; @@ -231,31 +221,6 @@ body { cursor: not-allowed; } -.welcome-help { - margin-top: 16px; -} - -.help-text { - font-size: 14px; - color: #6c757d; - margin: 0; -} - -.help-link { - background: none; - border: none; - color: #4285f4; - cursor: pointer; - text-decoration: underline; - font-size: 14px; - padding: 0; - outline: none; -} - -.help-link:hover { - color: #1a73e8; -} - /* Footer */ .welcome-footer { border-top: 1px solid #e9ecef; @@ -283,28 +248,28 @@ body { .welcome-container { padding: 16px; } - + .welcome-title { font-size: 28px; } - + .welcome-subtitle { font-size: 16px; } - + .features-grid { grid-template-columns: 1fr; gap: 16px; } - + .steps { flex-direction: column; align-items: center; } - + .step { max-width: 100%; text-align: center; flex-direction: column; } -} \ No newline at end of file +} diff --git a/apps/browser-extension/utils/api.ts b/apps/browser-extension/utils/api.ts index 93356bf2..1b544c9a 100644 --- a/apps/browser-extension/utils/api.ts +++ b/apps/browser-extension/utils/api.ts @@ -1,27 +1,27 @@ /** * API service for Supermemory browser extension */ -import { API_ENDPOINTS, STORAGE_KEYS } from "./constants"; +import { API_ENDPOINTS, STORAGE_KEYS } from "./constants" import { AuthenticationError, type MemoryPayload, type Project, type ProjectsResponse, SupermemoryAPIError, -} from "./types"; +} from "./types" /** * Get bearer token from storage */ async function getBearerToken(): Promise { - const result = await chrome.storage.local.get([STORAGE_KEYS.BEARER_TOKEN]); - const token = result[STORAGE_KEYS.BEARER_TOKEN]; + const result = await chrome.storage.local.get([STORAGE_KEYS.BEARER_TOKEN]) + const token = result[STORAGE_KEYS.BEARER_TOKEN] if (!token) { - throw new AuthenticationError("Bearer token not found"); + throw new AuthenticationError("Bearer token not found") } - return token; + return token } /** @@ -31,7 +31,7 @@ async function makeAuthenticatedRequest( endpoint: string, options: RequestInit = {}, ): Promise { - const token = await getBearerToken(); + const token = await getBearerToken() const response = await fetch(`${API_ENDPOINTS.SUPERMEMORY_API}${endpoint}`, { ...options, @@ -41,19 +41,19 @@ async function makeAuthenticatedRequest( "Content-Type": "application/json", ...options.headers, }, - }); + }) if (!response.ok) { if (response.status === 401) { - throw new AuthenticationError("Invalid or expired token"); + throw new AuthenticationError("Invalid or expired token") } throw new SupermemoryAPIError( `API request failed: ${response.statusText}`, response.status, - ); + ) } - return response.json(); + return response.json() } /** @@ -62,41 +62,39 @@ async function makeAuthenticatedRequest( export async function fetchProjects(): Promise { try { const response = - await makeAuthenticatedRequest("/v3/projects"); - return response.projects; + await makeAuthenticatedRequest("/v3/projects") + return response.projects } catch (error) { - console.error("Failed to fetch projects:", error); - throw error; + console.error("Failed to fetch projects:", error) + throw error } } /** * Get projects from cache or fetch fresh */ -export async function getProjects( - useCache: boolean = true, -): Promise { +export async function getProjects(useCache = true): Promise { if (useCache) { try { const cached = await chrome.storage.local.get([ STORAGE_KEYS.PROJECTS_CACHE, - ]); - const cachedData = cached[STORAGE_KEYS.PROJECTS_CACHE]; + ]) + const cachedData = cached[STORAGE_KEYS.PROJECTS_CACHE] if (cachedData?.timestamp && cachedData.projects) { // Cache for 5 minutes - const cacheAge = Date.now() - cachedData.timestamp; + const cacheAge = Date.now() - cachedData.timestamp if (cacheAge < 5 * 60 * 1000) { - return cachedData.projects; + return cachedData.projects } } } catch (error) { - console.warn("Failed to read projects cache:", error); + console.warn("Failed to read projects cache:", error) } } // Fetch fresh data - const projects = await fetchProjects(); + const projects = await fetchProjects() // Cache the results try { @@ -105,12 +103,12 @@ export async function getProjects( projects, timestamp: Date.now(), }, - }); + }) } catch (error) { - console.warn("Failed to cache projects:", error); + console.warn("Failed to cache projects:", error) } - return projects; + return projects } /** @@ -120,11 +118,11 @@ export async function getDefaultProject(): Promise { try { const result = await chrome.storage.local.get([ STORAGE_KEYS.DEFAULT_PROJECT, - ]); - return result[STORAGE_KEYS.DEFAULT_PROJECT] || null; + ]) + return result[STORAGE_KEYS.DEFAULT_PROJECT] || null } catch (error) { - console.error("Failed to get default project:", error); - return null; + console.error("Failed to get default project:", error) + return null } } @@ -135,10 +133,10 @@ export async function setDefaultProject(project: Project): Promise { try { await chrome.storage.local.set({ [STORAGE_KEYS.DEFAULT_PROJECT]: project, - }); + }) } catch (error) { - console.error("Failed to set default project:", error); - throw error; + console.error("Failed to set default project:", error) + throw error } } @@ -150,11 +148,11 @@ export async function saveMemory(payload: MemoryPayload): Promise { const response = await makeAuthenticatedRequest("/v3/memories", { method: "POST", body: JSON.stringify(payload), - }); - return response; + }) + return response } catch (error) { - console.error("Failed to save memory:", error); - throw error; + console.error("Failed to save memory:", error) + throw error } } @@ -166,11 +164,11 @@ export async function searchMemories(query: string): Promise { const response = await makeAuthenticatedRequest("/v3/search", { method: "POST", body: JSON.stringify({ q: query }), - }); - return response; + }) + return response } catch (error) { - console.error("Failed to search memories:", error); - throw error; + console.error("Failed to search memories:", error) + throw error } } @@ -180,20 +178,20 @@ export async function searchMemories(query: string): Promise { export async function saveTweet( content: string, metadata: { sm_source: string; [key: string]: unknown }, - containerTag: string = "sm_project_twitter_bookmarks", + containerTag = "sm_project_twitter_bookmarks", ): Promise { try { const payload: MemoryPayload = { containerTags: [containerTag], content, metadata, - }; - await saveMemory(payload); + } + await saveMemory(payload) } catch (error) { if (error instanceof SupermemoryAPIError && error.statusCode === 409) { // Skip if already exists (409 Conflict) - return; + return } - throw error; + throw error } } diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts index ffe39554..d21db8a4 100644 --- a/apps/browser-extension/utils/constants.ts +++ b/apps/browser-extension/utils/constants.ts @@ -8,7 +8,7 @@ export const API_ENDPOINTS = { SUPERMEMORY_WEB: import.meta.env.PROD ? "https://app.supermemory.ai" : "http://localhost:3000", -} as const; +} as const /** * Storage Keys @@ -22,7 +22,7 @@ export const STORAGE_KEYS = { TWITTER_AUTH_TOKEN: "auth", DEFAULT_PROJECT: "defaultProject", PROJECTS_CACHE: "projectsCache", -} as const; +} as const /** * DOM Element IDs @@ -37,7 +37,7 @@ export const ELEMENT_IDS = { SUPERMEMORY_SAVE_BUTTON: "supermemory-save-button", SAVE_TWEET_ELEMENT: "supermemory-save-tweet-element", CHATGPT_INPUT_BAR_ELEMENT: "supermemory-chatgpt-input-bar-element", -} as const; +} as const /** * UI Configuration @@ -47,7 +47,7 @@ export const UI_CONFIG = { TOAST_DURATION: 3000, // milliseconds RATE_LIMIT_BASE_WAIT: 60000, // 1 minute PAGINATION_DELAY: 1000, // 1 second between requests -} as const; +} as const /** * Supported Domains @@ -56,7 +56,7 @@ export const DOMAINS = { TWITTER: ["x.com", "twitter.com"], CHATGPT: ["chatgpt.com", "chat.openai.com"], SUPERMEMORY: ["localhost", "supermemory.ai", "app.supermemory.ai"], -} as const; +} as const /** * Container Tags @@ -64,7 +64,7 @@ export const DOMAINS = { export const CONTAINER_TAGS = { TWITTER_BOOKMARKS: "sm_project_twitter_bookmarks", DEFAULT_PROJECT: "sm_project_default", -} as const; +} as const /** * Message Types for extension communication @@ -76,13 +76,13 @@ export const MESSAGE_TYPES = { IMPORT_UPDATE: "import-update", IMPORT_DONE: "import-done", GET_RELATED_MEMORIES: "getRelatedMemories", -} as const; +} as const export const CONTEXT_MENU_IDS = { SAVE_TO_SUPERMEMORY: "save-to-supermemory", -} as const; +} as const export const CSS_CLASSES = { TOAST_STYLES_ID: "supermemory-toast-styles", SPINNER_STYLES_ID: "supermemory-spinner-styles", -} as const; +} as const diff --git a/apps/browser-extension/utils/twitter-auth.ts b/apps/browser-extension/utils/twitter-auth.ts index 699c1d4c..8734d144 100644 --- a/apps/browser-extension/utils/twitter-auth.ts +++ b/apps/browser-extension/utils/twitter-auth.ts @@ -4,9 +4,9 @@ */ export interface TwitterAuthTokens { - cookie: string; - csrf: string; - auth: string; + cookie: string + csrf: string + auth: string } /** @@ -16,41 +16,41 @@ export interface TwitterAuthTokens { */ export function captureTwitterTokens( details: chrome.webRequest.WebRequestDetails & { - requestHeaders?: chrome.webRequest.HttpHeader[]; + requestHeaders?: chrome.webRequest.HttpHeader[] }, ): boolean { if (!(details.url.includes("x.com") || details.url.includes("twitter.com"))) { - return false; + return false } const authHeader = details.requestHeaders?.find( (header) => header.name.toLowerCase() === "authorization", - ); + ) const cookieHeader = details.requestHeaders?.find( (header) => header.name.toLowerCase() === "cookie", - ); + ) const csrfHeader = details.requestHeaders?.find( (header) => header.name.toLowerCase() === "x-csrf-token", - ); + ) if (authHeader?.value && cookieHeader?.value && csrfHeader?.value) { chrome.storage.session.get(["tokens_logged"], (result) => { if (!result.tokens_logged) { - console.log("Twitter auth tokens captured successfully"); - chrome.storage.session.set({ tokens_logged: true }); + console.log("Twitter auth tokens captured successfully") + chrome.storage.session.set({ tokens_logged: true }) } - }); + }) chrome.storage.session.set({ cookie: cookieHeader.value, csrf: csrfHeader.value, auth: authHeader.value, - }); + }) - return true; + return true } - return false; + return false } /** @@ -58,17 +58,17 @@ export function captureTwitterTokens( * @returns Promise resolving to tokens or null if not available */ export async function getTwitterTokens(): Promise { - const result = await chrome.storage.session.get(["cookie", "csrf", "auth"]); + const result = await chrome.storage.session.get(["cookie", "csrf", "auth"]) if (!result.cookie || !result.csrf || !result.auth) { - return null; + return null } return { cookie: result.cookie, csrf: result.csrf, auth: result.auth, - }; + } } /** @@ -77,16 +77,16 @@ export async function getTwitterTokens(): Promise { * @returns Headers object ready for fetch requests */ export function createTwitterAPIHeaders(tokens: TwitterAuthTokens): Headers { - const headers = new Headers(); - headers.append("Cookie", tokens.cookie); - headers.append("X-Csrf-Token", tokens.csrf); - headers.append("Authorization", tokens.auth); - headers.append("Content-Type", "application/json"); + const headers = new Headers() + headers.append("Cookie", tokens.cookie) + headers.append("X-Csrf-Token", tokens.csrf) + headers.append("Authorization", tokens.auth) + headers.append("Content-Type", "application/json") headers.append( "User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", - ); - headers.append("Accept", "*/*"); - headers.append("Accept-Language", "en-US,en;q=0.9"); - return headers; + ) + headers.append("Accept", "*/*") + headers.append("Accept-Language", "en-US,en;q=0.9") + return headers } diff --git a/apps/browser-extension/utils/twitter-import.ts b/apps/browser-extension/utils/twitter-import.ts index ce7b1faf..c516e094 100644 --- a/apps/browser-extension/utils/twitter-import.ts +++ b/apps/browser-extension/utils/twitter-import.ts @@ -3,8 +3,8 @@ * Handles the import process for Twitter bookmarks */ -import { saveTweet } from "./api"; -import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth"; +import { saveTweet } from "./api" +import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth" import { BOOKMARKS_URL, buildRequestVariables, @@ -13,37 +13,37 @@ import { type Tweet, type TwitterAPIResponse, tweetToMarkdown, -} from "./twitter-utils"; +} from "./twitter-utils" -export type ImportProgressCallback = (message: string) => Promise; +export type ImportProgressCallback = (message: string) => Promise -export type ImportCompleteCallback = (totalImported: number) => Promise; +export type ImportCompleteCallback = (totalImported: number) => Promise export interface TwitterImportConfig { - onProgress: ImportProgressCallback; - onComplete: ImportCompleteCallback; - onError: (error: Error) => Promise; + onProgress: ImportProgressCallback + onComplete: ImportCompleteCallback + onError: (error: Error) => Promise } /** * Rate limiting configuration */ class RateLimiter { - private waitTime = 60000; // Start with 1 minute + private waitTime = 60000 // Start with 1 minute async handleRateLimit(onProgress: ImportProgressCallback): Promise { - const waitTimeInSeconds = this.waitTime / 1000; + const waitTimeInSeconds = this.waitTime / 1000 await onProgress( `Rate limit reached. Waiting for ${waitTimeInSeconds} seconds before retrying...`, - ); + ) - await new Promise((resolve) => setTimeout(resolve, this.waitTime)); - this.waitTime *= 2; // Exponential backoff + await new Promise((resolve) => setTimeout(resolve, this.waitTime)) + this.waitTime *= 2 // Exponential backoff } reset(): void { - this.waitTime = 60000; + this.waitTime = 60000 } } @@ -61,14 +61,14 @@ async function importTweet(tweetMd: string, tweet: Tweet): Promise { created_at: tweet.created_at, likes: tweet.favorite_count, retweets: tweet.retweet_count || 0, - }; + } try { - await saveTweet(tweetMd, metadata); + await saveTweet(tweetMd, metadata) } catch (error) { throw new Error( `Failed to save tweet: ${error instanceof Error ? error.message : "Unknown error"}`, - ); + ) } } @@ -76,8 +76,8 @@ async function importTweet(tweetMd: string, tweet: Tweet): Promise { * Main class for handling Twitter bookmarks import */ export class TwitterImporter { - private importInProgress = false; - private rateLimiter = new RateLimiter(); + private importInProgress = false + private rateLimiter = new RateLimiter() constructor(private config: TwitterImportConfig) {} @@ -87,18 +87,18 @@ export class TwitterImporter { */ async startImport(): Promise { if (this.importInProgress) { - throw new Error("Import already in progress"); + throw new Error("Import already in progress") } - this.importInProgress = true; + this.importInProgress = true try { - await this.batchImportAll("", 0); - this.rateLimiter.reset(); + await this.batchImportAll("", 0) + this.rateLimiter.reset() } catch (error) { - await this.config.onError(error as Error); + await this.config.onError(error as Error) } finally { - this.importInProgress = false; + this.importInProgress = false } } @@ -109,81 +109,84 @@ export class TwitterImporter { */ private async batchImportAll(cursor = "", totalImported = 0): Promise { try { + // Use a local variable to track imported count + let importedCount = totalImported + // Get authentication tokens - const tokens = await getTwitterTokens(); + const tokens = await getTwitterTokens() if (!tokens) { await this.config.onProgress( "Please visit Twitter/X first to capture authentication tokens", - ); - return; + ) + return } // Create headers for API request - const headers = createTwitterAPIHeaders(tokens); + const headers = createTwitterAPIHeaders(tokens) // Build API request with pagination - const variables = buildRequestVariables(cursor); + const variables = buildRequestVariables(cursor) const urlWithCursor = cursor ? `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}` - : BOOKMARKS_URL; + : BOOKMARKS_URL - console.log("Making Twitter API request to:", urlWithCursor); - console.log("Request headers:", Object.fromEntries(headers.entries())); + console.log("Making Twitter API request to:", urlWithCursor) + console.log("Request headers:", Object.fromEntries(headers.entries())) const response = await fetch(urlWithCursor, { method: "GET", headers, redirect: "follow", - }); + }) if (!response.ok) { - const errorText = await response.text(); - console.error(`Twitter API Error ${response.status}:`, errorText); + const errorText = await response.text() + console.error(`Twitter API Error ${response.status}:`, errorText) if (response.status === 429) { - await this.rateLimiter.handleRateLimit(this.config.onProgress); - return this.batchImportAll(cursor, totalImported); + await this.rateLimiter.handleRateLimit(this.config.onProgress) + return this.batchImportAll(cursor, totalImported) } throw new Error( `Failed to fetch data: ${response.status} - ${errorText}`, - ); + ) } - const data: TwitterAPIResponse = await response.json(); - const tweets = getAllTweets(data); + const data: TwitterAPIResponse = await response.json() + const tweets = getAllTweets(data) - console.log("Tweets:", tweets); + console.log("Tweets:", tweets) // Process each tweet for (const tweet of tweets) { try { - const tweetMd = tweetToMarkdown(tweet); - await importTweet(tweetMd, tweet); - totalImported++; - await this.config.onProgress(`Imported ${totalImported} tweets`); + const tweetMd = tweetToMarkdown(tweet) + await importTweet(tweetMd, tweet) + importedCount++ + await this.config.onProgress(`Imported ${importedCount} tweets`) } catch (error) { - console.error("Error importing tweet:", error); + console.error("Error importing tweet:", error) // Continue with next tweet } } // Handle pagination const instructions = - data.data?.bookmark_timeline_v2?.timeline?.instructions; - const nextCursor = extractNextCursor(instructions || []); + data.data?.bookmark_timeline_v2?.timeline?.instructions + const nextCursor = extractNextCursor(instructions || []) - console.log("Next cursor:", nextCursor); - console.log("Tweets length:", tweets.length); + console.log("Next cursor:", nextCursor) + console.log("Tweets length:", tweets.length) if (nextCursor && tweets.length > 0) { - await new Promise((resolve) => setTimeout(resolve, 1000)); // Rate limiting - await this.batchImportAll(nextCursor, totalImported); + await new Promise((resolve) => setTimeout(resolve, 1000)) // Rate limiting + await this.batchImportAll(nextCursor, importedCount) } else { - await this.config.onComplete(totalImported); + await this.config.onComplete(importedCount) } } catch (error) { - console.error("Batch import error:", error); - await this.config.onError(error as Error); + console.error("Batch import error:", error) + await this.config.onError(error as Error) } } } diff --git a/apps/browser-extension/utils/twitter-utils.ts b/apps/browser-extension/utils/twitter-utils.ts index 2b42fafa..7a7b86db 100644 --- a/apps/browser-extension/utils/twitter-utils.ts +++ b/apps/browser-extension/utils/twitter-utils.ts @@ -1,120 +1,120 @@ // Twitter API data structures and transformation utilities interface TwitterAPITweet { - __typename?: string; + __typename?: string legacy: { - lang?: string; - favorite_count: number; - created_at: string; - display_text_range?: [number, number]; + lang?: string + favorite_count: number + created_at: string + display_text_range?: [number, number] entities?: { - hashtags?: Array<{ indices: [number, number]; text: string }>; + hashtags?: Array<{ indices: [number, number]; text: string }> urls?: Array<{ - display_url: string; - expanded_url: string; - indices: [number, number]; - url: string; - }>; + display_url: string + expanded_url: string + indices: [number, number] + url: string + }> user_mentions?: Array<{ - id_str: string; - indices: [number, number]; - name: string; - screen_name: string; - }>; - symbols?: Array<{ indices: [number, number]; text: string }>; - media?: MediaEntity[]; - }; - id_str: string; - full_text: string; - reply_count?: number; - retweet_count?: number; - quote_count?: number; - }; + id_str: string + indices: [number, number] + name: string + screen_name: string + }> + symbols?: Array<{ indices: [number, number]; text: string }> + media?: MediaEntity[] + } + id_str: string + full_text: string + reply_count?: number + retweet_count?: number + quote_count?: number + } core?: { user_results?: { result?: { legacy?: { - id_str: string; - name: string; - profile_image_url_https: string; - screen_name: string; - verified: boolean; - }; - is_blue_verified?: boolean; - }; - }; - }; + id_str: string + name: string + profile_image_url_https: string + screen_name: string + verified: boolean + } + is_blue_verified?: boolean + } + } + } } interface MediaEntity { - type: string; - media_url_https: string; + type: string + media_url_https: string sizes?: { large?: { - w: number; - h: number; - }; - }; + w: number + h: number + } + } video_info?: { variants?: Array<{ - url: string; - }>; - duration_millis?: number; - }; + url: string + }> + duration_millis?: number + } } export interface Tweet { - __typename?: string; - lang?: string; - favorite_count: number; - created_at: string; - display_text_range?: [number, number]; + __typename?: string + lang?: string + favorite_count: number + created_at: string + display_text_range?: [number, number] entities: { hashtags: Array<{ - indices: [number, number]; - text: string; - }>; + indices: [number, number] + text: string + }> urls?: Array<{ - display_url: string; - expanded_url: string; - indices: [number, number]; - url: string; - }>; + display_url: string + expanded_url: string + indices: [number, number] + url: string + }> user_mentions: Array<{ - id_str: string; - indices: [number, number]; - name: string; - screen_name: string; - }>; + id_str: string + indices: [number, number] + name: string + screen_name: string + }> symbols: Array<{ - indices: [number, number]; - text: string; - }>; - }; - id_str: string; - text: string; + indices: [number, number] + text: string + }> + } + id_str: string + text: string user: { - id_str: string; - name: string; - profile_image_url_https: string; - screen_name: string; - verified: boolean; - is_blue_verified?: boolean; - }; - conversation_count: number; + id_str: string + name: string + profile_image_url_https: string + screen_name: string + verified: boolean + is_blue_verified?: boolean + } + conversation_count: number photos?: Array<{ - url: string; - width: number; - height: number; - }>; + url: string + width: number + height: number + }> videos?: Array<{ - url: string; - thumbnail_url: string; - duration: number; - }>; - retweet_count?: number; - quote_count?: number; - reply_count?: number; + url: string + thumbnail_url: string + duration: number + }> + retweet_count?: number + quote_count?: number + reply_count?: number } export interface TwitterAPIResponse { @@ -122,16 +122,16 @@ export interface TwitterAPIResponse { bookmark_timeline_v2: { timeline: { instructions: Array<{ - type: string; + type: string entries?: Array<{ - entryId: string; - sortIndex: string; - content: Record; - }>; - }>; - }; - }; - }; + entryId: string + sortIndex: string + content: Record + }> + }> + } + } + } } // Twitter API features configuration @@ -165,9 +165,9 @@ export const TWITTER_API_FEATURES = { articles_preview_enabled: true, rweb_video_timestamps_enabled: true, verified_phone_label_enabled: true, -}; +} -export const BOOKMARKS_URL = `https://x.com/i/api/graphql/xLjCVTqYWz8CGSprLU349w/Bookmarks?features=${encodeURIComponent(JSON.stringify(TWITTER_API_FEATURES))}`; +export const BOOKMARKS_URL = `https://x.com/i/api/graphql/xLjCVTqYWz8CGSprLU349w/Bookmarks?features=${encodeURIComponent(JSON.stringify(TWITTER_API_FEATURES))}` /** * Transform raw Twitter API response data into standardized Tweet format @@ -177,29 +177,29 @@ export function transformTweetData( ): Tweet | null { try { const content = input.content as { - itemContent?: { tweet_results?: { result?: unknown } }; - }; - const tweetData = content?.itemContent?.tweet_results?.result; + itemContent?: { tweet_results?: { result?: unknown } } + } + const tweetData = content?.itemContent?.tweet_results?.result if (!tweetData) { - return null; + return null } - const tweet = tweetData as TwitterAPITweet; + const tweet = tweetData as TwitterAPITweet if (!tweet.legacy) { - return null; + return null } // Handle media entities - const media = (tweet.legacy.entities?.media as MediaEntity[]) || []; + const media = (tweet.legacy.entities?.media as MediaEntity[]) || [] const photos = media .filter((m) => m.type === "photo") .map((m) => ({ url: m.media_url_https, width: m.sizes?.large?.w || 0, height: m.sizes?.large?.h || 0, - })); + })) const videos = media .filter((m) => m.type === "video") @@ -207,7 +207,7 @@ export function transformTweetData( url: m.video_info?.variants?.[0]?.url || "", thumbnail_url: m.media_url_https, duration: m.video_info?.duration_millis || 0, - })); + })) const transformed: Tweet = { __typename: tweet.__typename, @@ -239,20 +239,20 @@ export function transformTweetData( retweet_count: tweet.legacy.retweet_count || 0, quote_count: tweet.legacy.quote_count || 0, reply_count: tweet.legacy.reply_count || 0, - }; + } if (photos.length > 0) { - transformed.photos = photos; + transformed.photos = photos } if (videos.length > 0) { - transformed.videos = videos; + transformed.videos = videos } - return transformed; + return transformed } catch (error) { - console.error("Error transforming tweet data:", error); - return null; + console.error("Error transforming tweet data:", error) + return null } } @@ -260,29 +260,29 @@ export function transformTweetData( * Extract all tweets from Twitter API response */ export function getAllTweets(data: TwitterAPIResponse): Tweet[] { - const tweets: Tweet[] = []; + const tweets: Tweet[] = [] try { const instructions = - data.data?.bookmark_timeline_v2?.timeline?.instructions || []; + data.data?.bookmark_timeline_v2?.timeline?.instructions || [] for (const instruction of instructions) { if (instruction.type === "TimelineAddEntries" && instruction.entries) { for (const entry of instruction.entries) { if (entry.entryId.startsWith("tweet-")) { - const tweet = transformTweetData(entry); + const tweet = transformTweetData(entry) if (tweet) { - tweets.push(tweet); + tweets.push(tweet) } } } } } } catch (error) { - console.error("Error extracting tweets:", error); + console.error("Error extracting tweets:", error) } - return tweets; + return tweets } /** @@ -295,83 +295,83 @@ export function extractNextCursor( for (const instruction of instructions) { if (instruction.type === "TimelineAddEntries" && instruction.entries) { const entries = instruction.entries as Array<{ - entryId: string; - content?: { value?: string }; - }>; + entryId: string + content?: { value?: string } + }> for (const entry of entries) { if (entry.entryId.startsWith("cursor-bottom-")) { - return entry.content?.value || null; + return entry.content?.value || null } } } } } catch (error) { - console.error("Error extracting cursor:", error); + console.error("Error extracting cursor:", error) } - return null; + return null } /** * Convert Tweet object to markdown format for storage */ export function tweetToMarkdown(tweet: Tweet): string { - const username = tweet.user?.screen_name || "unknown"; - const displayName = tweet.user?.name || "Unknown User"; - const date = new Date(tweet.created_at).toLocaleDateString(); - const time = new Date(tweet.created_at).toLocaleTimeString(); + const username = tweet.user?.screen_name || "unknown" + const displayName = tweet.user?.name || "Unknown User" + const date = new Date(tweet.created_at).toLocaleDateString() + const time = new Date(tweet.created_at).toLocaleTimeString() - let markdown = `# Tweet by @${username} (${displayName})\n\n`; - markdown += `**Date:** ${date} ${time}\n`; - markdown += `**Likes:** ${tweet.favorite_count} | **Retweets:** ${tweet.retweet_count || 0} | **Replies:** ${tweet.reply_count || 0}\n\n`; + let markdown = `# Tweet by @${username} (${displayName})\n\n` + markdown += `**Date:** ${date} ${time}\n` + markdown += `**Likes:** ${tweet.favorite_count} | **Retweets:** ${tweet.retweet_count || 0} | **Replies:** ${tweet.reply_count || 0}\n\n` // Add tweet text - markdown += `${tweet.text}\n\n`; + markdown += `${tweet.text}\n\n` // Add media if present if (tweet.photos && tweet.photos.length > 0) { - markdown += `**Images:**\n`; + markdown += "**Images:**\n" tweet.photos.forEach((photo, index) => { - markdown += `![Image ${index + 1}](${photo.url})\n`; - }); - markdown += "\n"; + markdown += `![Image ${index + 1}](${photo.url})\n` + }) + markdown += "\n" } if (tweet.videos && tweet.videos.length > 0) { - markdown += `**Videos:**\n`; + markdown += "**Videos:**\n" tweet.videos.forEach((video, index) => { - markdown += `[Video ${index + 1}](${video.url})\n`; - }); - markdown += "\n"; + markdown += `[Video ${index + 1}](${video.url})\n` + }) + markdown += "\n" } // Add hashtags and mentions if (tweet.entities.hashtags.length > 0) { - markdown += `**Hashtags:** ${tweet.entities.hashtags.map((h) => `#${h.text}`).join(", ")}\n`; + markdown += `**Hashtags:** ${tweet.entities.hashtags.map((h) => `#${h.text}`).join(", ")}\n` } if (tweet.entities.user_mentions.length > 0) { - markdown += `**Mentions:** ${tweet.entities.user_mentions.map((m) => `@${m.screen_name}`).join(", ")}\n`; + markdown += `**Mentions:** ${tweet.entities.user_mentions.map((m) => `@${m.screen_name}`).join(", ")}\n` } // Add raw data for reference - markdown += `\n---\n
\nRaw Tweet Data\n\n\`\`\`json\n${JSON.stringify(tweet, null, 2)}\n\`\`\`\n
`; + markdown += `\n---\n
\nRaw Tweet Data\n\n\`\`\`json\n${JSON.stringify(tweet, null, 2)}\n\`\`\`\n
` - return markdown; + return markdown } /** * Build Twitter API request variables for pagination */ -export function buildRequestVariables(cursor?: string, count: number = 100) { +export function buildRequestVariables(cursor?: string, count = 100) { const variables = { count, includePromotedContent: false, - }; - - if (cursor) { - (variables as Record).cursor = cursor; } - return variables; + if (cursor) { + ;(variables as Record).cursor = cursor + } + + return variables } diff --git a/apps/browser-extension/utils/types.ts b/apps/browser-extension/utils/types.ts index 2fafda50..2d0981c8 100644 --- a/apps/browser-extension/utils/types.ts +++ b/apps/browser-extension/utils/types.ts @@ -5,99 +5,99 @@ /** * Toast states for UI feedback */ -export type ToastState = "loading" | "success" | "error"; +export type ToastState = "loading" | "success" | "error" /** * Message types for extension communication */ export interface ExtensionMessage { - action?: string; - type?: string; - data?: unknown; - state?: ToastState; - importedMessage?: string; - totalImported?: number; + action?: string + type?: string + data?: unknown + state?: ToastState + importedMessage?: string + totalImported?: number } /** * Memory data structure for saving content */ export interface MemoryData { - html: string; - highlightedText?: string; - url?: string; + html: string + highlightedText?: string + url?: string } /** * Supermemory API payload for storing memories */ export interface MemoryPayload { - containerTags: string[]; - content: string; + containerTags: string[] + content: string metadata: { - sm_source: string; - [key: string]: unknown; - }; + sm_source: string + [key: string]: unknown + } } /** * Twitter-specific memory metadata */ export interface TwitterMemoryMetadata { - sm_source: "twitter_bookmarks"; - tweet_id: string; - author: string; - created_at: string; - likes: number; - retweets: number; + sm_source: "twitter_bookmarks" + tweet_id: string + author: string + created_at: string + likes: number + retweets: number } /** * Storage data structure for Chrome storage */ export interface StorageData { - bearerToken?: string; + bearerToken?: string twitterAuth?: { - cookie: string; - csrf: string; - auth: string; - }; - tokens_logged?: boolean; - cookie?: string; - csrf?: string; - auth?: string; - defaultProject?: Project; + cookie: string + csrf: string + auth: string + } + tokens_logged?: boolean + cookie?: string + csrf?: string + auth?: string + defaultProject?: Project projectsCache?: { - projects: Project[]; - timestamp: number; - }; + projects: Project[] + timestamp: number + } } /** * Context menu click info */ export interface ContextMenuClickInfo { - menuItemId: string | number; - editable?: boolean; - frameId?: number; - frameUrl?: string; - linkUrl?: string; - mediaType?: string; - pageUrl?: string; - parentMenuItemId?: string | number; - selectionText?: string; - srcUrl?: string; - targetElementId?: number; - wasChecked?: boolean; + menuItemId: string | number + editable?: boolean + frameId?: number + frameUrl?: string + linkUrl?: string + mediaType?: string + pageUrl?: string + parentMenuItemId?: string | number + selectionText?: string + srcUrl?: string + targetElementId?: number + wasChecked?: boolean } /** * API Response types */ export interface APIResponse { - success: boolean; - data?: T; - error?: string; + success: boolean + data?: T + error?: string } /** @@ -109,41 +109,41 @@ export class ExtensionError extends Error { public code?: string, public statusCode?: number, ) { - super(message); - this.name = "ExtensionError"; + super(message) + this.name = "ExtensionError" } } export class TwitterAPIError extends ExtensionError { constructor(message: string, statusCode?: number) { - super(message, "TWITTER_API_ERROR", statusCode); - this.name = "TwitterAPIError"; + super(message, "TWITTER_API_ERROR", statusCode) + this.name = "TwitterAPIError" } } export class SupermemoryAPIError extends ExtensionError { constructor(message: string, statusCode?: number) { - super(message, "SUPERMEMORY_API_ERROR", statusCode); - this.name = "SupermemoryAPIError"; + super(message, "SUPERMEMORY_API_ERROR", statusCode) + this.name = "SupermemoryAPIError" } } export class AuthenticationError extends ExtensionError { - constructor(message: string = "Authentication required") { - super(message, "AUTH_ERROR"); - this.name = "AuthenticationError"; + constructor(message = "Authentication required") { + super(message, "AUTH_ERROR") + this.name = "AuthenticationError" } } export interface Project { - id: string; - name: string; - containerTag: string; - createdAt: string; - updatedAt: string; - documentCount: number; + id: string + name: string + containerTag: string + createdAt: string + updatedAt: string + documentCount: number } export interface ProjectsResponse { - projects: Project[]; + projects: Project[] } diff --git a/apps/browser-extension/utils/ui-components.ts b/apps/browser-extension/utils/ui-components.ts index 2cc5e5a0..7b4376c2 100644 --- a/apps/browser-extension/utils/ui-components.ts +++ b/apps/browser-extension/utils/ui-components.ts @@ -3,8 +3,8 @@ * Reusable UI components for the browser extension */ -import { API_ENDPOINTS, ELEMENT_IDS, UI_CONFIG } from "./constants"; -import type { ToastState } from "./types"; +import { API_ENDPOINTS, ELEMENT_IDS, UI_CONFIG } from "./constants" +import type { ToastState } from "./types" /** * Creates a toast notification element @@ -12,8 +12,8 @@ import type { ToastState } from "./types"; * @returns HTMLElement - The toast element */ export function createToast(state: ToastState): HTMLElement { - const toast = document.createElement("div"); - toast.id = ELEMENT_IDS.SUPERMEMORY_TOAST; + const toast = document.createElement("div") + toast.id = ELEMENT_IDS.SUPERMEMORY_TOAST toast.style.cssText = ` position: fixed; @@ -32,12 +32,12 @@ export function createToast(state: ToastState): HTMLElement { min-width: 200px; max-width: 300px; animation: slideIn 0.3s ease-out; - `; + ` // Add keyframe animations and fonts if not already present if (!document.getElementById("supermemory-toast-styles")) { - const style = document.createElement("style"); - style.id = "supermemory-toast-styles"; + const style = document.createElement("style") + style.id = "supermemory-toast-styles" style.textContent = ` @font-face { font-family: 'Space Grotesk'; @@ -86,15 +86,15 @@ export function createToast(state: ToastState): HTMLElement { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } - `; - document.head.appendChild(style); + ` + document.head.appendChild(style) } - const icon = document.createElement("div"); - icon.style.cssText = "width: 20px; height: 20px; flex-shrink: 0;"; + const icon = document.createElement("div") + icon.style.cssText = "width: 20px; height: 20px; flex-shrink: 0;" - const text = document.createElement("span"); - text.style.fontWeight = "500"; + const text = document.createElement("span") + text.style.fontWeight = "500" // Configure toast based on state switch (state) { @@ -110,16 +110,16 @@ export function createToast(state: ToastState): HTMLElement { - `; - icon.style.animation = "spin 1s linear infinite"; - text.textContent = "Adding to Memory..."; - break; + ` + icon.style.animation = "spin 1s linear infinite" + text.textContent = "Adding to Memory..." + break case "success": { - const iconUrl = browser.runtime.getURL("/icon-16.png"); - icon.innerHTML = `Success`; - text.textContent = "Added to Memory"; - break; + const iconUrl = browser.runtime.getURL("/icon-16.png") + icon.innerHTML = `Success` + text.textContent = "Added to Memory" + break } case "error": @@ -129,15 +129,15 @@ export function createToast(state: ToastState): HTMLElement { - `; - text.textContent = "Failed to save memory / Make sure you are logged in"; - break; + ` + text.textContent = "Failed to save memory / Make sure you are logged in" + break } - toast.appendChild(icon); - toast.appendChild(text); + toast.appendChild(icon) + toast.appendChild(text) - return toast; + return toast } /** @@ -146,8 +146,8 @@ export function createToast(state: ToastState): HTMLElement { * @returns HTMLElement - The button element */ export function createTwitterImportButton(onClick: () => void): HTMLElement { - const button = document.createElement("div"); - button.id = ELEMENT_IDS.TWITTER_IMPORT_BUTTON; + const button = document.createElement("div") + button.id = ELEMENT_IDS.TWITTER_IMPORT_BUTTON button.style.cssText = ` position: fixed; top: 10px; @@ -163,26 +163,26 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement { align-items: center; gap: 8px; transition: all 0.2s ease; - `; + ` - const iconUrl = browser.runtime.getURL("/light-mode-icon.png"); + const iconUrl = browser.runtime.getURL("/light-mode-icon.png") button.innerHTML = ` Save to Memory - `; + ` button.addEventListener("mouseenter", () => { - button.style.transform = "scale(1.05)"; - button.style.boxShadow = "0 4px 12px rgba(29, 155, 240, 0.4)"; - }); + button.style.transform = "scale(1.05)" + button.style.boxShadow = "0 4px 12px rgba(29, 155, 240, 0.4)" + }) button.addEventListener("mouseleave", () => { - button.style.transform = "scale(1)"; - button.style.boxShadow = "0 2px 8px rgba(29, 155, 240, 0.3)"; - }); + button.style.transform = "scale(1)" + button.style.boxShadow = "0 2px 8px rgba(29, 155, 240, 0.3)" + }) - button.addEventListener("click", onClick); + button.addEventListener("click", onClick) - return button; + return button } /** @@ -197,7 +197,7 @@ export function createTwitterImportUI( onImport: () => void, isAuthenticated: boolean, ): HTMLElement { - const container = document.createElement("div"); + const container = document.createElement("div") container.style.cssText = ` position: fixed; top: 20px; @@ -211,7 +211,7 @@ export function createTwitterImportUI( max-width: 400px; border: 1px solid #e1e5e9; font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - `; + ` container.innerHTML = `
@@ -261,25 +261,25 @@ export function createTwitterImportUI( 100% { transform: rotate(360deg); } } - `; + ` // Add event listeners - const closeBtn = container.querySelector(`#${ELEMENT_IDS.TWITTER_CLOSE_BTN}`); - closeBtn?.addEventListener("click", onClose); + const closeBtn = container.querySelector(`#${ELEMENT_IDS.TWITTER_CLOSE_BTN}`) + closeBtn?.addEventListener("click", onClose) const importBtn = container.querySelector( `#${ELEMENT_IDS.TWITTER_IMPORT_BTN}`, - ); - importBtn?.addEventListener("click", onImport); + ) + importBtn?.addEventListener("click", onImport) const signinBtn = container.querySelector( `#${ELEMENT_IDS.TWITTER_SIGNIN_BTN}`, - ); + ) signinBtn?.addEventListener("click", () => { - browser.tabs.create({ url: `${API_ENDPOINTS.SUPERMEMORY_WEB}/login` }); - }); + browser.tabs.create({ url: `${API_ENDPOINTS.SUPERMEMORY_WEB}/login` }) + }) - return container; + return container } /** @@ -288,7 +288,7 @@ export function createTwitterImportUI( * @returns HTMLElement - The save button element */ export function createSaveTweetElement(onClick: () => void): HTMLElement { - const iconButton = document.createElement("div"); + const iconButton = document.createElement("div") iconButton.style.cssText = ` display: inline-flex; align-items: flex-end; @@ -301,36 +301,36 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement { margin-right: 10px; margin-bottom: 2px; z-index: 1000; - `; + ` // Check body background color to determine which icon to use - const bodyStyle = window.getComputedStyle(document.body); - const backgroundColor = bodyStyle.backgroundColor; - const isLightMode = backgroundColor === "rgb(255, 255, 255)"; + const bodyStyle = window.getComputedStyle(document.body) + const backgroundColor = bodyStyle.backgroundColor + const isLightMode = backgroundColor === "rgb(255, 255, 255)" const iconFileName = isLightMode ? "/light-mode-icon.png" - : "/dark-mode-icon.png"; - const iconUrl = browser.runtime.getURL(iconFileName); + : "/dark-mode-icon.png" + const iconUrl = browser.runtime.getURL(iconFileName) iconButton.innerHTML = ` Save to Memory - `; + ` iconButton.addEventListener("mouseenter", () => { - iconButton.style.opacity = "1"; - }); + iconButton.style.opacity = "1" + }) iconButton.addEventListener("mouseleave", () => { - iconButton.style.opacity = "0.7"; - }); + iconButton.style.opacity = "0.7" + }) iconButton.addEventListener("click", (event) => { - event.stopPropagation(); - event.preventDefault(); - onClick(); - }); + event.stopPropagation() + event.preventDefault() + onClick() + }) - return iconButton; + return iconButton } /** @@ -339,7 +339,7 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement { * @returns HTMLElement - The save button element */ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement { - const iconButton = document.createElement("div"); + const iconButton = document.createElement("div") iconButton.style.cssText = ` display: inline-flex; align-items: center; @@ -349,31 +349,31 @@ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement { cursor: pointer; transition: opacity 0.2s ease; border-radius: 50%; - `; + ` // Use appropriate icon based on theme - const isDark = DOMUtils.isDarkMode(); - const iconFileName = isDark ? "/dark-mode-icon.png" : "/light-mode-icon.png"; - const iconUrl = browser.runtime.getURL(iconFileName); + const isDark = DOMUtils.isDarkMode() + const iconFileName = isDark ? "/dark-mode-icon.png" : "/light-mode-icon.png" + const iconUrl = browser.runtime.getURL(iconFileName) iconButton.innerHTML = ` Save to Memory - `; + ` iconButton.addEventListener("mouseenter", () => { - iconButton.style.opacity = "0.8"; - }); + iconButton.style.opacity = "0.8" + }) iconButton.addEventListener("mouseleave", () => { - iconButton.style.opacity = "1"; - }); + iconButton.style.opacity = "1" + }) iconButton.addEventListener("click", (event) => { - event.stopPropagation(); - event.preventDefault(); - onClick(); - }); + event.stopPropagation() + event.preventDefault() + onClick() + }) - return iconButton; + return iconButton } /** @@ -386,7 +386,7 @@ export const DOMUtils = { * @returns boolean */ isOnDomain(domains: readonly string[]): boolean { - return domains.includes(window.location.hostname); + return domains.includes(window.location.hostname) }, /** @@ -394,9 +394,9 @@ export const DOMUtils = { * @returns boolean - true if dark mode, false if light mode */ isDarkMode(): boolean { - const htmlElement = document.documentElement; - const style = htmlElement.getAttribute("style"); - return style?.includes("color-scheme: dark") || false; + const htmlElement = document.documentElement + const style = htmlElement.getAttribute("style") + return style?.includes("color-scheme: dark") || false }, /** @@ -405,7 +405,7 @@ export const DOMUtils = { * @returns boolean */ elementExists(id: string): boolean { - return !!document.getElementById(id); + return !!document.getElementById(id) }, /** @@ -413,8 +413,8 @@ export const DOMUtils = { * @param id - Element ID to remove */ removeElement(id: string): void { - const element = document.getElementById(id); - element?.remove(); + const element = document.getElementById(id) + element?.remove() }, /** @@ -430,28 +430,28 @@ export const DOMUtils = { // Remove all existing toasts more aggressively const existingToasts = document.querySelectorAll( `#${ELEMENT_IDS.SUPERMEMORY_TOAST}`, - ); + ) existingToasts.forEach((toast) => { - toast.remove(); - }); + toast.remove() + }) - const toast = createToast(state); - document.body.appendChild(toast); + const toast = createToast(state) + document.body.appendChild(toast) // Auto-dismiss for success and error states if (state === "success" || state === "error") { setTimeout(() => { if (document.body.contains(toast)) { - toast.style.animation = "fadeOut 0.3s ease-out"; + toast.style.animation = "fadeOut 0.3s ease-out" setTimeout(() => { if (document.body.contains(toast)) { - toast.remove(); + toast.remove() } - }, 300); + }, 300) } - }, duration); + }, duration) } - return toast; + return toast }, -}; +} diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts index 382d62b1..0cae799c 100644 --- a/apps/browser-extension/wxt.config.ts +++ b/apps/browser-extension/wxt.config.ts @@ -1,10 +1,10 @@ -import { defineConfig } from "wxt"; +import { defineConfig } from "wxt" // See https://wxt.dev/api/config.html export default defineConfig({ modules: ["@wxt-dev/module-react"], manifest: { - name: "Supermemory", + name: "supermemory", homepage_url: "https://supermemory.ai", permissions: [ "contextMenus", @@ -25,10 +25,10 @@ export default defineConfig({ web_accessible_resources: [ { resources: [ - "icon-16.png", - "light-mode-icon.png", + "icon-16.png", + "light-mode-icon.png", "dark-mode-icon.png", - "fonts/*.ttf" + "fonts/*.ttf", ], matches: [""], }, @@ -37,4 +37,4 @@ export default defineConfig({ webExt: { disabled: true, }, -}); +})