revamped the login experience and extension experience

This commit is contained in:
Mahesh Sanikommmu 2025-08-26 21:49:00 -07:00
parent b52c3bbd74
commit ee9252e98e
23 changed files with 2683 additions and 1845 deletions

View file

@ -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;
}
});
});
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;
}
},
);
});

View file

@ -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: ['<all_urls>'],
main() {
let twitterImportUI: HTMLElement | null = null;
let isTwitterImportOpen = false;
matches: ["<all_urls>"],
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 + '<br>' + "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}<br>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 = `
<div class="flex items-center justify-center gap-2">
<img src="${iconUrl}" alt="Supermemory" style="width: 16px; height: 16px; flex-shrink: 0; border-radius: 2px;" />
Save to Supermemory
</div>
`;
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 = `
<div style="display: flex; align-items: center; gap: 8px; color: #92400e; background: #fef3c7; border: 1px solid #f59e0b; border-radius: 8px; padding: 8px 12px; font-size: 13px;">
<div style="width: 12px; height: 12px; border: 2px solid #f59e0b; border-top: 2px solid transparent; border-radius: 50%; animation: spin 1s linear infinite;"></div>
<span>${message.importedMessage}</span>
</div>
`;
}
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 = `
<div style="display: flex; align-items: center; gap: 8px; color: #0369a1; background: #f0f9ff; border: 1px solid #0ea5e9; border-radius: 8px; padding: 8px 12px; font-size: 13px;">
<span style="color: #059669;"></span>
<span>Successfully imported ${message.totalImported} tweets!</span>
</div>
`;
}
}
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 }, () => {});
}
});
},
});

View file

@ -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;
}

View file

@ -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<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
const [projects, setProjects] = useState<Project[]>([]);
const [defaultProject, setDefaultProjectState] = useState<Project | null>(null);
const [loadingProjects, setLoadingProjects] = useState<boolean>(false);
const [showProjectSelector, setShowProjectSelector] = useState<boolean>(false);
const [userSignedIn, setUserSignedIn] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
const [projects, setProjects] = useState<Project[]>([]);
const [defaultProject, setDefaultProjectState] = useState<Project | null>(
null,
);
const [loadingProjects, setLoadingProjects] = useState<boolean>(false);
const [showProjectSelector, setShowProjectSelector] =
useState<boolean>(false);
const [currentUrl, setCurrentUrl] = useState<string>("");
const [currentTitle, setCurrentTitle] = useState<string>("");
const [saving, setSaving] = useState<boolean>(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 (
<div className="popup-container">
<div className="header">
<img src="/icon-48.png" alt="Supermemory" className="logo" />
<h1>Supermemory</h1>
</div>
<div className="content">
<div>Loading...</div>
</div>
</div>
);
}
const handleShowProjectSelector = () => {
console.log("handleShowProjectSelector, projects.length:", projects.length);
if (projects.length === 0) {
loadProjects();
}
setShowProjectSelector(true);
};
return (
<div className="popup-container">
<div className="header">
<img src="/icon-48.png" alt="Supermemory" className="logo" />
<h1>Supermemory</h1>
</div>
<div className="content">
{userSignedIn ? (
<div className="authenticated">
<div className="project-section">
<div className="project-header">
<span className="project-label">Default Project:</span>
<button
className="project-change-btn"
onClick={handleShowProjectSelector}
>
Change
</button>
</div>
<div className="project-current">
{defaultProject ? (
<div className="project-info">
<span className="project-name">{defaultProject.name}</span>
</div>
) : (
<span className="project-none">No project selected</span>
)}
</div>
</div>
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 && (
<div className="project-selector">
<div className="project-selector-header">
<span>Select Default Project</span>
<button
className="project-close-btn"
onClick={() => setShowProjectSelector(false)}
>
×
</button>
</div>
{loadingProjects ? (
<div className="project-loading">Loading projects...</div>
) : (
<div className="project-list">
{projects.map((project) => (
<div
key={project.id}
className={`project-item ${defaultProject?.id === project.id ? 'selected' : ''}`}
onClick={() => handleProjectSelect(project)}
>
<div className="project-item-info">
<span className="project-item-name">{project.name}</span>
<span className="project-item-count">{project.documentCount} docs</span>
</div>
{defaultProject?.id === project.id && (
<span className="project-item-check"></span>
)}
</div>
))}
</div>
)}
</div>
)}
<div className="actions">
<button
onClick={() => {
chrome.tabs.create({
url: 'https://chatgpt.com/#settings/Personalization',
});
}}
className="chatgpt-btn"
>
<img
src="https://upload.wikimedia.org/wikipedia/commons/1/13/ChatGPT-Logo.png"
alt="ChatGPT"
className="chatgpt-logo"
/>
Import ChatGPT Memories
</button>
<button className="sign-out-btn" onClick={handleSignOut}>
Sign Out
</button>
</div>
</div>
) : (
<div className="unauthenticated">
<div className="status">
<span className="status-indicator signed-out"></span>
<span>Not signed in</span>
</div>
<p className="instruction">
<a
onClick={() => {
chrome.tabs.create({
url: 'https://app.supermemory.ai/login',
});
}}
>
Login to Supermemory
</a>
.
</p>
</div>
)}
</div>
</div>
);
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 (
<div className="popup-container">
<div className="header">
<img src="/icon-48.png" alt="Supermemory" className="logo" />
<h1>Supermemory</h1>
</div>
<div className="content">
<div>Loading...</div>
</div>
</div>
);
}
return (
<div className="popup-container">
<div className="header">
<img
src="/logo-trademark.svg"
alt="Supermemory"
className="logo"
style={{ width: "80%", height: "32px" }}
/>
{userSignedIn && (
<button
type="button"
className="header-sign-out"
onClick={handleSignOut}
title="Logout"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<title>Logout</title>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16,17 21,12 16,7" />
<line x1="21" y1="12" x2="9" y2="12" />
</svg>
</button>
)}
</div>
<div className="content">
{userSignedIn ? (
<div className="authenticated">
{/* Tab Navigation */}
<div className="tab-navigation">
<button
type="button"
className={`tab-btn ${activeTab === "save" ? "active" : ""}`}
onClick={() => setActiveTab("save")}
>
Save
</button>
<button
type="button"
className={`tab-btn ${activeTab === "imports" ? "active" : ""}`}
onClick={() => setActiveTab("imports")}
>
Imports
</button>
</div>
{/* Tab Content */}
{activeTab === "save" ? (
<div className="tab-content">
{/* Current Page Info */}
<div className="current-page">
<div className="page-info">
<h3 className="page-title">
{currentTitle || "Current Page"}
</h3>
<p className="page-url">{currentUrl}</p>
</div>
</div>
{/* Project Selection */}
<div className="project-section">
<button
type="button"
className="project-selector-btn"
onClick={handleShowProjectSelector}
>
<div className="project-selector-content">
<span className="project-label">Save to project:</span>
<div className="project-value">
<span className="project-name">
{defaultProject
? defaultProject.name
: "Default Project"}
</span>
<svg
className="project-arrow"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Select project"
>
<title>Select project</title>
<path d="M9 18l6-6-6-6" />
</svg>
</div>
</div>
</button>
</div>
{/* Save Button at Bottom */}
<div className="save-action">
<button
type="button"
className="login-primary-btn"
onClick={handleSaveCurrentPage}
disabled={saving}
>
{saving ? "Saving..." : "Save Current Page"}
</button>
</div>
</div>
) : (
<div className="tab-content">
{/* Import Actions */}
<div className="import-actions">
<div className="import-item">
<button
type="button"
onClick={() => {
chrome.tabs.create({
url: "https://chatgpt.com/#settings/Personalization",
});
}}
className="chatgpt-btn"
>
<svg
className="chatgpt-logo"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
aria-label="ChatGPT Logo"
>
<title>ChatGPT Logo</title>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
Import ChatGPT Memories
</button>
</div>
<div className="import-item">
<button
type="button"
onClick={() => {
chrome.tabs.create({
url: "https://x.com/i/bookmarks",
});
}}
className="twitter-btn"
>
<svg
className="twitter-logo"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
aria-label="X Twitter Logo"
>
<title>X Twitter Logo</title>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
Import X Bookmarks
</button>
<p className="import-instructions">
Click on supermemory on top right to import bookmarks
</p>
</div>
</div>
</div>
)}
{showProjectSelector && (
<div className="project-selector">
<div className="project-selector-header">
<span>Select the Project</span>
<button
type="button"
className="project-close-btn"
onClick={() => setShowProjectSelector(false)}
>
×
</button>
</div>
{loadingProjects ? (
<div className="project-loading">Loading projects...</div>
) : (
<div className="project-list">
{projects.map((project) => (
<button
key={project.id}
type="button"
className={`project-item ${defaultProject?.id === project.id ? "selected" : ""}`}
onClick={() => handleProjectSelect(project)}
>
<div className="project-item-info">
<span className="project-item-name">
{project.name}
</span>
<span className="project-item-count">
{project.documentCount} docs
</span>
</div>
{defaultProject?.id === project.id && (
<span className="project-item-check"></span>
)}
</button>
))}
</div>
)}
</div>
)}
</div>
) : (
<div className="unauthenticated">
<div className="login-intro">
<h2 className="login-title">
Login to unlock all chrome extension features
</h2>
<ul className="features-list">
<li>Save any page to your supermemory</li>
<li>Import all your Twitter / X Bookmarks</li>
<li>Import your ChatGPT Memories</li>
</ul>
</div>
<div className="login-actions">
<p className="login-help">
having trouble to login?{" "}
<button
type="button"
className="help-link"
onClick={() => {
window.open("mailto:dhravya@supermemory.com", "_blank");
}}
>
reach out to us
</button>
</p>
<button
type="button"
className="login-primary-btn"
onClick={() => {
chrome.tabs.create({
url: import.meta.env.PROD
? "https://app.supermemory.ai/login"
: "http://localhost:3000/login",
});
}}
>
Login in
</button>
</div>
</div>
)}
</div>
</div>
);
}
export default App;

View file

@ -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(
<React.StrictMode>
<App />
</React.StrictMode>,
);
const rootElement = document.getElementById("root");
if (rootElement) {
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
}

View file

@ -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;
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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"
}
}

View file

@ -0,0 +1,15 @@
<svg width="1138" height="135" viewBox="0 0 1138 135" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M267.914 105.886C258.2 105.886 250.229 103.796 244.024 99.6159C237.808 95.4359 234.055 89.4661 232.753 81.6949L250.183 77.2146C250.88 80.7019 252.065 83.4385 253.715 85.4246C255.365 87.4222 257.422 88.831 259.873 89.6855C262.325 90.5284 265.009 90.9556 267.914 90.9556C272.318 90.9556 275.571 90.182 277.675 88.6462C279.778 87.0989 280.835 85.1937 280.835 82.8958C280.835 80.5979 279.836 78.8428 277.826 77.5957C275.815 76.3486 272.62 75.3325 268.205 74.5357L263.998 73.7852C258.793 72.7921 254.029 71.418 249.729 69.6744C245.419 67.9308 241.968 65.5175 239.365 62.4344C236.762 59.3513 235.461 55.3676 235.461 50.4947C235.461 43.1277 238.168 37.4812 243.571 33.5436C248.986 29.6176 256.085 27.6431 264.905 27.6431C273.213 27.6431 280.126 29.4906 285.634 33.1626C291.142 36.8461 294.744 41.6728 296.452 47.6426L278.871 53.012C278.07 49.2361 276.443 46.5456 273.991 44.9521C271.539 43.3586 268.507 42.5619 264.905 42.5619C261.303 42.5619 258.537 43.1854 256.643 44.4325C254.737 45.6796 253.785 47.4001 253.785 49.5825C253.785 51.9727 254.784 53.7394 256.794 54.8826C258.793 56.0258 261.5 56.9033 264.905 57.4922L269.111 58.2428C274.723 59.2358 279.801 60.5522 284.356 62.2034C288.911 63.8431 292.513 66.1872 295.174 69.2241C297.823 72.2609 299.159 76.3717 299.159 81.5448C299.159 89.3044 296.324 95.3089 290.665 99.5351C285.007 103.773 277.419 105.886 267.903 105.886H267.914Z" fill="#1C2026"/>
<path d="M332.159 105.585C326.35 105.585 321.272 104.269 316.914 101.624C312.557 98.9915 309.176 95.3311 306.77 90.6545C304.365 85.978 303.168 80.597 303.168 74.5348V30.3442H322.097V73.0453C322.097 78.6225 323.468 82.8025 326.233 85.5854C328.987 88.3682 332.915 89.7654 338.027 89.7654C343.837 89.7654 348.345 87.8486 351.552 84.015C354.759 80.1813 356.363 74.8351 356.363 67.9646V30.3442H375.291V104.396H356.665V94.696H353.958C352.761 97.1902 350.507 99.6266 347.195 102.017C343.895 104.407 338.875 105.596 332.171 105.596L332.159 105.585Z" fill="#1C2026"/>
<path d="M388.805 134.255V30.3429H407.431V39.3035H410.138C411.835 36.4167 414.496 33.8533 418.098 31.6131C421.7 29.373 426.859 28.2529 433.575 28.2529C439.583 28.2529 445.148 29.7194 450.249 32.6524C455.362 35.5853 459.464 39.8924 462.566 45.562C465.669 51.2316 467.226 58.1021 467.226 66.1619V68.5522C467.226 76.612 465.669 83.4825 462.566 89.1521C459.464 94.8217 455.35 99.1288 450.249 102.062C445.137 104.995 439.583 106.461 433.575 106.461C429.067 106.461 425.29 105.942 422.234 104.891C419.179 103.852 416.727 102.501 414.868 100.861C413.008 99.2212 411.533 97.5584 410.441 95.861H407.733V134.232H388.805V134.255ZM427.87 90.0643C433.784 90.0643 438.665 88.1937 442.522 84.464C446.38 80.7343 448.309 75.2841 448.309 68.1134V66.6238C448.309 59.4531 446.357 54.0029 442.453 50.2732C438.548 46.5435 433.691 44.6728 427.882 44.6728C422.072 44.6728 417.215 46.5435 413.311 50.2732C409.406 54.0029 407.454 59.4531 407.454 66.6238V68.1134C407.454 75.2841 409.406 80.7343 413.311 84.464C417.215 88.1937 422.072 90.0643 427.882 90.0643H427.87Z" fill="#1C2026"/>
<path d="M511.102 106.484C503.688 106.484 497.158 104.914 491.5 101.785C485.841 98.6439 481.437 94.2213 478.276 88.494C475.127 82.7666 473.547 76.0232 473.547 68.2635V66.4737C473.547 58.7141 475.093 51.9706 478.207 46.2433C481.309 40.5159 485.666 36.0934 491.279 32.9526C496.891 29.8233 503.398 28.2529 510.811 28.2529C518.225 28.2529 524.488 29.8695 529.891 33.1027C535.294 36.3359 539.512 40.8161 542.51 46.5435C545.519 52.2708 547.018 58.9104 547.018 66.4737V72.8939H492.778C492.975 77.9746 494.881 82.0969 498.483 85.2839C502.085 88.4709 506.5 90.0644 511.706 90.0644C516.912 90.0644 520.92 88.9212 523.419 86.6349C525.917 84.3486 527.822 81.8082 529.124 79.0254L544.601 87.0852C543.195 89.6718 541.173 92.4893 538.513 95.5262C535.852 98.563 532.331 101.15 527.915 103.286C523.512 105.422 517.899 106.496 511.09 106.496L511.102 106.484ZM492.917 58.8527H527.776C527.369 54.5687 525.65 51.1392 522.594 48.5527C519.538 45.9661 515.552 44.6729 510.649 44.6729C505.745 44.6729 501.481 45.9661 498.483 48.5527C495.473 51.1392 493.626 54.5803 492.929 58.8527H492.917Z" fill="#1C2026"/>
<path d="M556.324 104.396V30.3442H574.95V38.7042H577.658C578.762 35.7136 580.586 33.5312 583.142 32.134C585.699 30.7368 588.673 30.0439 592.078 30.0439H601.095V46.7641H591.776C586.965 46.7641 583.014 48.0343 579.912 50.5746C576.81 53.115 575.253 57.0179 575.253 62.2949V104.396H556.324Z" fill="#1C2026"/>
<path d="M608.299 104.395V30.3436H626.925V38.4035H629.633C630.934 35.9209 633.084 33.75 636.093 31.914C639.103 30.0781 643.053 29.1543 647.957 29.1543C653.267 29.1543 657.52 30.1704 660.727 32.2143C663.934 34.2581 666.386 36.9139 668.094 40.2048H670.801C672.498 37.0178 674.903 34.3851 678.017 32.2951C681.119 30.2051 685.523 29.1658 691.24 29.1658C695.841 29.1658 700.024 30.1358 703.789 32.0757C707.542 34.0156 710.552 36.9486 712.806 40.8861C715.06 44.8237 716.187 49.7658 716.187 55.7472V104.418H697.259V57.0866C697.259 53.0105 696.202 49.9506 694.11 47.9067C692.007 45.8629 689.044 44.8468 685.244 44.8468C680.933 44.8468 677.61 46.2209 675.251 48.9575C672.893 51.6942 671.719 55.5971 671.719 60.6778V104.43H652.791V57.0982C652.791 53.0221 651.733 49.9621 649.642 47.9183C647.539 45.8744 644.576 44.8583 640.776 44.8583C636.465 44.8583 633.142 46.2324 630.783 48.9691C628.424 51.7057 627.251 55.6086 627.251 60.6893V104.441H608.322L608.299 104.395Z" fill="#1C2026"/>
<path d="M763.05 106.484C755.637 106.484 749.106 104.914 743.448 101.785C737.789 98.6439 733.385 94.2213 730.224 88.494C727.076 82.7666 725.495 76.0232 725.495 68.2635V66.4737C725.495 58.7141 727.041 51.9706 730.155 46.2433C733.257 40.5159 737.615 36.0934 743.227 32.9526C748.839 29.8233 755.346 28.2529 762.759 28.2529C770.173 28.2529 776.436 29.8695 781.839 33.1027C787.242 36.3359 791.46 40.8161 794.458 46.5435C797.467 52.2708 798.966 58.9104 798.966 66.4737V72.8939H744.726C744.923 77.9746 746.829 82.0969 750.431 85.2839C754.033 88.4709 758.449 90.0644 763.654 90.0644C768.86 90.0644 772.868 88.9212 775.378 86.6349C777.876 84.3486 779.782 81.8082 781.084 79.0254L796.561 87.0852C795.155 89.6718 793.133 92.4893 790.472 95.5262C787.811 98.563 784.291 101.15 779.875 103.286C775.471 105.422 769.859 106.496 763.05 106.496V106.484ZM744.877 58.8527H779.736C779.329 54.5687 777.609 51.1392 774.553 48.5527C771.497 45.9661 767.512 44.6729 762.608 44.6729C757.705 44.6729 753.44 45.9661 750.443 48.5527C747.433 51.1392 745.586 54.5803 744.888 58.8527H744.877Z" fill="#1C2026"/>
<path d="M808.272 104.395V30.3436H826.898V38.4035H829.606C830.907 35.9209 833.057 33.75 836.066 31.914C839.076 30.0781 843.026 29.1543 847.93 29.1543C853.24 29.1543 857.493 30.1704 860.7 32.2143C863.907 34.2581 866.359 36.9139 868.067 40.2048H870.774C872.471 37.0178 874.876 34.3851 877.99 32.2951C881.092 30.2051 885.496 29.1658 891.213 29.1658C895.814 29.1658 899.997 30.1358 903.762 32.0757C907.515 34.0156 910.525 36.9486 912.779 40.8861C915.033 44.8237 916.16 49.7658 916.16 55.7472V104.418H897.232V57.0866C897.232 53.0105 896.175 49.9506 894.083 47.9067C891.98 45.8629 889.017 44.8468 885.217 44.8468C880.906 44.8468 877.583 46.2209 875.224 48.9575C872.866 51.6942 871.692 55.5971 871.692 60.6778V104.43H852.764V57.0982C852.764 53.0221 851.706 49.9621 849.615 47.9183C847.512 45.8744 844.549 44.8583 840.749 44.8583C836.438 44.8583 833.115 46.2324 830.756 48.9691C828.397 51.7057 827.224 55.6086 827.224 60.6893V104.441H808.295L808.272 104.395Z" fill="#1C2026"/>
<path d="M964.534 106.484C957.12 106.484 950.462 104.995 944.548 102.004C938.633 99.0134 933.974 94.6832 930.569 89.0136C927.165 83.344 925.457 76.5197 925.457 68.5637V66.1735C925.457 58.206 927.153 51.3933 930.569 45.7236C933.974 40.054 938.633 35.7239 944.548 32.7332C950.451 29.7425 957.12 28.2529 964.534 28.2529C971.947 28.2529 978.605 29.7425 984.519 32.7332C990.422 35.7239 995.082 40.054 998.498 45.7236C1001.9 51.3933 1003.6 58.2176 1003.6 66.1735V68.5637C1003.6 76.5312 1001.89 83.344 998.498 89.0136C995.093 94.6832 990.434 99.0134 984.519 102.004C978.605 104.995 971.947 106.484 964.534 106.484ZM964.534 89.7642C970.343 89.7642 975.142 87.8935 978.954 84.1638C982.765 80.4341 984.659 75.0763 984.659 68.1134V66.6238C984.659 59.661 982.776 54.3031 979.023 50.5734C975.27 46.8437 970.436 44.9731 964.522 44.9731C958.608 44.9731 953.902 46.8437 950.102 50.5734C946.291 54.3031 944.397 59.661 944.397 66.6238V68.1134C944.397 75.0763 946.291 80.4341 950.102 84.1638C953.913 87.8935 958.712 89.7642 964.522 89.7642H964.534Z" fill="#1C2026"/>
<path d="M1013.51 104.396V30.3442H1032.14V38.7042H1034.84C1035.95 35.7136 1037.77 33.5312 1040.33 32.134C1042.88 30.7368 1045.86 30.0439 1049.26 30.0439H1058.28V46.7641H1048.96C1044.15 46.7641 1040.2 48.0343 1037.1 50.5746C1034 53.115 1032.44 57.0179 1032.44 62.2949V104.396H1013.51Z" fill="#1C2026"/>
<path d="M1073.61 134.256V117.836H1114.17C1116.97 117.836 1118.38 116.347 1118.38 113.356V94.696H1115.67C1114.87 96.3934 1113.61 98.0793 1111.92 99.7767C1110.21 101.474 1107.91 102.86 1105 103.957C1102.1 105.054 1098.39 105.596 1093.88 105.596C1088.07 105.596 1082.98 104.28 1078.64 101.636C1074.28 99.0031 1070.9 95.3426 1068.49 90.6661C1066.09 85.9895 1064.89 80.6086 1064.89 74.5464V30.3442H1083.82V73.0453C1083.82 78.6225 1085.19 82.8025 1087.96 85.5854C1090.71 88.3682 1094.64 89.7654 1099.75 89.7654C1105.56 89.7654 1110.07 87.8486 1113.27 84.015C1116.48 80.1814 1118.09 74.8351 1118.09 67.9646V30.3442H1137.01V117.536C1137.01 122.617 1135.51 126.67 1132.51 129.707C1129.5 132.744 1125.49 134.256 1120.49 134.256H1073.62H1073.61Z" fill="#1C2026"/>
<path d="M167.067 53.1049H105.065V0H85.0325V57.6198C85.0325 63.7398 87.4842 69.6172 91.8416 73.9474L142.468 124.258L156.633 110.182L119.241 73.0236H167.079V53.1165L167.067 53.1049Z" fill="#1C2026"/>
<path d="M10.446 24.4567L47.838 61.6152H0V81.5223H62.0023V134.627H82.0345V77.0074C82.0345 70.8875 79.5828 65.01 75.2254 60.6798L24.6103 10.3809L10.446 24.4567Z" fill="#1C2026"/>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

View file

@ -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"]
}
}

View file

@ -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<string> {
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<T>(
endpoint: string,
options: RequestInit = {}
endpoint: string,
options: RequestInit = {},
): Promise<T> {
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<Project[]> {
try {
const response = await makeAuthenticatedRequest<ProjectsResponse>('/v3/projects');
return response.projects;
} catch (error) {
console.error('Failed to fetch projects:', error);
throw error;
}
try {
const response =
await makeAuthenticatedRequest<ProjectsResponse>("/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<Project[]> {
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<Project[]> {
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<Project | 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;
}
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<void> {
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<any> {
try {
const response = await makeAuthenticatedRequest<any>('/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<unknown> {
try {
const response = await makeAuthenticatedRequest<unknown>("/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<any> {
try {
const response = await makeAuthenticatedRequest<any>('/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<unknown> {
try {
const response = await makeAuthenticatedRequest<unknown>("/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<void> {
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;
}
}
export async function saveTweet(
content: string,
metadata: { sm_source: string; [key: string]: unknown },
containerTag: string = "sm_project_twitter_bookmarks",
): Promise<void> {
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;
}
}

View file

@ -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;
TOAST_STYLES_ID: "supermemory-toast-styles",
SPINNER_STYLES_ID: "supermemory-spinner-styles",
} as const;

View file

@ -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<TwitterAuthTokens | null> {
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<TwitterAuthTokens | null> {
* @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;
}
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;
}

View file

@ -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<void>;
export type ImportCompleteCallback = (totalImported: number) => Promise<void>;
export interface TwitterImportConfig {
onProgress: ImportProgressCallback;
onComplete: ImportCompleteCallback;
onError: (error: Error) => Promise<void>;
onProgress: ImportProgressCallback;
onComplete: ImportCompleteCallback;
onError: (error: Error) => Promise<void>;
}
/**
* Rate limiting configuration
*/
class RateLimiter {
private waitTime = 60000; // Start with 1 minute
async handleRateLimit(onProgress: ImportProgressCallback): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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);
}
}
}
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);
}
}
}

View file

@ -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<any>;
};
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<string, unknown>;
}>;
}>;
};
};
};
}
// 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<string, unknown>,
): 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<Record<string, unknown>>,
): 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<details>\n<summary>Raw Tweet Data</summary>\n\n\`\`\`json\n${JSON.stringify(tweet, null, 2)}\n\`\`\`\n</details>`;
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<details>\n<summary>Raw Tweet Data</summary>\n\n\`\`\`json\n${JSON.stringify(tweet, null, 2)}\n\`\`\`\n</details>`;
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;
}
const variables = {
count,
includePromotedContent: false,
};
if (cursor) {
(variables as Record<string, unknown>).cursor = cursor;
}
return variables;
}

