diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts index 81c67394..1066e6b0 100644 --- a/apps/browser-extension/entrypoints/background.ts +++ b/apps/browser-extension/entrypoints/background.ts @@ -1,164 +1,197 @@ -import { TwitterImporter, type TwitterImportConfig } from '../utils/twitter-import'; -import { captureTwitterTokens } from '../utils/twitter-auth'; -import { CONTEXT_MENU_IDS, MESSAGE_TYPES, CONTAINER_TAGS } from '../utils/constants'; -import type { ExtensionMessage, MemoryPayload } from '../utils/types'; -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"; +import { + type TwitterImportConfig, + TwitterImporter, +} from "../utils/twitter-import"; +import type { + ExtensionMessage, + MemoryData, + MemoryPayload, +} from "../utils/types"; + +interface SearchResponse { + results: Array<{ + chunks: Array<{ + content: string; + }>; + }>; +} export default defineBackground(() => { - let twitterImporter: TwitterImporter | null = null; + let twitterImporter: TwitterImporter | null = null; - browser.runtime.onInstalled.addListener(() => { - browser.contextMenus.create({ - id: CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY, - title: 'Save to Supermemory', - contexts: ['selection', 'page', 'link'], - }); - }); + browser.runtime.onInstalled.addListener(() => { + browser.contextMenus.create({ + id: CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY, + title: "Save to Supermemory", + contexts: ["selection", "page", "link"], + }); + }); + // Intercept Twitter requests to capture authentication headers. + browser.webRequest.onBeforeSendHeaders.addListener( + (details) => { + captureTwitterTokens(details); + return {}; + }, + { urls: ["*://x.com/*", "*://twitter.com/*"] }, + ["requestHeaders", "extraHeaders"], + ); - // Intercept Twitter requests to capture authentication headers. - browser.webRequest.onBeforeSendHeaders.addListener( - (details) => { - captureTwitterTokens(details); - return {}; - }, - { urls: ['*://x.com/*', '*://twitter.com/*'] }, - ['requestHeaders', 'extraHeaders'] - ); + // Handle context menu clicks. + browser.contextMenus.onClicked.addListener(async (info, tab) => { + if (info.menuItemId === CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY) { + if (tab?.id) { + try { + await browser.tabs.sendMessage(tab.id, { + action: MESSAGE_TYPES.SAVE_MEMORY, + }); + } catch (error) { + console.error("Failed to send message to content script:", error); + } + } + } + }); - // Handle context menu clicks. - browser.contextMenus.onClicked.addListener(async (info, tab) => { - if (info.menuItemId === CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY) { - if (tab?.id) { - try { - await browser.tabs.sendMessage(tab.id, { - action: MESSAGE_TYPES.SAVE_MEMORY, - }); - } catch (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 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 + */ + const sendImportDoneMessage = async (totalImported: number) => { + 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, + }); + } + }; - /** - * Send import completion message - */ - const sendImportDoneMessage = async (totalImported: number) => { - 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 + */ + const saveMemoryToSupermemory = async ( + data: MemoryData, + ): Promise<{ success: boolean; data?: unknown; error?: string }> => { + try { + let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT; + try { + const defaultProject = await getDefaultProject(); + if (defaultProject?.containerTag) { + containerTag = defaultProject.containerTag; + } + } catch (error) { + console.warn("Failed to get default project, using fallback:", error); + } - /** - * Save memory to Supermemory API - */ - const saveMemoryToSupermemory = async (data: any): Promise<{ success: boolean; data?: any; error?: string }> => { - try { - let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT; - try { - const defaultProject = await getDefaultProject(); - if (defaultProject?.containerTag) { - containerTag = defaultProject.containerTag; - } - } catch (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 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 }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }; + } + }; - 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 content = (responseData as SearchResponse).results[0].chunks[0] + .content; + console.log("Content:", content); + return { success: true, data: content }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }; + } + }; - const getRelatedMemories = async (data: any): Promise<{ success: boolean; data?: any; error?: string }> => { - try { - const responseData = await searchMemories(data); - const content = responseData.results[0].chunks[0].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 + */ + browser.runtime.onMessage.addListener( + (message: ExtensionMessage, _sender, sendResponse) => { + // Handle Twitter import request + if (message.type === MESSAGE_TYPES.BATCH_IMPORT_ALL) { + const importConfig: TwitterImportConfig = { + onProgress: sendMessageToCurrentTab, + onComplete: sendImportDoneMessage, + onError: async (error: Error) => { + await sendMessageToCurrentTab(`Error: ${error.message}`); + }, + }; - /** - * Handle extension messages - */ - browser.runtime.onMessage.addListener((message: ExtensionMessage, _sender, sendResponse) => { - // Handle Twitter import request - if (message.type === MESSAGE_TYPES.BATCH_IMPORT_ALL) { - const importConfig: TwitterImportConfig = { - onProgress: sendMessageToCurrentTab, - onComplete: sendImportDoneMessage, - onError: async (error: Error) => { - 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 () => { + try { + const result = await saveMemoryToSupermemory( + message.data as MemoryData, + ); + sendResponse(result); + } catch (error) { + sendResponse({ + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }); + } + })(); + return true; + } - // Handle regular memory save request - if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { - (async () => { - try { - const result = await saveMemoryToSupermemory(message.data); - sendResponse(result); - } catch (error) { - sendResponse({ - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }); - } - })(); - return true; - } - - if (message.action === MESSAGE_TYPES.GET_RELATED_MEMORIES) { - (async () => { - try { - const result = await getRelatedMemories(message.data); - sendResponse(result); - } catch (error) { - sendResponse({ - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }); - } - })(); - return true; - } - }); -}); \ No newline at end of file + if (message.action === MESSAGE_TYPES.GET_RELATED_MEMORIES) { + (async () => { + try { + 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; + } + }, + ); +}); diff --git a/apps/browser-extension/entrypoints/content.ts b/apps/browser-extension/entrypoints/content.ts index c4cb512b..c62855f1 100644 --- a/apps/browser-extension/entrypoints/content.ts +++ b/apps/browser-extension/entrypoints/content.ts @@ -1,165 +1,164 @@ +import { DOMAINS, ELEMENT_IDS, MESSAGE_TYPES } from "../utils/constants"; import { - createTwitterImportButton, - createTwitterImportUI, - createSaveTweetElement, - createChatGPTInputBarElement, - DOMUtils, -} from '../utils/ui-components'; -import { DOMAINS, ELEMENT_IDS, MESSAGE_TYPES } from '../utils/constants'; + createChatGPTInputBarElement, + createSaveTweetElement, + createTwitterImportButton, + createTwitterImportUI, + DOMUtils, +} from "../utils/ui-components"; export default defineContentScript({ - matches: [''], - main() { - let twitterImportUI: HTMLElement | null = null; - let isTwitterImportOpen = false; + matches: [""], + main() { + 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); - } else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { - await saveMemory(); - } else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { - updateTwitterImportUI(message); - } else if (message.type === MESSAGE_TYPES.IMPORT_DONE) { - updateTwitterImportUI(message); - } - }); + browser.runtime.onMessage.addListener(async (message) => { + if (message.action === MESSAGE_TYPES.SHOW_TOAST) { + DOMUtils.showToast(message.state); + } else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { + await saveMemory(); + } else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { + updateTwitterImportUI(message); + } else if (message.type === MESSAGE_TYPES.IMPORT_DONE) { + updateTwitterImportUI(message); + } + }); - const observeForMemoriesDialog = () => { - const observer = new MutationObserver(() => { - if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { - addSupermemoryButtonToMemoriesDialog(); - addSaveChatGPTElementBeforeComposerBtn(); - } - if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - addTwitterImportButton(); - //addSaveTweetElement(); - } - }); + const observeForMemoriesDialog = () => { + const observer = new MutationObserver(() => { + if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { + addSupermemoryButtonToMemoriesDialog(); + addSaveChatGPTElementBeforeComposerBtn(); + } + if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) { + addTwitterImportButton(); + //addSaveTweetElement(); + } + }); - observer.observe(document.body, { - childList: true, - subtree: true, - }); + observer.observe(document.body, { + childList: true, + subtree: true, + }); - if ( - window.location.hostname === 'chatgpt.com' || - window.location.hostname === 'chat.openai.com' - ) { - addSupermemoryButtonToMemoriesDialog(); - addSaveChatGPTElementBeforeComposerBtn(); - } - if ( - window.location.hostname === 'x.com' || - window.location.hostname === 'twitter.com' - ) { - addTwitterImportButton(); - //addSaveTweetElement(); - } - }; + if ( + window.location.hostname === "chatgpt.com" || + window.location.hostname === "chat.openai.com" + ) { + addSupermemoryButtonToMemoriesDialog(); + addSaveChatGPTElementBeforeComposerBtn(); + } + if ( + window.location.hostname === "x.com" || + window.location.hostname === "twitter.com" + ) { + addTwitterImportButton(); + //addSaveTweetElement(); + } + }; - if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - setTimeout(() => { - addTwitterImportButton(); // Wait 2 seconds for page to load - //addSaveTweetElement(); - }, 2000); - } + if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) { + setTimeout(() => { + addTwitterImportButton(); // Wait 2 seconds for page to load + //addSaveTweetElement(); + }, 2000); + } - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', observeForMemoriesDialog); - } else { - observeForMemoriesDialog(); - } + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", observeForMemoriesDialog); + } else { + observeForMemoriesDialog(); + } - async function saveMemory() { - try { - DOMUtils.showToast('loading'); + async function saveMemory() { + try { + 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, - data: { - html, - highlightedText, - url, - }, - }); + const response = await browser.runtime.sendMessage({ + action: MESSAGE_TYPES.SAVE_MEMORY, + data: { + html, + highlightedText, + url, + }, + }); - console.log('Response from enxtension:', response); - if (response.success) { - DOMUtils.showToast('success'); - } else { - DOMUtils.showToast('error'); - } - } catch (error) { - console.error('Error saving memory:', error); - DOMUtils.showToast('error'); - } - } + console.log("Response from enxtension:", response); + if (response.success) { + DOMUtils.showToast("success"); + } else { + DOMUtils.showToast("error"); + } + } catch (error) { + console.error("Error saving memory:", error); + DOMUtils.showToast("error"); + } + } - async function getRelatedMemories() { - try { - const userQuery = - document.getElementById('prompt-textarea')?.textContent || ''; + async function getRelatedMemories() { + try { + const userQuery = + document.getElementById("prompt-textarea")?.textContent || ""; - const response = await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.GET_RELATED_MEMORIES, - data: userQuery, - }); + 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'); - if (promptElement) { - const currentContent = promptElement.innerHTML; - promptElement.innerHTML = currentContent + '
' + "Supermemories: " + response.data; - } - } - } catch (error) { - console.error('Error getting related memories:', error); - } - } + if (response.success && response.data) { + const promptElement = document.getElementById("prompt-textarea"); + if (promptElement) { + const currentContent = promptElement.innerHTML; + promptElement.innerHTML = `${currentContent}
Supermemories: ${response.data}`; + } + } + } catch (error) { + console.error("Error getting related memories:", error); + } + } - function addSupermemoryButtonToMemoriesDialog() { - const dialogs = document.querySelectorAll('[role="dialog"]'); - let memoriesDialog: HTMLElement | null = null; + function addSupermemoryButtonToMemoriesDialog() { + const dialogs = document.querySelectorAll('[role="dialog"]'); + let memoriesDialog: HTMLElement | null = null; - for (const dialog of dialogs) { - const headerText = dialog.querySelector('h2'); - if (headerText && headerText.textContent?.includes('Saved memories')) { - memoriesDialog = dialog as HTMLElement; - break; - } - } + for (const dialog of dialogs) { + const headerText = dialog.querySelector("h2"); + if (headerText?.textContent?.includes("Saved memories")) { + 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; + const deleteAllContainer = memoriesDialog.querySelector( + ".mt-5.flex.justify-end", + ); + 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 = ` + supermemoryButton.innerHTML = `
Supermemory Save to Supermemory
`; - supermemoryButton.style.cssText = ` + supermemoryButton.style.cssText = ` background: #1C2026 !important; color: white !important; border: 1px solid #1C2026 !important; @@ -171,287 +170,287 @@ export default defineContentScript({ cursor: pointer !important; `; - supermemoryButton.addEventListener('mouseenter', () => { - supermemoryButton.style.backgroundColor = '#2B2E33'; - }); + supermemoryButton.addEventListener("mouseenter", () => { + supermemoryButton.style.backgroundColor = "#2B2E33"; + }); - supermemoryButton.addEventListener('mouseleave', () => { - supermemoryButton.style.backgroundColor = '#1C2026'; - }); + supermemoryButton.addEventListener("mouseleave", () => { + supermemoryButton.style.backgroundColor = "#1C2026"; + }); - supermemoryButton.addEventListener('click', async () => { - await saveMemoriesToSupermemory(); - }); + supermemoryButton.addEventListener("click", async () => { + await saveMemoriesToSupermemory(); + }); - deleteAllContainer.insertBefore( - supermemoryButton, - deleteAllContainer.firstChild - ); - } + deleteAllContainer.insertBefore( + supermemoryButton, + deleteAllContainer.firstChild, + ); + } - async function saveMemoriesToSupermemory() { - try { - DOMUtils.showToast('loading'); + async function saveMemoriesToSupermemory() { + try { + DOMUtils.showToast("loading"); - const memoriesTable = document.querySelector( - '[role="dialog"] table tbody' - ); - if (!memoriesTable) { - DOMUtils.showToast('error'); - return; - } + const memoriesTable = document.querySelector( + '[role="dialog"] table tbody', + ); + if (!memoriesTable) { + 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'); - if (memoryCell && memoryCell.textContent) { - memories.push(memoryCell.textContent.trim()); - } - }); + memoryRows.forEach((row) => { + const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap"); + if (memoryCell?.textContent) { + memories.push(memoryCell.textContent.trim()); + } + }); - console.log('Memories:', memories); + console.log("Memories:", memories); - if (memories.length === 0) { - DOMUtils.showToast('error'); - return; - } + if (memories.length === 0) { + 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, - }, - }); + const response = await browser.runtime.sendMessage({ + action: "saveMemory", + data: { + html: combinedContent, + }, + }); - if (response.success) { - DOMUtils.showToast('success'); - } else { - DOMUtils.showToast('error'); - } - } catch (error) { - console.error('Error saving memories to Supermemory:', error); - DOMUtils.showToast('error'); - } - } + if (response.success) { + DOMUtils.showToast("success"); + } else { + DOMUtils.showToast("error"); + } + } catch (error) { + console.error("Error saving memories to Supermemory:", error); + DOMUtils.showToast("error"); + } + } - function addTwitterImportButton() { - if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - return; - } + function addTwitterImportButton() { + if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { + return; + } - if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { - return; - } + if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { + return; + } - const button = createTwitterImportButton(() => { - showTwitterImportUI(); - }); + const button = createTwitterImportButton(() => { + showTwitterImportUI(); + }); - document.body.appendChild(button); - } + document.body.appendChild(button); + } - function showTwitterImportUI() { - if (twitterImportUI) { - twitterImportUI.remove(); - } + function showTwitterImportUI() { + if (twitterImportUI) { + twitterImportUI.remove(); + } - isTwitterImportOpen = true; + isTwitterImportOpen = true; - // Check if user is authenticated - browser.storage.local.get(['bearerToken'], ({ bearerToken }) => { - const isAuthenticated = !!bearerToken; + // Check if user is authenticated + browser.storage.local.get(["bearerToken"], ({ bearerToken }) => { + const isAuthenticated = !!bearerToken; - twitterImportUI = createTwitterImportUI( - hideTwitterImportUI, - async () => { - try { - await browser.runtime.sendMessage({ - type: MESSAGE_TYPES.BATCH_IMPORT_ALL, - }); - } catch (error) { - console.error('Error starting import:', error); - } - }, - isAuthenticated - ); + twitterImportUI = createTwitterImportUI( + hideTwitterImportUI, + async () => { + try { + await browser.runtime.sendMessage({ + type: MESSAGE_TYPES.BATCH_IMPORT_ALL, + }); + } catch (error) { + console.error("Error starting import:", error); + } + }, + isAuthenticated, + ); - document.body.appendChild(twitterImportUI); - }); - } + document.body.appendChild(twitterImportUI); + }); + } - function hideTwitterImportUI() { - if (twitterImportUI) { - twitterImportUI.remove(); - twitterImportUI = null; - } - isTwitterImportOpen = false; - } + function hideTwitterImportUI() { + if (twitterImportUI) { + twitterImportUI.remove(); + twitterImportUI = null; + } + isTwitterImportOpen = false; + } - function updateTwitterImportUI(message: any) { - if (!isTwitterImportOpen || !twitterImportUI) return; + function updateTwitterImportUI(message: { + type: string; + importedMessage?: string; + totalImported?: number; + }) { + 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) { - statusDiv.innerHTML = ` + if (message.type === "import-update") { + if (statusDiv) { + statusDiv.innerHTML = `
${message.importedMessage}
`; - } - if (button) { - (button as HTMLButtonElement).disabled = true; - (button as HTMLButtonElement).textContent = 'Importing...'; - } - } + } + if (button) { + (button as HTMLButtonElement).disabled = true; + (button as HTMLButtonElement).textContent = "Importing..."; + } + } - if (message.type === 'import-done') { - if (statusDiv) { - statusDiv.innerHTML = ` + if (message.type === "import-done") { + if (statusDiv) { + statusDiv.innerHTML = `
Successfully imported ${message.totalImported} tweets!
`; - } + } - setTimeout(() => { - hideTwitterImportUI(); - }, 3000); - } - } + setTimeout(() => { + hideTwitterImportUI(); + }, 3000); + } + } - function addSaveChatGPTElementBeforeComposerBtn() { - if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { - return; - } + function addSaveChatGPTElementBeforeComposerBtn() { + if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { + 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; - } + composerButtons.forEach((button) => { + if (button.hasAttribute("data-supermemory-icon-added-before")) { + 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; - for (const sibling of parentSiblings) { - if ( - sibling.getAttribute('data-testid') === - 'composer-speech-button-container' - ) { - hasSpeechButtonSibling = true; - break; - } - } + let hasSpeechButtonSibling = false; + for (const sibling of parentSiblings) { + if ( + sibling.getAttribute("data-testid") === + "composer-speech-button-container" + ) { + 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; - } + const existingIcon = grandParent.querySelector( + `#${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer`, + ); + if (existingIcon) { + button.setAttribute("data-supermemory-icon-added-before", "true"); + return; + } - const saveChatGPTElement = createChatGPTInputBarElement(async () => { - await getRelatedMemories(); - }); + const saveChatGPTElement = createChatGPTInputBarElement(async () => { + 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; - } + // TODO: Add Tweet Capture Functionality + function _addSaveTweetElement() { + if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { + return; + } - const targetDivs = document.querySelectorAll( - 'div.css-175oi2r.r-18u37iz.r-1h0z5md.r-1wron08' - ); + const targetDivs = document.querySelectorAll( + "div.css-175oi2r.r-18u37iz.r-1h0z5md.r-1wron08", + ); - targetDivs.forEach((targetDiv) => { - if (targetDiv.hasAttribute('data-supermemory-icon-added')) { - return; - } + targetDivs.forEach((targetDiv) => { + if (targetDiv.hasAttribute("data-supermemory-icon-added")) { + return; + } - const previousElement = targetDiv.previousElementSibling; - if ( - previousElement && - previousElement.id && - previousElement.id.startsWith(ELEMENT_IDS.SAVE_TWEET_ELEMENT) - ) { - targetDiv.setAttribute('data-supermemory-icon-added', 'true'); - return; - } + const previousElement = targetDiv.previousElementSibling; + if (previousElement?.id?.startsWith(ELEMENT_IDS.SAVE_TWEET_ELEMENT)) { + targetDiv.setAttribute("data-supermemory-icon-added", "true"); + return; + } - const saveTweetElement = createSaveTweetElement(async () => { - await saveMemory(); - }); + const saveTweetElement = createSaveTweetElement(async () => { + 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) => { - if ( - (event.ctrlKey || event.metaKey) && - event.shiftKey && - event.key === 'm' - ) { - event.preventDefault(); - await saveMemory(); - } - }); + document.addEventListener("keydown", async (event) => { + if ( + (event.ctrlKey || event.metaKey) && + event.shiftKey && + event.key === "m" + ) { + event.preventDefault(); + await saveMemory(); + } + }); - window.addEventListener('message', (event) => { - if (event.source !== window) { - return; - } - const bearerToken = event.data.token; + window.addEventListener("message", (event) => { + if (event.source !== window) { + return; + } + const bearerToken = event.data.token; - if (bearerToken) { - if ( - !( - window.location.hostname === 'localhost' || - window.location.hostname === 'supermemory.ai' || - window.location.hostname === 'app.supermemory.ai' - ) - ) { - console.log( - 'Bearer token is only allowed to be used on localhost or supermemory.ai' - ); - return; - } + if (bearerToken) { + if ( + !( + window.location.hostname === "localhost" || + window.location.hostname === "supermemory.ai" || + window.location.hostname === "app.supermemory.ai" + ) + ) { + console.log( + "Bearer token is only allowed to be used on localhost or supermemory.ai", + ); + 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 eab4fe8f..dc8eb590 100644 --- a/apps/browser-extension/entrypoints/popup/App.css +++ b/apps/browser-extension/entrypoints/popup/App.css @@ -1,285 +1,698 @@ +/* Custom Font Definitions */ +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url('/fonts/SpaceGrotesk-Light.ttf') format('truetype'); +} + +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/SpaceGrotesk-Regular.ttf') format('truetype'); +} + +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/fonts/SpaceGrotesk-Medium.ttf') format('truetype'); +} + +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('/fonts/SpaceGrotesk-SemiBold.ttf') format('truetype'); +} + +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('/fonts/SpaceGrotesk-Bold.ttf') format('truetype'); +} + .popup-container { - width: 320px; - padding: 0; - font-family: - -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background: #ffffff; - border-radius: 8px; + width: 320px; + padding: 0; + font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: #ffffff; + border-radius: 8px; + position: relative; + overflow: hidden; } .header { - display: flex; - align-items: center; - gap: 12px; - padding: 16px; - border-bottom: 1px solid #e5e7eb; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 16px; + border-bottom: 1px solid #e5e7eb; + position: relative; } .header .logo { - width: 32px; - height: 32px; - flex-shrink: 0; + width: 32px; + height: 32px; + flex-shrink: 0; } .header h1 { - margin: 0; - font-size: 18px; - font-weight: 600; - color: #000000; + margin: 0; + font-size: 18px; + font-weight: 600; + color: #000000; + flex: 1; +} + +.header-sign-out { + background: none; + border: none; + font-size: 16px; + cursor: pointer; + color: #6c757d; + padding: 4px; + border-radius: 4px; + transition: color 0.2s ease, background-color 0.2s ease; +} + +.header-sign-out:hover { + color: #000000; + background-color: #f1f3f4; } .content { - padding: 16px; + padding: 16px; } .status { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 16px; - font-size: 14px; - color: #000000; + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + font-size: 14px; + color: #000000; } .status-indicator { - width: 8px; - height: 8px; - border-radius: 50%; - flex-shrink: 0; + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; } .status-indicator.signed-in { - background-color: #000000; + background-color: #000000; } .status-indicator.signed-out { - background-color: #666666; + background-color: #666666; } .sign-out-btn { - width: 100%; - padding: 8px 16px; - background-color: #000000; - color: white; - border: none; - border-radius: 6px; - font-size: 14px; - font-weight: 500; - cursor: pointer; + width: 100%; + padding: 8px 16px; + background-color: #000000; + color: white; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; } .sign-out-btn:hover { - background-color: #333333; + background-color: #333333; } .instruction { - margin: 0; - font-size: 13px; - color: #666666; - line-height: 1.4; + margin: 0; + font-size: 13px; + color: #666666; + line-height: 1.4; +} + +.login-btn { + background: none; + border: none; + color: #1976d2; + cursor: pointer; + text-decoration: underline; + font-size: 13px; + padding: 0; +} + +.login-btn:hover { + color: #1565c0; +} + +.authenticated { + text-align: left; } -.authenticated, .unauthenticated { - text-align: left; + text-align: center; + padding: 8px 0; +} + +/* Login Screen Styles */ +.login-intro { + margin-bottom: 32px; +} + +.login-title { + margin: 0 0 16px 0; + font-size: 14px; + font-weight: 400; + color: #000000; + line-height: 1.3; +} + +.features-list { + list-style: none; + padding: 0; + margin: 0; + text-align: left; +} + +.features-list li { + padding: 6px 0; + font-size: 14px; + color: #000000; + position: relative; + padding-left: 20px; +} + +.features-list li::before { + content: "•"; + position: absolute; + left: 0; + color: #000000; + font-weight: bold; +} + +.login-actions { + margin-top: 32px; +} + +.login-help { + margin: 0 0 16px 0; + font-size: 14px; + color: #6c757d; +} + +.help-link { + background: none; + border: none; + color: #4285f4; + cursor: pointer; + text-decoration: underline; + font-size: 14px; + padding: 0; +} + +.help-link:hover { + color: #1a73e8; +} + +.login-primary-btn { + width: 100%; + padding: 12px 24px; + background-color: #374151; + color: white; + border: none; + border-radius: 24px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.login-primary-btn:hover:not(:disabled) { + background-color: #1f2937; +} + +.login-primary-btn:disabled { + background-color: #9e9e9e; + cursor: not-allowed; +} + +/* Tab Navigation Styles */ +.tab-navigation { + display: flex; + background-color: #f1f3f4; + border-radius: 8px; + padding: 4px; + margin-bottom: 16px; +} + +.tab-btn { + flex: 1; + padding: 8px 16px; + background: transparent; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + color: #6c757d; + cursor: pointer; + transition: all 0.2s ease; + outline: none; + box-shadow: none; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.tab-btn:focus { + outline: none; + box-shadow: none; + border: none; +} + +.tab-btn:active { + outline: none; + box-shadow: none; +} + +.tab-btn.active { + background-color: #ffffff; + color: #000000; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.tab-btn:hover:not(.active) { + color: #374151; +} + +/* Tab Content */ +.tab-content { + display: flex; + flex-direction: column; + gap: 16px; + min-height: 200px; +} + +/* Save Action at Bottom */ +.save-action { + margin-top: auto; + padding-top: 16px; +} + +/* Import Actions */ +.import-actions { + display: flex; + flex-direction: column; + gap: 16px; +} + +.import-item { + display: flex; + flex-direction: column; + gap: 8px; +} + +.import-instructions { + margin: 0; + font-size: 12px; + color: #6c757d; + line-height: 1.3; + padding-left: 4px; +} + +/* Save Section Styles */ +.save-section { + margin-bottom: 16px; +} + +.current-page { + margin-bottom: 0; +} + +.page-info { + background-color: #f8f9fa; + padding: 12px; + border-radius: 6px; + border: 1px solid #e9ecef; +} + +.page-title { + margin: 0 0 4px 0; + font-size: 14px; + font-weight: 600; + color: #000000; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.page-url { + margin: 0; + font-size: 12px; + color: #6c757d; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.save-page-btn { + width: 100%; + padding: 12px 16px; + background-color: #1976d2; + color: white; + border: none; + border-radius: 6px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.save-page-btn:hover:not(:disabled) { + background-color: #1565c0; +} + +.save-page-btn:disabled { + background-color: #9e9e9e; + cursor: not-allowed; +} + +.secondary-actions { + margin-top: 16px; +} + +.secondary-btn { + width: 100%; + padding: 8px 12px; + background-color: white; + color: #6c757d; + border: 1px solid #e4e6eb; + border-radius: 6px; + font-size: 13px; + font-weight: 400; + cursor: pointer; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.secondary-btn:hover { + background-color: #f8f9fa; + color: #000000; } .actions { - display: flex; - flex-direction: column; - gap: 12px; + display: flex; + flex-direction: column; + gap: 12px; } .chatgpt-btn { - width: 100%; - padding: 12px 12px; - background-color: white; - color: black; - border: 1px solid #e4e6eb; - border-radius: 6px; - font-size: 14px; - font-weight: 500; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: background-color 0.2s ease; + width: 100%; + padding: 12px 12px; + background-color: white; + color: black; + border: 1px solid #e4e6eb; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s ease; } .chatgpt-btn:hover { - background-color: #f0f0f0; - border-color: #e4e6eb; + background-color: #f0f0f0; + border-color: #e4e6eb; } .chatgpt-logo { - width: 35px; - height: 20px; - flex-shrink: 0; + width: 18px; + height: 18px; + flex-shrink: 0; + margin-right: 8px; +} + +.twitter-btn { + width: 100%; + padding: 12px 12px; + background-color: white; + color: black; + border: 1px solid #e4e6eb; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s ease; + outline: none; + box-shadow: none; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.twitter-btn:hover { + background-color: #f0f0f0; + border-color: #e4e6eb; +} + +.twitter-btn:focus { + outline: none; + box-shadow: none; +} + +.twitter-logo { + width: 18px; + height: 18px; + flex-shrink: 0; + margin-right: 8px; } /* Project Selection Styles */ .project-section { - margin-bottom: 16px; - padding: 10px; - background-color: #f8f9fa; - border-radius: 3%; - border: 1px solid #e9ecef; + margin-bottom: 0; } -.project-header { - display: flex; - justify-content: space-between; - align-items: center; +.project-selector-btn { + width: 100%; + background: none; + border: none; + padding: 0; + cursor: pointer; + text-align: left; +} + +.project-selector-content { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px; + background-color: #f8f9fa; + border-radius: 8px; + border: 1px solid #e9ecef; + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +.project-selector-btn:hover .project-selector-content { + background-color: #e9ecef; + border-color: #ced4da; } .project-label { - font-size: 13px; - font-weight: 500; - color: #495057; + font-size: 14px; + font-weight: 500; + color: #495057; } -.project-change-btn { - padding: 4px 8px; - background-color: #ffffff; - color: #000000; - border: 1px solid #ced4da; - border-radius: 4px; - font-size: 12px; - cursor: pointer; - transition: background-color 0.2s ease; -} - -.project-change-btn:hover { - background-color: #f8f9fa; -} - -.project-current { - padding: 4px 0; -} - -.project-info { - display: flex; - justify-content: space-between; - align-items: center; +.project-value { + display: flex; + align-items: center; + gap: 8px; } .project-name { - font-size: 14px; - font-weight: 500; - color: #000000; - flex: 1; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; + font-size: 14px; + font-weight: 500; + color: #000000; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 120px; +} + +.project-arrow { + color: #6c757d; + flex-shrink: 0; + transition: transform 0.2s ease; +} + +.project-selector-btn:hover .project-arrow { + color: #495057; + transform: translateX(2px); } .project-count { - font-size: 12px; - color: #6c757d; - margin-left: 8px; + font-size: 12px; + color: #6c757d; + margin-left: 8px; } .project-none { - font-size: 14px; - color: #6c757d; - font-style: italic; + font-size: 14px; + color: #6c757d; + font-style: italic; } /* Project Selector Modal */ .project-selector { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #ffffff; - border-radius: 8px; - z-index: 100; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ffffff; + border-radius: 8px; + z-index: 1000; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; } .project-selector-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px; - border-bottom: 1px solid #e5e7eb; - font-size: 16px; - font-weight: 600; + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + border-bottom: 1px solid #e5e7eb; + font-size: 16px; + font-weight: 600; + color: #000000; + flex-shrink: 0; +} + +.project-header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.project-logout-btn { + background: none; + border: none; + font-size: 14px; + color: #6c757d; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + transition: color 0.2s ease, background-color 0.2s ease; + outline: none; +} + +.project-logout-btn:hover { + color: #dc3545; + background-color: #f8f9fa; +} + +.project-logout-btn:focus { + outline: none; } .project-close-btn { - background: none; - border: none; - font-size: 20px; - cursor: pointer; - color: #6c757d; - padding: 0; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; + background: none; + border: none; + font-size: 20px; + cursor: pointer; + color: #6c757d; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; } .project-close-btn:hover { - color: #000000; + color: #000000; } .project-loading { - padding: 32px 16px; - text-align: center; - color: #6c757d; - font-size: 14px; + padding: 32px 16px; + text-align: center; + color: #6c757d; + font-size: 14px; } .project-list { - max-height: 240px; - overflow-y: auto; + flex: 1; + overflow-y: auto; + min-height: 0; } .project-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 16px; - cursor: pointer; - transition: background-color 0.2s ease; - border-bottom: 1px solid #f1f3f4; + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + cursor: pointer; + transition: background-color 0.2s ease; + border-bottom: 1px solid #f1f3f4; + background: none; + border: none; + width: 100%; + text-align: left; } .project-item:hover { - background-color: #f8f9fa; + background-color: #f8f9fa; } .project-item:last-child { - border-bottom: none; + border-bottom: none; } .project-item.selected { - background-color: #e3f2fd; + background-color: #e3f2fd; } .project-item-info { - display: flex; - flex-direction: column; - flex: 1; - gap: 2px; + display: flex; + flex-direction: column; + flex: 1; + gap: 2px; } .project-item-name { - font-size: 14px; - font-weight: 500; - color: #000000; + font-size: 14px; + font-weight: 500; + color: #000000; + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; + line-height: 1.3; } .project-item-count { - font-size: 12px; - color: #6c757d; + font-size: 12px; + color: #6c757d; } .project-item-check { - color: #1976d2; - font-weight: bold; - font-size: 16px; + color: #1976d2; + font-weight: bold; + font-size: 16px; } diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx index 796a42be..6d8675fe 100644 --- a/apps/browser-extension/entrypoints/popup/App.tsx +++ b/apps/browser-extension/entrypoints/popup/App.tsx @@ -1,213 +1,412 @@ -import React, { useState, useEffect } from 'react'; -import './App.css'; -import { getProjects, getDefaultProject, setDefaultProject } from '../../utils/api'; -import { Project } from '../../utils/types'; +import { useEffect, useState } from "react"; +import "./App.css"; +import { + getDefaultProject, + getProjects, + setDefaultProject, +} 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 [defaultProject, setDefaultProjectState] = useState(null); - const [loadingProjects, setLoadingProjects] = useState(false); - const [showProjectSelector, setShowProjectSelector] = useState(false); + 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"); - useEffect(() => { - const checkAuthStatus = async () => { - try { - const result = await chrome.storage.local.get(['bearerToken']); - const isSignedIn = !!result.bearerToken; - setUserSignedIn(isSignedIn); - - if (isSignedIn) { - try { - const defaultProj = await getDefaultProject(); - setDefaultProjectState(defaultProj); - } catch (error) { - console.error('Error loading default project:', error); - } - } - } catch (error) { - console.error('Error checking auth status:', error); - setUserSignedIn(false); - } finally { - setLoading(false); - } - }; + useEffect(() => { + const checkAuthStatus = async () => { + try { + const result = await chrome.storage.local.get(["bearerToken"]); + const isSignedIn = !!result.bearerToken; + setUserSignedIn(isSignedIn); - checkAuthStatus(); - }, []); + if (isSignedIn) { + try { + const defaultProj = await getDefaultProject(); + setDefaultProjectState(defaultProj); + } catch (error) { + console.error("Error loading default project:", error); + } + } + } catch (error) { + console.error("Error checking auth status:", error); + setUserSignedIn(false); + } finally { + setLoading(false); + } + }; - const handleSignOut = async () => { - try { - await chrome.storage.local.remove(['bearerToken']); - setUserSignedIn(false); - } catch (error) { - console.error('Error signing out:', error); - } - }; + 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); + } + } catch (error) { + console.error("Error getting current tab:", error); + } + }; - const loadProjects = async () => { - setLoadingProjects(true); - try { - 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); - } - } catch (error) { - console.error('Error loading projects:', error); - } finally { - setLoadingProjects(false); - } - }; + checkAuthStatus(); + getCurrentTab(); + }, []); - const handleProjectSelect = async (project: Project) => { - try { - await setDefaultProject(project); - setDefaultProjectState(project); - setShowProjectSelector(false); - } catch (error) { - console.error('Error setting default project:', error); - } - }; + const loadProjects = async () => { + setLoadingProjects(true); + try { + 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); + } + } catch (error) { + console.error("Error loading projects:", error); + } finally { + setLoadingProjects(false); + } + }; - const handleShowProjectSelector = () => { - console.log('handleShowProjectSelector, projects.length:', projects.length); - if (projects.length === 0) { - loadProjects(); - } - setShowProjectSelector(true); - }; + const handleProjectSelect = async (project: Project) => { + try { + await setDefaultProject(project); + setDefaultProjectState(project); + setShowProjectSelector(false); + } catch (error) { + console.error("Error setting default project:", error); + } + }; - if (loading) { - return ( -
-
- Supermemory -

Supermemory

-
-
-
Loading...
-
-
- ); - } + const handleShowProjectSelector = () => { + console.log("handleShowProjectSelector, projects.length:", projects.length); + if (projects.length === 0) { + loadProjects(); + } + setShowProjectSelector(true); + }; - return ( -
-
- Supermemory -

Supermemory

-
-
- {userSignedIn ? ( -
- -
-
- Default Project: - -
-
- {defaultProject ? ( -
- {defaultProject.name} -
- ) : ( - No project selected - )} -
-
+ const handleSaveCurrentPage = async () => { + 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); + } finally { + setSaving(false); + } + }; - {showProjectSelector && ( -
-
- Select Default Project - -
- {loadingProjects ? ( -
Loading projects...
- ) : ( -
- {projects.map((project) => ( -
handleProjectSelect(project)} - > -
- {project.name} - {project.documentCount} docs -
- {defaultProject?.id === project.id && ( - - )} -
- ))} -
- )} -
- )} - -
- - -
-
- ) : ( - - )} -
-
- ); + const handleSignOut = async () => { + try { + await chrome.storage.local.remove(["bearerToken"]); + setUserSignedIn(false); + setDefaultProjectState(null); + setProjects([]); + } catch (error) { + console.error("Error signing out:", error); + } + }; + + if (loading) { + return ( +
+
+ Supermemory +

Supermemory

+
+
+
Loading...
+
+
+ ); + } + + return ( +
+
+ Supermemory + {userSignedIn && ( + + )} +
+
+ {userSignedIn ? ( +
+ {/* Tab Navigation */} +
+ + +
+ + {/* Tab Content */} + {activeTab === "save" ? ( +
+ {/* Current Page Info */} +
+
+

+ {currentTitle || "Current Page"} +

+

{currentUrl}

+
+
+ + {/* Project Selection */} +
+ +
+ + {/* Save Button at Bottom */} +
+ +
+
+ ) : ( +
+ {/* Import Actions */} +
+
+ +
+ +
+ +

+ Click on supermemory on top right to import bookmarks +

+
+
+
+ )} + + {showProjectSelector && ( +
+
+ Select the Project + +
+ {loadingProjects ? ( +
Loading projects...
+ ) : ( +
+ {projects.map((project) => ( + + ))} +
+ )} +
+ )} +
+ ) : ( +
+
+

+ Login to unlock all chrome extension features +

+ +
    +
  • Save any page to your supermemory
  • +
  • Import all your Twitter / X Bookmarks
  • +
  • Import your ChatGPT Memories
  • +
+
+ +
+

+ having trouble to login?{" "} + +

+ + +
+
+ )} +
+
+ ); } export default App; diff --git a/apps/browser-extension/entrypoints/popup/main.tsx b/apps/browser-extension/entrypoints/popup/main.tsx index cde361f0..e0f969f0 100644 --- a/apps/browser-extension/entrypoints/popup/main.tsx +++ b/apps/browser-extension/entrypoints/popup/main.tsx @@ -1,10 +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"; -ReactDOM.createRoot(document.getElementById('root')!).render( - - - , -); +const rootElement = document.getElementById("root"); +if (rootElement) { + ReactDOM.createRoot(rootElement).render( + + + , + ); +} diff --git a/apps/browser-extension/entrypoints/popup/style.css b/apps/browser-extension/entrypoints/popup/style.css index c354e896..ee057e19 100644 --- a/apps/browser-extension/entrypoints/popup/style.css +++ b/apps/browser-extension/entrypoints/popup/style.css @@ -1,69 +1,70 @@ :root { - font-family: 'Space Grotesk', Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; + font-family: + "Space Grotesk", Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; } a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; + font-weight: 500; + color: #646cff; + text-decoration: inherit; } a:hover { - color: #535bf2; + color: #535bf2; } body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; } h1 { - font-size: 3.2em; - line-height: 1.1; + font-size: 3.2em; + line-height: 1.1; } button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; } button:hover { - border-color: #646cff; + border-color: #646cff; } button:focus, button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; + outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } } diff --git a/apps/browser-extension/fonts/SpaceGrotesk-Bold.ttf b/apps/browser-extension/fonts/SpaceGrotesk-Bold.ttf new file mode 100644 index 00000000..8a8611a5 Binary files /dev/null and b/apps/browser-extension/fonts/SpaceGrotesk-Bold.ttf differ diff --git a/apps/browser-extension/fonts/SpaceGrotesk-Light.ttf b/apps/browser-extension/fonts/SpaceGrotesk-Light.ttf new file mode 100644 index 00000000..0f03f08b Binary files /dev/null and b/apps/browser-extension/fonts/SpaceGrotesk-Light.ttf differ diff --git a/apps/browser-extension/fonts/SpaceGrotesk-Medium.ttf b/apps/browser-extension/fonts/SpaceGrotesk-Medium.ttf new file mode 100644 index 00000000..e530cf83 Binary files /dev/null and b/apps/browser-extension/fonts/SpaceGrotesk-Medium.ttf differ diff --git a/apps/browser-extension/fonts/SpaceGrotesk-Regular.ttf b/apps/browser-extension/fonts/SpaceGrotesk-Regular.ttf new file mode 100644 index 00000000..8215f81e Binary files /dev/null and b/apps/browser-extension/fonts/SpaceGrotesk-Regular.ttf differ diff --git a/apps/browser-extension/fonts/SpaceGrotesk-SemiBold.ttf b/apps/browser-extension/fonts/SpaceGrotesk-SemiBold.ttf new file mode 100644 index 00000000..e05b9673 Binary files /dev/null and b/apps/browser-extension/fonts/SpaceGrotesk-SemiBold.ttf differ diff --git a/apps/browser-extension/fonts/SpaceGrotesk-VariableFont_wght.ttf b/apps/browser-extension/fonts/SpaceGrotesk-VariableFont_wght.ttf new file mode 100644 index 00000000..2c6cc59a Binary files /dev/null and b/apps/browser-extension/fonts/SpaceGrotesk-VariableFont_wght.ttf differ diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json index e35d35fb..4c9d1402 100644 --- a/apps/browser-extension/package.json +++ b/apps/browser-extension/package.json @@ -1,29 +1,29 @@ { - "name": "supermemory-browser-extension", - "description": "An extension for https://app.supermemory.ai - an AI hub for all your knowledge.", - "private": true, - "version": "0.0.1", - "type": "module", - "scripts": { - "dev": "wxt --port 3001", - "dev:firefox": "wxt -b firefox", - "build": "wxt build", - "build:firefox": "wxt build -b firefox", - "zip": "wxt zip", - "zip:firefox": "wxt zip -b firefox", - "compile": "tsc --noEmit", - "postinstall": "wxt prepare" - }, - "dependencies": { - "react": "^19.1.0", - "react-dom": "^19.1.0" - }, - "devDependencies": { - "@types/chrome": "^0.1.4", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.3", - "@wxt-dev/module-react": "^1.1.3", - "typescript": "^5.8.3", - "wxt": "^0.20.6" - } + "name": "supermemory-browser-extension", + "description": "An extension for https://app.supermemory.ai - an AI hub for all your knowledge.", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "wxt --port 3001", + "dev:firefox": "wxt -b firefox", + "build": "wxt build", + "build:firefox": "wxt build -b firefox", + "zip": "wxt zip", + "zip:firefox": "wxt zip -b firefox", + "compile": "tsc --noEmit", + "postinstall": "wxt prepare" + }, + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/chrome": "^0.1.4", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.3", + "@wxt-dev/module-react": "^1.1.3", + "typescript": "^5.8.3", + "wxt": "^0.20.6" + } } diff --git a/apps/browser-extension/public/logo-trademark.svg b/apps/browser-extension/public/logo-trademark.svg new file mode 100644 index 00000000..8407cbd4 --- /dev/null +++ b/apps/browser-extension/public/logo-trademark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/browser-extension/tsconfig.json b/apps/browser-extension/tsconfig.json index 4217f189..621fa129 100644 --- a/apps/browser-extension/tsconfig.json +++ b/apps/browser-extension/tsconfig.json @@ -1,8 +1,8 @@ { - "extends": "./.wxt/tsconfig.json", - "compilerOptions": { - "allowImportingTsExtensions": true, - "jsx": "react-jsx", - "types": ["chrome"] - } + "extends": "./.wxt/tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "types": ["chrome"] + } } diff --git a/apps/browser-extension/utils/api.ts b/apps/browser-extension/utils/api.ts index cbd41311..93356bf2 100644 --- a/apps/browser-extension/utils/api.ts +++ b/apps/browser-extension/utils/api.ts @@ -1,188 +1,199 @@ /** * API service for Supermemory browser extension */ -import { API_ENDPOINTS, STORAGE_KEYS } from './constants'; -import { - Project, - ProjectsResponse, - MemoryPayload, - SupermemoryAPIError, - AuthenticationError -} from './types'; +import { API_ENDPOINTS, STORAGE_KEYS } from "./constants"; +import { + AuthenticationError, + type MemoryPayload, + type Project, + type ProjectsResponse, + SupermemoryAPIError, +} 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]; - - if (!token) { - throw new AuthenticationError('Bearer token not found'); - } - - return 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"); + } + + return token; } /** * Make authenticated API request */ async function makeAuthenticatedRequest( - endpoint: string, - options: RequestInit = {} + endpoint: string, + options: RequestInit = {}, ): Promise { - const token = await getBearerToken(); - - const response = await fetch(`${API_ENDPOINTS.SUPERMEMORY_API}${endpoint}`, { - ...options, - credentials: 'omit', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - ...options.headers, - }, - }); + const token = await getBearerToken(); - if (!response.ok) { - if (response.status === 401) { - throw new AuthenticationError('Invalid or expired token'); - } - throw new SupermemoryAPIError( - `API request failed: ${response.statusText}`, - response.status - ); - } + const response = await fetch(`${API_ENDPOINTS.SUPERMEMORY_API}${endpoint}`, { + ...options, + credentials: "omit", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + ...options.headers, + }, + }); - return response.json(); + if (!response.ok) { + if (response.status === 401) { + throw new AuthenticationError("Invalid or expired token"); + } + throw new SupermemoryAPIError( + `API request failed: ${response.statusText}`, + response.status, + ); + } + + return response.json(); } /** * Fetch all projects from API */ export async function fetchProjects(): Promise { - try { - const response = await makeAuthenticatedRequest('/v3/projects'); - return response.projects; - } catch (error) { - console.error('Failed to fetch projects:', error); - throw error; - } + try { + const response = + await makeAuthenticatedRequest("/v3/projects"); + return response.projects; + } catch (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 { - if (useCache) { - try { - const cached = await chrome.storage.local.get([STORAGE_KEYS.PROJECTS_CACHE]); - const cachedData = cached[STORAGE_KEYS.PROJECTS_CACHE]; - - if (cachedData && cachedData.timestamp && cachedData.projects) { - // Cache for 5 minutes - const cacheAge = Date.now() - cachedData.timestamp; - if (cacheAge < 5 * 60 * 1000) { - return cachedData.projects; - } - } - } catch (error) { - console.warn('Failed to read projects cache:', error); - } - } +export async function getProjects( + useCache: boolean = true, +): Promise { + if (useCache) { + try { + const cached = await chrome.storage.local.get([ + STORAGE_KEYS.PROJECTS_CACHE, + ]); + const cachedData = cached[STORAGE_KEYS.PROJECTS_CACHE]; - // Fetch fresh data - const projects = await fetchProjects(); - - // Cache the results - try { - await chrome.storage.local.set({ - [STORAGE_KEYS.PROJECTS_CACHE]: { - projects, - timestamp: Date.now(), - }, - }); - } catch (error) { - console.warn('Failed to cache projects:', error); - } + if (cachedData?.timestamp && cachedData.projects) { + // Cache for 5 minutes + const cacheAge = Date.now() - cachedData.timestamp; + if (cacheAge < 5 * 60 * 1000) { + return cachedData.projects; + } + } + } catch (error) { + console.warn("Failed to read projects cache:", error); + } + } - return projects; + // Fetch fresh data + const projects = await fetchProjects(); + + // Cache the results + try { + await chrome.storage.local.set({ + [STORAGE_KEYS.PROJECTS_CACHE]: { + projects, + timestamp: Date.now(), + }, + }); + } catch (error) { + console.warn("Failed to cache projects:", error); + } + + return projects; } /** * Get default project from storage */ export async function getDefaultProject(): Promise { - try { - const result = await chrome.storage.local.get([STORAGE_KEYS.DEFAULT_PROJECT]); - return result[STORAGE_KEYS.DEFAULT_PROJECT] || null; - } catch (error) { - console.error('Failed to get default project:', error); - return null; - } + try { + const result = await chrome.storage.local.get([ + STORAGE_KEYS.DEFAULT_PROJECT, + ]); + return result[STORAGE_KEYS.DEFAULT_PROJECT] || null; + } catch (error) { + console.error("Failed to get default project:", error); + return null; + } } /** * Set default project in storage */ 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; - } + try { + await chrome.storage.local.set({ + [STORAGE_KEYS.DEFAULT_PROJECT]: project, + }); + } catch (error) { + console.error("Failed to set default project:", error); + throw error; + } } /** * Save memory to Supermemory API */ -export async function saveMemory(payload: MemoryPayload): Promise { - try { - const response = await makeAuthenticatedRequest('/v3/memories', { - method: 'POST', - body: JSON.stringify(payload), - }); - return response; - } catch (error) { - console.error('Failed to save memory:', error); - throw error; - } +export async function saveMemory(payload: MemoryPayload): Promise { + try { + const response = await makeAuthenticatedRequest("/v3/memories", { + method: "POST", + body: JSON.stringify(payload), + }); + return response; + } catch (error) { + console.error("Failed to save memory:", error); + throw error; + } } /** * Search memories using Supermemory API */ -export async function searchMemories(query: string): Promise { - try { - const response = await makeAuthenticatedRequest('/v3/search', { - method: 'POST', - body: JSON.stringify({ q: query }), - }); - return response; - } catch (error) { - console.error('Failed to search memories:', error); - throw error; - } +export async function searchMemories(query: string): Promise { + try { + const response = await makeAuthenticatedRequest("/v3/search", { + method: "POST", + body: JSON.stringify({ q: query }), + }); + return response; + } catch (error) { + console.error("Failed to search memories:", error); + throw error; + } } /** * Save tweet to Supermemory API (specific for Twitter imports) */ -export async function saveTweet(content: string, metadata: any, containerTag: string = 'sm_project_twitter_bookmarks'): Promise { - try { - const payload: MemoryPayload = { - containerTags: [containerTag], - content, - metadata, - }; - await saveMemory(payload); - } catch (error) { - if (error instanceof SupermemoryAPIError && error.statusCode === 409) { - // Skip if already exists (409 Conflict) - return; - } - throw error; - } -} \ No newline at end of file +export async function saveTweet( + content: string, + metadata: { sm_source: string; [key: string]: unknown }, + containerTag: string = "sm_project_twitter_bookmarks", +): Promise { + try { + const payload: MemoryPayload = { + containerTags: [containerTag], + content, + metadata, + }; + await saveMemory(payload); + } catch (error) { + if (error instanceof SupermemoryAPIError && error.statusCode === 409) { + // Skip if already exists (409 Conflict) + return; + } + throw error; + } +} diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts index db76c1d8..ffe39554 100644 --- a/apps/browser-extension/utils/constants.ts +++ b/apps/browser-extension/utils/constants.ts @@ -2,83 +2,87 @@ * API Endpoints */ export const API_ENDPOINTS = { - SUPERMEMORY_API: import.meta.env.PROD ? 'https://api.supermemory.ai' : 'http://localhost:8787', - SUPERMEMORY_WEB: import.meta.env.PROD ? 'https://app.supermemory.ai' : 'http://localhost:3000', + SUPERMEMORY_API: import.meta.env.PROD + ? "https://api.supermemory.ai" + : "http://localhost:8787", + SUPERMEMORY_WEB: import.meta.env.PROD + ? "https://app.supermemory.ai" + : "http://localhost:3000", } as const; /** * Storage Keys */ export const STORAGE_KEYS = { - BEARER_TOKEN: 'bearerToken', - TWITTER_AUTH: 'twitterAuth', - TOKENS_LOGGED: 'tokens_logged', - TWITTER_COOKIE: 'cookie', - TWITTER_CSRF: 'csrf', - TWITTER_AUTH_TOKEN: 'auth', - DEFAULT_PROJECT: 'defaultProject', - PROJECTS_CACHE: 'projectsCache', + BEARER_TOKEN: "bearerToken", + TWITTER_AUTH: "twitterAuth", + TOKENS_LOGGED: "tokens_logged", + TWITTER_COOKIE: "cookie", + TWITTER_CSRF: "csrf", + TWITTER_AUTH_TOKEN: "auth", + DEFAULT_PROJECT: "defaultProject", + PROJECTS_CACHE: "projectsCache", } as const; /** * DOM Element IDs */ export const ELEMENT_IDS = { - TWITTER_IMPORT_BUTTON: 'supermemory-twitter-import-button', - TWITTER_IMPORT_STATUS: 'twitter-import-status', - TWITTER_CLOSE_BTN: 'twitter-close-btn', - TWITTER_IMPORT_BTN: 'twitter-import-button', - TWITTER_SIGNIN_BTN: 'twitter-signin-btn', - SUPERMEMORY_TOAST: 'supermemory-toast', - SUPERMEMORY_SAVE_BUTTON: 'supermemory-save-button', - SAVE_TWEET_ELEMENT: 'supermemory-save-tweet-element', - CHATGPT_INPUT_BAR_ELEMENT: 'supermemory-chatgpt-input-bar-element', + TWITTER_IMPORT_BUTTON: "supermemory-twitter-import-button", + TWITTER_IMPORT_STATUS: "twitter-import-status", + TWITTER_CLOSE_BTN: "twitter-close-btn", + TWITTER_IMPORT_BTN: "twitter-import-button", + TWITTER_SIGNIN_BTN: "twitter-signin-btn", + SUPERMEMORY_TOAST: "supermemory-toast", + SUPERMEMORY_SAVE_BUTTON: "supermemory-save-button", + SAVE_TWEET_ELEMENT: "supermemory-save-tweet-element", + CHATGPT_INPUT_BAR_ELEMENT: "supermemory-chatgpt-input-bar-element", } as const; /** * UI Configuration */ export const UI_CONFIG = { - BUTTON_SHOW_DELAY: 2000, // milliseconds - TOAST_DURATION: 3000, // milliseconds - RATE_LIMIT_BASE_WAIT: 60000, // 1 minute - PAGINATION_DELAY: 1000, // 1 second between requests + BUTTON_SHOW_DELAY: 2000, // milliseconds + TOAST_DURATION: 3000, // milliseconds + RATE_LIMIT_BASE_WAIT: 60000, // 1 minute + PAGINATION_DELAY: 1000, // 1 second between requests } as const; /** * Supported Domains */ export const DOMAINS = { - TWITTER: ['x.com', 'twitter.com'], - CHATGPT: ['chatgpt.com', 'chat.openai.com'], - SUPERMEMORY: ['localhost', 'supermemory.ai', 'app.supermemory.ai'], + TWITTER: ["x.com", "twitter.com"], + CHATGPT: ["chatgpt.com", "chat.openai.com"], + SUPERMEMORY: ["localhost", "supermemory.ai", "app.supermemory.ai"], } as const; /** * Container Tags */ export const CONTAINER_TAGS = { - TWITTER_BOOKMARKS: 'sm_project_twitter_bookmarks', - DEFAULT_PROJECT: 'sm_project_default', + TWITTER_BOOKMARKS: "sm_project_twitter_bookmarks", + DEFAULT_PROJECT: "sm_project_default", } as const; /** * Message Types for extension communication */ export const MESSAGE_TYPES = { - SAVE_MEMORY: 'saveMemory', - SHOW_TOAST: 'showToast', - BATCH_IMPORT_ALL: 'batchImportAll', - IMPORT_UPDATE: 'import-update', - IMPORT_DONE: 'import-done', - GET_RELATED_MEMORIES: 'getRelatedMemories', + SAVE_MEMORY: "saveMemory", + SHOW_TOAST: "showToast", + BATCH_IMPORT_ALL: "batchImportAll", + IMPORT_UPDATE: "import-update", + IMPORT_DONE: "import-done", + GET_RELATED_MEMORIES: "getRelatedMemories", } as const; export const CONTEXT_MENU_IDS = { - SAVE_TO_SUPERMEMORY: 'save-to-supermemory', + SAVE_TO_SUPERMEMORY: "save-to-supermemory", } as const; export const CSS_CLASSES = { - TOAST_STYLES_ID: 'supermemory-toast-styles', - SPINNER_STYLES_ID: 'supermemory-spinner-styles', -} as const; \ No newline at end of file + TOAST_STYLES_ID: "supermemory-toast-styles", + SPINNER_STYLES_ID: "supermemory-spinner-styles", +} as const; diff --git a/apps/browser-extension/utils/twitter-auth.ts b/apps/browser-extension/utils/twitter-auth.ts index 4fa99542..699c1d4c 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; } /** @@ -14,39 +14,43 @@ export interface TwitterAuthTokens { * @param details - Web request details containing headers * @returns True if tokens were captured, false otherwise */ -export function captureTwitterTokens(details: any): boolean { - if (!(details.url.includes('x.com') || details.url.includes('twitter.com'))) { - return false; - } +export function captureTwitterTokens( + details: chrome.webRequest.WebRequestDetails & { + requestHeaders?: chrome.webRequest.HttpHeader[]; + }, +): boolean { + if (!(details.url.includes("x.com") || details.url.includes("twitter.com"))) { + return false; + } - const authHeader = details.requestHeaders?.find( - (header: any) => header.name.toLowerCase() === 'authorization' - ); - const cookieHeader = details.requestHeaders?.find( - (header: any) => header.name.toLowerCase() === 'cookie' - ); - const csrfHeader = details.requestHeaders?.find( - (header: any) => header.name.toLowerCase() === 'x-csrf-token' - ); + 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) { - browser.storage.session.get(['tokens_logged'], (result) => { - if (!result.tokens_logged) { - console.log('Twitter auth tokens captured successfully'); - browser.storage.session.set({ tokens_logged: true }); - } - }); + 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 }); + } + }); - browser.storage.session.set({ - cookie: cookieHeader.value, - csrf: csrfHeader.value, - auth: authHeader.value - }); + chrome.storage.session.set({ + cookie: cookieHeader.value, + csrf: csrfHeader.value, + auth: authHeader.value, + }); - return true; - } + return true; + } - return false; + return false; } /** @@ -54,17 +58,17 @@ export function captureTwitterTokens(details: any): boolean { * @returns Promise resolving to tokens or null if not available */ export async function getTwitterTokens(): Promise { - const result = await browser.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; - } + if (!result.cookie || !result.csrf || !result.auth) { + return null; + } - return { - cookie: result.cookie, - csrf: result.csrf, - auth: result.auth - }; + return { + cookie: result.cookie, + csrf: result.csrf, + auth: result.auth, + }; } /** @@ -73,13 +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'); - 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; -} \ No newline at end of file + 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; +} diff --git a/apps/browser-extension/utils/twitter-import.ts b/apps/browser-extension/utils/twitter-import.ts index 22794bfe..ce7b1faf 100644 --- a/apps/browser-extension/utils/twitter-import.ts +++ b/apps/browser-extension/utils/twitter-import.ts @@ -3,48 +3,48 @@ * Handles the import process for Twitter bookmarks */ -import { - BOOKMARKS_URL, - getAllTweets, - extractNextCursor, - tweetToMarkdown, - buildRequestVariables, - type TwitterAPIResponse, - type Tweet -} from './twitter-utils'; -import { getTwitterTokens, createTwitterAPIHeaders, type TwitterAuthTokens } from './twitter-auth'; -import { saveTweet } from './api'; +import { saveTweet } from "./api"; +import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth"; +import { + BOOKMARKS_URL, + buildRequestVariables, + extractNextCursor, + getAllTweets, + type Tweet, + type TwitterAPIResponse, + tweetToMarkdown, +} from "./twitter-utils"; export type ImportProgressCallback = (message: string) => 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 - - async handleRateLimit(onProgress: ImportProgressCallback): Promise { - 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 - } - - reset(): void { - this.waitTime = 60000; - } + private waitTime = 60000; // Start with 1 minute + + async handleRateLimit(onProgress: ImportProgressCallback): Promise { + 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 + } + + reset(): void { + this.waitTime = 60000; + } } /** @@ -54,129 +54,136 @@ class RateLimiter { * @returns Promise that resolves when tweet is imported */ async function importTweet(tweetMd: string, tweet: Tweet): Promise { - const metadata = { - sm_source: 'consumer', - tweet_id: tweet.id_str, - author: tweet.user.screen_name, - created_at: tweet.created_at, - likes: tweet.favorite_count, - retweets: tweet.retweet_count || 0, - }; + const metadata = { + sm_source: "consumer", + tweet_id: tweet.id_str, + author: tweet.user.screen_name, + created_at: tweet.created_at, + likes: tweet.favorite_count, + retweets: tweet.retweet_count || 0, + }; - try { - await saveTweet(tweetMd, metadata); - } catch (error) { - throw new Error(`Failed to save tweet: ${error instanceof Error ? error.message : 'Unknown error'}`); - } + try { + await saveTweet(tweetMd, metadata); + } catch (error) { + throw new Error( + `Failed to save tweet: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } } /** * Main class for handling Twitter bookmarks import */ export class TwitterImporter { - private importInProgress = false; - private rateLimiter = new RateLimiter(); - - constructor(private config: TwitterImportConfig) {} - - /** - * Starts the import process for all Twitter bookmarks - * @returns Promise that resolves when import is complete - */ - async startImport(): Promise { - if (this.importInProgress) { - throw new Error('Import already in progress'); - } - - this.importInProgress = true; - - try { - await this.batchImportAll('', 0); - this.rateLimiter.reset(); - } catch (error) { - await this.config.onError(error as Error); - } finally { - this.importInProgress = false; - } - } - - /** - * Recursive function to import all bookmarks with pagination - * @param cursor - Pagination cursor for Twitter API - * @param totalImported - Number of tweets imported so far - */ - private async batchImportAll(cursor = '', totalImported = 0): Promise { - try { - // Get authentication tokens - const tokens = await getTwitterTokens(); - if (!tokens) { - await this.config.onProgress('Please visit Twitter/X first to capture authentication tokens'); - return; - } + private importInProgress = false; + private rateLimiter = new RateLimiter(); - // Create headers for API request - const headers = createTwitterAPIHeaders(tokens); + constructor(private config: TwitterImportConfig) {} - // Build API request with pagination - const variables = buildRequestVariables(cursor); - const urlWithCursor = cursor - ? `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}` - : BOOKMARKS_URL; + /** + * Starts the import process for all Twitter bookmarks + * @returns Promise that resolves when import is complete + */ + async startImport(): Promise { + if (this.importInProgress) { + throw new Error("Import already in progress"); + } - console.log('Making Twitter API request to:', urlWithCursor); - console.log('Request headers:', Object.fromEntries(headers.entries())); + this.importInProgress = true; - const response = await fetch(urlWithCursor, { - method: 'GET', - headers, - redirect: 'follow', - }); + try { + await this.batchImportAll("", 0); + this.rateLimiter.reset(); + } catch (error) { + await this.config.onError(error as Error); + } finally { + this.importInProgress = false; + } + } - if (!response.ok) { - 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); - } - throw new Error(`Failed to fetch data: ${response.status} - ${errorText}`); - } + /** + * Recursive function to import all bookmarks with pagination + * @param cursor - Pagination cursor for Twitter API + * @param totalImported - Number of tweets imported so far + */ + private async batchImportAll(cursor = "", totalImported = 0): Promise { + try { + // Get authentication tokens + const tokens = await getTwitterTokens(); + if (!tokens) { + await this.config.onProgress( + "Please visit Twitter/X first to capture authentication tokens", + ); + return; + } - const data: TwitterAPIResponse = await response.json(); - const tweets = getAllTweets(data); + // Create headers for API request + const headers = createTwitterAPIHeaders(tokens); - 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`); - } catch (error) { - console.error('Error importing tweet:', error); - // Continue with next tweet - } - } + // Build API request with pagination + const variables = buildRequestVariables(cursor); + const urlWithCursor = cursor + ? `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}` + : BOOKMARKS_URL; - // Handle pagination - const instructions = data.data?.bookmark_timeline_v2?.timeline?.instructions; - const nextCursor = extractNextCursor(instructions || []); + console.log("Making Twitter API request to:", urlWithCursor); + console.log("Request headers:", Object.fromEntries(headers.entries())); - 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); - } else { - await this.config.onComplete(totalImported); - } - } catch (error) { - console.error('Batch import error:', error); - await this.config.onError(error as Error); - } - } -} \ No newline at end of file + 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); + + if (response.status === 429) { + 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); + + 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`); + } catch (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 || []); + + 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); + } else { + await this.config.onComplete(totalImported); + } + } catch (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 13c5e45d..2b42fafa 100644 --- a/apps/browser-extension/utils/twitter-utils.ts +++ b/apps/browser-extension/utils/twitter-utils.ts @@ -1,104 +1,170 @@ // Twitter API data structures and transformation utilities +interface TwitterAPITweet { + __typename?: string; + legacy: { + lang?: string; + favorite_count: number; + created_at: string; + display_text_range?: [number, number]; + entities?: { + hashtags?: Array<{ indices: [number, number]; text: string }>; + urls?: Array<{ + 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; + }; + core?: { + user_results?: { + result?: { + legacy?: { + 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; + sizes?: { + large?: { + w: number; + h: number; + }; + }; + video_info?: { + variants?: Array<{ + url: string; + }>; + duration_millis?: number; + }; +} + export interface Tweet { - __typename?: string; - lang?: string; - favorite_count: number; - created_at: string; - display_text_range?: [number, number]; - entities: { - hashtags: Array<{ - indices: [number, number]; - text: string; - }>; - urls?: Array<{ - 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; - }; - 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; - photos?: Array<{ - url: string; - width: number; - height: number; - }>; - videos?: Array<{ - url: string; - thumbnail_url: string; - duration: number; - }>; - retweet_count?: number; - quote_count?: number; - reply_count?: number; + __typename?: string; + lang?: string; + favorite_count: number; + created_at: string; + display_text_range?: [number, number]; + entities: { + hashtags: Array<{ + indices: [number, number]; + text: string; + }>; + urls?: Array<{ + 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; + }>; + }; + 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; + photos?: Array<{ + url: string; + width: number; + height: number; + }>; + videos?: Array<{ + url: string; + thumbnail_url: string; + duration: number; + }>; + retweet_count?: number; + quote_count?: number; + reply_count?: number; } export interface TwitterAPIResponse { - data: { - bookmark_timeline_v2: { - timeline: { - instructions: Array<{ - type: string; - entries?: Array<{ - entryId: string; - sortIndex: string; - content: any; - }>; - }>; - }; - }; - }; + data: { + bookmark_timeline_v2: { + timeline: { + instructions: Array<{ + type: string; + entries?: Array<{ + entryId: string; + sortIndex: string; + content: Record; + }>; + }>; + }; + }; + }; } // Twitter API features configuration export const TWITTER_API_FEATURES = { - graphql_timeline_v2_bookmark_timeline: true, - responsive_web_graphql_exclude_directive_enabled: true, - responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, - responsive_web_graphql_timeline_navigation_enabled: true, - responsive_web_enhance_cards_enabled: false, - rweb_tipjar_consumption_enabled: true, - responsive_web_twitter_article_notes_tab_enabled: true, - creator_subscriptions_tweet_preview_api_enabled: true, - freedom_of_speech_not_reach_fetch_enabled: true, - standardized_nudges_misinfo: true, - tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, - longform_notetweets_rich_text_read_enabled: true, - longform_notetweets_inline_media_enabled: true, - responsive_web_media_download_video_enabled: false, - responsive_web_text_conversations_enabled: false, - // Missing features that the API is complaining about - creator_subscriptions_quote_tweet_preview_enabled: true, - view_counts_everywhere_api_enabled: true, - c9s_tweet_anatomy_moderator_badge_enabled: true, - graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, - tweetypie_unmention_optimization_enabled: true, - responsive_web_twitter_article_tweet_consumption_enabled: true, - tweet_awards_web_tipping_enabled: true, - communities_web_enable_tweet_community_results_fetch: true, - responsive_web_edit_tweet_api_enabled: true, - longform_notetweets_consumption_enabled: true, - articles_preview_enabled: true, - rweb_video_timestamps_enabled: true, - verified_phone_label_enabled: true + graphql_timeline_v2_bookmark_timeline: true, + responsive_web_graphql_exclude_directive_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_enhance_cards_enabled: false, + rweb_tipjar_consumption_enabled: true, + responsive_web_twitter_article_notes_tab_enabled: true, + creator_subscriptions_tweet_preview_api_enabled: true, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_media_download_video_enabled: false, + responsive_web_text_conversations_enabled: false, + // Missing features that the API is complaining about + creator_subscriptions_quote_tweet_preview_enabled: true, + view_counts_everywhere_api_enabled: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + tweetypie_unmention_optimization_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: true, + communities_web_enable_tweet_community_results_fetch: true, + responsive_web_edit_tweet_api_enabled: true, + longform_notetweets_consumption_enabled: true, + 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))}`; @@ -106,184 +172,206 @@ export const BOOKMARKS_URL = `https://x.com/i/api/graphql/xLjCVTqYWz8CGSprLU349w /** * Transform raw Twitter API response data into standardized Tweet format */ -export function transformTweetData(input: any): Tweet | null { - try { - const tweet = input.content?.itemContent?.tweet_results?.result; +export function transformTweetData( + input: Record, +): Tweet | null { + try { + const content = input.content as { + itemContent?: { tweet_results?: { result?: unknown } }; + }; + const tweetData = content?.itemContent?.tweet_results?.result; - if (!tweet || !tweet.legacy) { - return null; - } + if (!tweetData) { + return null; + } - // Handle media entities - const media = tweet.legacy.entities?.media || []; - const photos = media - .filter((m: any) => m.type === 'photo') - .map((m: any) => ({ - url: m.media_url_https, - width: m.sizes?.large?.w || 0, - height: m.sizes?.large?.h || 0, - })); + const tweet = tweetData as TwitterAPITweet; - const videos = media - .filter((m: any) => m.type === 'video') - .map((m: any) => ({ - url: m.video_info?.variants?.[0]?.url || '', - thumbnail_url: m.media_url_https, - duration: m.video_info?.duration_millis || 0, - })); + if (!tweet.legacy) { + return null; + } - const transformed: Tweet = { - __typename: tweet.__typename, - lang: tweet.legacy?.lang, - favorite_count: tweet.legacy.favorite_count || 0, - created_at: new Date(tweet.legacy.created_at).toISOString(), - display_text_range: tweet.legacy.display_text_range, - entities: { - hashtags: tweet.legacy.entities?.hashtags || [], - urls: tweet.legacy.entities?.urls || [], - user_mentions: tweet.legacy.entities?.user_mentions || [], - symbols: tweet.legacy.entities?.symbols || [], - }, - id_str: tweet.legacy.id_str, - text: tweet.legacy.full_text, - user: { - id_str: tweet.core?.user_results?.result?.legacy?.id_str || '', - name: tweet.core?.user_results?.result?.legacy?.name || 'Unknown', - profile_image_url_https: tweet.core?.user_results?.result?.legacy?.profile_image_url_https || '', - screen_name: tweet.core?.user_results?.result?.legacy?.screen_name || 'unknown', - verified: tweet.core?.user_results?.result?.legacy?.verified || false, - is_blue_verified: tweet.core?.user_results?.result?.is_blue_verified || false, - }, - conversation_count: tweet.legacy.reply_count || 0, - retweet_count: tweet.legacy.retweet_count || 0, - quote_count: tweet.legacy.quote_count || 0, - reply_count: tweet.legacy.reply_count || 0, - }; + // Handle media entities + 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, + })); - if (photos.length > 0) { - transformed.photos = photos; - } + const videos = media + .filter((m) => m.type === "video") + .map((m) => ({ + url: m.video_info?.variants?.[0]?.url || "", + thumbnail_url: m.media_url_https, + duration: m.video_info?.duration_millis || 0, + })); - if (videos.length > 0) { - transformed.videos = videos; - } + const transformed: Tweet = { + __typename: tweet.__typename, + lang: tweet.legacy?.lang, + favorite_count: tweet.legacy.favorite_count || 0, + created_at: new Date(tweet.legacy.created_at).toISOString(), + display_text_range: tweet.legacy.display_text_range, + entities: { + hashtags: tweet.legacy.entities?.hashtags || [], + urls: tweet.legacy.entities?.urls || [], + user_mentions: tweet.legacy.entities?.user_mentions || [], + symbols: tweet.legacy.entities?.symbols || [], + }, + id_str: tweet.legacy.id_str, + text: tweet.legacy.full_text, + user: { + id_str: tweet.core?.user_results?.result?.legacy?.id_str || "", + name: tweet.core?.user_results?.result?.legacy?.name || "Unknown", + profile_image_url_https: + tweet.core?.user_results?.result?.legacy?.profile_image_url_https || + "", + screen_name: + tweet.core?.user_results?.result?.legacy?.screen_name || "unknown", + verified: tweet.core?.user_results?.result?.legacy?.verified || false, + is_blue_verified: + tweet.core?.user_results?.result?.is_blue_verified || false, + }, + conversation_count: tweet.legacy.reply_count || 0, + retweet_count: tweet.legacy.retweet_count || 0, + quote_count: tweet.legacy.quote_count || 0, + reply_count: tweet.legacy.reply_count || 0, + }; - return transformed; - } catch (error) { - console.error('Error transforming tweet data:', error); - return null; - } + if (photos.length > 0) { + transformed.photos = photos; + } + + if (videos.length > 0) { + transformed.videos = videos; + } + + return transformed; + } catch (error) { + console.error("Error transforming tweet data:", error); + return null; + } } /** * Extract all tweets from Twitter API response */ export function getAllTweets(data: TwitterAPIResponse): Tweet[] { - const tweets: Tweet[] = []; - - try { - const 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); - if (tweet) { - tweets.push(tweet); - } - } - } - } - } - } catch (error) { - console.error('Error extracting tweets:', error); - } + const tweets: Tweet[] = []; - return tweets; + try { + const 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); + if (tweet) { + tweets.push(tweet); + } + } + } + } + } + } catch (error) { + console.error("Error extracting tweets:", error); + } + + return tweets; } /** * Extract pagination cursor from Twitter API response */ -export function extractNextCursor(instructions: any[]): string | null { - try { - for (const instruction of instructions) { - if (instruction.type === 'TimelineAddEntries' && instruction.entries) { - for (const entry of instruction.entries) { - if (entry.entryId.startsWith('cursor-bottom-')) { - return entry.content?.value || null; - } - } - } - } - } catch (error) { - console.error('Error extracting cursor:', error); - } - - return null; +export function extractNextCursor( + instructions: Array>, +): string | null { + try { + for (const instruction of instructions) { + if (instruction.type === "TimelineAddEntries" && instruction.entries) { + const entries = instruction.entries as Array<{ + entryId: string; + content?: { value?: string }; + }>; + for (const entry of entries) { + if (entry.entryId.startsWith("cursor-bottom-")) { + return entry.content?.value || null; + } + } + } + } + } catch (error) { + console.error("Error extracting cursor:", error); + } + + 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(); - - 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`; - - // Add media if present - if (tweet.photos && tweet.photos.length > 0) { - markdown += `**Images:**\n`; - tweet.photos.forEach((photo, index) => { - markdown += `![Image ${index + 1}](${photo.url})\n`; - }); - markdown += '\n'; - } - - if (tweet.videos && tweet.videos.length > 0) { - markdown += `**Videos:**\n`; - tweet.videos.forEach((video, index) => { - 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`; - } - - if (tweet.entities.user_mentions.length > 0) { - 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
`; - - return markdown; + 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`; + + // Add tweet text + markdown += `${tweet.text}\n\n`; + + // Add media if present + if (tweet.photos && tweet.photos.length > 0) { + markdown += `**Images:**\n`; + tweet.photos.forEach((photo, index) => { + markdown += `![Image ${index + 1}](${photo.url})\n`; + }); + markdown += "\n"; + } + + if (tweet.videos && tweet.videos.length > 0) { + markdown += `**Videos:**\n`; + tweet.videos.forEach((video, index) => { + 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`; + } + + if (tweet.entities.user_mentions.length > 0) { + 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
`; + + return markdown; } /** * Build Twitter API request variables for pagination */ export function buildRequestVariables(cursor?: string, count: number = 100) { - const variables = { - count, - includePromotedContent: false, - }; - - if (cursor) { - (variables as any).cursor = cursor; - } - - return variables; -} \ No newline at end of file + const variables = { + count, + includePromotedContent: false, + }; + + 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 69cccf0f..2fafda50 100644 --- a/apps/browser-extension/utils/types.ts +++ b/apps/browser-extension/utils/types.ts @@ -5,145 +5,145 @@ /** * 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?: any; - 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; - metadata: { - sm_source: string; - [key: string]: any; - }; + containerTags: string[]; + content: string; + metadata: { + 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; - twitterAuth?: { - cookie: string; - csrf: string; - auth: string; - }; - tokens_logged?: boolean; - cookie?: string; - csrf?: string; - auth?: string; - defaultProject?: Project; - projectsCache?: { - projects: Project[]; - timestamp: number; - }; + bearerToken?: string; + twitterAuth?: { + cookie: string; + csrf: string; + auth: string; + }; + tokens_logged?: boolean; + cookie?: string; + csrf?: string; + auth?: string; + defaultProject?: Project; + projectsCache?: { + 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; +export interface APIResponse { + success: boolean; + data?: T; + error?: string; } /** * Error types for better error handling */ export class ExtensionError extends Error { - constructor( - message: string, - public code?: string, - public statusCode?: number - ) { - super(message); - this.name = 'ExtensionError'; - } + constructor( + message: string, + public code?: string, + public statusCode?: number, + ) { + super(message); + this.name = "ExtensionError"; + } } export class TwitterAPIError extends ExtensionError { - constructor(message: string, statusCode?: number) { - super(message, 'TWITTER_API_ERROR', statusCode); - this.name = 'TwitterAPIError'; - } + constructor(message: string, statusCode?: number) { + 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'; - } + constructor(message: string, statusCode?: number) { + 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: string = "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[]; -} \ No newline at end of file + projects: Project[]; +} diff --git a/apps/browser-extension/utils/ui-components.ts b/apps/browser-extension/utils/ui-components.ts index 73305ea8..58f0b104 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 { ELEMENT_IDS, UI_CONFIG, API_ENDPOINTS } 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,10 +12,10 @@ 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 = ` + toast.style.cssText = ` position: fixed; top: 20px; right: 20px; @@ -26,7 +26,7 @@ export function createToast(state: ToastState): HTMLElement { display: flex; align-items: center; gap: 12px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #374151; min-width: 200px; @@ -34,11 +34,46 @@ export function createToast(state: ToastState): HTMLElement { animation: slideIn 0.3s ease-out; `; - // Add keyframe animations if not already present - if (!document.getElementById('supermemory-toast-styles')) { - const style = document.createElement('style'); - style.id = 'supermemory-toast-styles'; - style.textContent = ` + // 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"; + style.textContent = ` + @font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url('${chrome.runtime.getURL("fonts/SpaceGrotesk-Light.ttf")}') format('truetype'); + } + @font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('${chrome.runtime.getURL("fonts/SpaceGrotesk-Regular.ttf")}') format('truetype'); + } + @font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('${chrome.runtime.getURL("fonts/SpaceGrotesk-Medium.ttf")}') format('truetype'); + } + @font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('${chrome.runtime.getURL("fonts/SpaceGrotesk-SemiBold.ttf")}') format('truetype'); + } + @font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('${chrome.runtime.getURL("fonts/SpaceGrotesk-Bold.ttf")}') format('truetype'); + } @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } @@ -52,19 +87,19 @@ export function createToast(state: ToastState): HTMLElement { 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) { - case 'loading': - icon.innerHTML = ` + // Configure toast based on state + switch (state) { + case "loading": + icon.innerHTML = ` @@ -76,32 +111,33 @@ 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; + case "success": { + const iconUrl = browser.runtime.getURL("/icon-16.png"); + icon.innerHTML = `Success`; + text.textContent = "Added to Memory"; + break; + } - case 'error': - icon.innerHTML = ` + case "error": + icon.innerHTML = ` `; - 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; } /** @@ -110,9 +146,9 @@ 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; - button.style.cssText = ` + const button = document.createElement("div"); + button.id = ELEMENT_IDS.TWITTER_IMPORT_BUTTON; + button.style.cssText = ` position: fixed; top: 10px; right: 10px; @@ -129,24 +165,24 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement { transition: all 0.2s ease; `; - const iconUrl = browser.runtime.getURL('/light-mode-icon.png'); - button.innerHTML = ` + 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.addEventListener('mouseleave', () => { - button.style.transform = 'scale(1)'; - button.style.boxShadow = '0 2px 8px rgba(29, 155, 240, 0.3)'; - }); - button.addEventListener('click', onClick); - - return button; + button.addEventListener("mouseenter", () => { + 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.addEventListener("click", onClick); + + return button; } /** @@ -157,12 +193,12 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement { * @returns HTMLElement - The dialog element */ export function createTwitterImportUI( - onClose: () => void, - onImport: () => void, - isAuthenticated: boolean + onClose: () => void, + onImport: () => void, + isAuthenticated: boolean, ): HTMLElement { - const container = document.createElement('div'); - container.style.cssText = ` + const container = document.createElement("div"); + container.style.cssText = ` position: fixed; top: 20px; right: 20px; @@ -174,10 +210,10 @@ export function createTwitterImportUI( min-width: 280px; max-width: 400px; border: 1px solid #e1e5e9; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; `; - container.innerHTML = ` + container.innerHTML = `
@@ -192,7 +228,9 @@ export function createTwitterImportUI(
- ${isAuthenticated ? ` + ${ + isAuthenticated + ? `

This will import all your Twitter bookmarks to Supermemory @@ -204,7 +242,8 @@ export function createTwitterImportUI(

- ` : ` + ` + : `

Please sign in to Supermemory first @@ -213,7 +252,8 @@ export function createTwitterImportUI( Sign In

- `} + ` + } `; - // Add event listeners - const closeBtn = container.querySelector(`#${ELEMENT_IDS.TWITTER_CLOSE_BTN}`); - closeBtn?.addEventListener('click', onClose); + // Add event listeners + 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); + const importBtn = container.querySelector( + `#${ELEMENT_IDS.TWITTER_IMPORT_BTN}`, + ); + 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` }); - }); + const signinBtn = container.querySelector( + `#${ELEMENT_IDS.TWITTER_SIGNIN_BTN}`, + ); + signinBtn?.addEventListener("click", () => { + browser.tabs.create({ url: `${API_ENDPOINTS.SUPERMEMORY_WEB}/login` }); + }); - return container; + return container; } /** @@ -244,8 +288,8 @@ export function createTwitterImportUI( * @returns HTMLElement - The save button element */ export function createSaveTweetElement(onClick: () => void): HTMLElement { - const iconButton = document.createElement('div'); - iconButton.style.cssText = ` + const iconButton = document.createElement("div"); + iconButton.style.cssText = ` display: inline-flex; align-items: flex-end; opacity: 0.7; @@ -259,32 +303,34 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement { 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 iconFileName = isLightMode ? '/light-mode-icon.png' : '/dark-mode-icon.png'; - const iconUrl = browser.runtime.getURL(iconFileName); - iconButton.innerHTML = ` + // 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 iconFileName = isLightMode + ? "/light-mode-icon.png" + : "/dark-mode-icon.png"; + const iconUrl = browser.runtime.getURL(iconFileName); + iconButton.innerHTML = ` Save to Memory `; - iconButton.addEventListener('mouseenter', () => { - iconButton.style.opacity = '1'; - }); + iconButton.addEventListener("mouseenter", () => { + iconButton.style.opacity = "1"; + }); - iconButton.addEventListener('mouseleave', () => { - iconButton.style.opacity = '0.7'; - }); + iconButton.addEventListener("mouseleave", () => { + iconButton.style.opacity = "0.7"; + }); - iconButton.addEventListener('click', (event) => { - event.stopPropagation(); - event.preventDefault(); - onClick(); - }); + iconButton.addEventListener("click", (event) => { + event.stopPropagation(); + event.preventDefault(); + onClick(); + }); - return iconButton; + return iconButton; } /** @@ -293,8 +339,8 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement { * @returns HTMLElement - The save button element */ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement { - const iconButton = document.createElement('div'); - iconButton.style.cssText = ` + const iconButton = document.createElement("div"); + iconButton.style.cssText = ` display: inline-flex; align-items: center; justify-content: center; @@ -305,100 +351,107 @@ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement { 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); - iconButton.innerHTML = ` + // 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); + iconButton.innerHTML = ` Save to Memory `; - iconButton.addEventListener('mouseenter', () => { - iconButton.style.opacity = '0.8'; - }); + iconButton.addEventListener("mouseenter", () => { + iconButton.style.opacity = "0.8"; + }); - iconButton.addEventListener('mouseleave', () => { - iconButton.style.opacity = '1'; - }); + iconButton.addEventListener("mouseleave", () => { + iconButton.style.opacity = "1"; + }); - iconButton.addEventListener('click', (event) => { - event.stopPropagation(); - event.preventDefault(); - onClick(); - }); + iconButton.addEventListener("click", (event) => { + event.stopPropagation(); + event.preventDefault(); + onClick(); + }); - return iconButton; + return iconButton; } /** * Utility functions for DOM manipulation */ export const DOMUtils = { - /** - * Check if current page is on specified domains - * @param domains - Array of domain names to check - * @returns boolean - */ - isOnDomain(domains: readonly string[]): boolean { - return domains.includes(window.location.hostname); - }, + /** + * Check if current page is on specified domains + * @param domains - Array of domain names to check + * @returns boolean + */ + isOnDomain(domains: readonly string[]): boolean { + return domains.includes(window.location.hostname); + }, - /** - * Detect if the page is in dark mode based on color-scheme style - * @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; - }, + /** + * Detect if the page is in dark mode based on color-scheme style + * @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; + }, - /** - * Check if element exists in DOM - * @param id - Element ID to check - * @returns boolean - */ - elementExists(id: string): boolean { - return !!document.getElementById(id); - }, + /** + * Check if element exists in DOM + * @param id - Element ID to check + * @returns boolean + */ + elementExists(id: string): boolean { + return !!document.getElementById(id); + }, - /** - * Remove element from DOM if it exists - * @param id - Element ID to remove - */ - removeElement(id: string): void { - const element = document.getElementById(id); - element?.remove(); - }, + /** + * Remove element from DOM if it exists + * @param id - Element ID to remove + */ + removeElement(id: string): void { + const element = document.getElementById(id); + element?.remove(); + }, - /** - * Show toast notification with auto-dismiss - * @param state - Toast state - * @param duration - Duration to show toast (default from config) - * @returns The toast element - */ - showToast(state: ToastState, duration: number = UI_CONFIG.TOAST_DURATION): HTMLElement { - // Remove all existing toasts more aggressively - const existingToasts = document.querySelectorAll(`#${ELEMENT_IDS.SUPERMEMORY_TOAST}`); - existingToasts.forEach(toast => toast.remove()); + /** + * Show toast notification with auto-dismiss + * @param state - Toast state + * @param duration - Duration to show toast (default from config) + * @returns The toast element + */ + showToast( + state: ToastState, + duration: number = UI_CONFIG.TOAST_DURATION, + ): HTMLElement { + // Remove all existing toasts more aggressively + const existingToasts = document.querySelectorAll( + `#${ELEMENT_IDS.SUPERMEMORY_TOAST}`, + ); + existingToasts.forEach((toast) => { + 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'; - setTimeout(() => { - if (document.body.contains(toast)) { - toast.remove(); - } - }, 300); - } - }, duration); - } + // 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"; + setTimeout(() => { + if (document.body.contains(toast)) { + toast.remove(); + } + }, 300); + } + }, duration); + } - return toast; - } -}; \ No newline at end of file + return toast; + }, +}; diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts index 8efd7a53..382d62b1 100644 --- a/apps/browser-extension/wxt.config.ts +++ b/apps/browser-extension/wxt.config.ts @@ -1,35 +1,40 @@ -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', - homepage_url: 'https://supermemory.ai', - permissions: [ - 'contextMenus', - 'storage', - 'scripting', - 'activeTab', - 'webRequest', - 'tabs', - ], - host_permissions: [ - '*://x.com/*', - '*://twitter.com/*', - '*://supermemory.ai/*', - '*://api.supermemory.ai/*', - '*://chatgpt.com/*', - '*://chat.openai.com/*', - ], - web_accessible_resources: [ - { - resources: ['icon-16.png', 'light-mode-icon.png', 'dark-mode-icon.png'], - matches: [''], - }, - ], - }, - webExt: { - disabled: true, - }, + modules: ["@wxt-dev/module-react"], + manifest: { + name: "Supermemory", + homepage_url: "https://supermemory.ai", + permissions: [ + "contextMenus", + "storage", + "scripting", + "activeTab", + "webRequest", + "tabs", + ], + host_permissions: [ + "*://x.com/*", + "*://twitter.com/*", + "*://supermemory.ai/*", + "*://api.supermemory.ai/*", + "*://chatgpt.com/*", + "*://chat.openai.com/*", + ], + web_accessible_resources: [ + { + resources: [ + "icon-16.png", + "light-mode-icon.png", + "dark-mode-icon.png", + "fonts/*.ttf" + ], + matches: [""], + }, + ], + }, + webExt: { + disabled: true, + }, });