View file

@ -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<T = any> {
success: boolean;
data?: T;
error?: string;
export interface APIResponse<T = unknown> {
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[];
}
projects: Project[];
}

View file

@ -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 = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 6V2" stroke="#6366f1" stroke-width="2" stroke-linecap="round"/>
<path d="M12 22V18" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
@ -76,32 +111,33 @@ export function createToast(state: ToastState): HTMLElement {
<path d="M5.64 6.36L3.51 8.49" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
</svg>
`;
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 = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`;
text.textContent = 'Added to Memory';
break;
case "success": {
const iconUrl = browser.runtime.getURL("/icon-16.png");
icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`;
text.textContent = "Added to Memory";
break;
}
case 'error':
icon.innerHTML = `
case "error":
icon.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#ef4444"/>
<path d="M15 9L9 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 9L15 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;
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 = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
`;
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 = `
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
<div style="display: flex; align-items: center; gap: 8px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="#1d9bf0">
@ -192,7 +228,9 @@ export function createTwitterImportUI(
</button>
</div>
${isAuthenticated ? `
${
isAuthenticated
? `
<div>
<p style="color: #536471; font-size: 14px; margin: 0 0 12px 0; line-height: 1.4;">
This will import all your Twitter bookmarks to Supermemory
@ -204,7 +242,8 @@ export function createTwitterImportUI(
<div id="${ELEMENT_IDS.TWITTER_IMPORT_STATUS}"></div>
</div>
` : `
`
: `
<div style="text-align: center;">
<p style="color: #536471; font-size: 14px; margin: 0 0 12px 0;">
Please sign in to Supermemory first
@ -213,7 +252,8 @@ export function createTwitterImportUI(
Sign In
</button>
</div>
`}
`
}
<style>
@keyframes spin {
@ -223,19 +263,23 @@ export function createTwitterImportUI(
</style>
`;
// 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 = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
`;
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 = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 50%;" />
`;
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;
}
};
return toast;
},
};

View file

@ -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: ['<all_urls>'],
},
],
},
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: ["<all_urls>"],
},
],
},
webExt: {
disabled: true,
},
});