mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-06 08:16:03 +00:00
extension: updated minor changes
This commit is contained in:
parent
10081a5ae2
commit
e97d642217
16 changed files with 848 additions and 912 deletions
|
|
@ -1,55 +1,55 @@
|
|||
import { getDefaultProject, saveMemory, searchMemories } from "../utils/api";
|
||||
import { getDefaultProject, saveMemory, searchMemories } from "../utils/api"
|
||||
import {
|
||||
CONTAINER_TAGS,
|
||||
CONTEXT_MENU_IDS,
|
||||
MESSAGE_TYPES,
|
||||
} from "../utils/constants";
|
||||
import { captureTwitterTokens } from "../utils/twitter-auth";
|
||||
} from "../utils/constants"
|
||||
import { captureTwitterTokens } from "../utils/twitter-auth"
|
||||
import {
|
||||
type TwitterImportConfig,
|
||||
TwitterImporter,
|
||||
} from "../utils/twitter-import";
|
||||
} from "../utils/twitter-import"
|
||||
import type {
|
||||
ExtensionMessage,
|
||||
MemoryData,
|
||||
MemoryPayload,
|
||||
} from "../utils/types";
|
||||
} from "../utils/types"
|
||||
|
||||
interface SearchResponse {
|
||||
results: Array<{
|
||||
chunks: Array<{
|
||||
content: string;
|
||||
}>;
|
||||
}>;
|
||||
content: string
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
export default defineBackground(() => {
|
||||
let twitterImporter: TwitterImporter | null = null;
|
||||
let twitterImporter: TwitterImporter | null = null
|
||||
|
||||
browser.runtime.onInstalled.addListener((details) => {
|
||||
browser.contextMenus.create({
|
||||
id: CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY,
|
||||
title: "Save to supermemory",
|
||||
contexts: ["selection", "page", "link"],
|
||||
});
|
||||
})
|
||||
|
||||
// Open welcome tab on first install
|
||||
if (details.reason === "install") {
|
||||
browser.tabs.create({
|
||||
url: browser.runtime.getURL("/welcome.html"),
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Intercept Twitter requests to capture authentication headers.
|
||||
browser.webRequest.onBeforeSendHeaders.addListener(
|
||||
(details) => {
|
||||
captureTwitterTokens(details);
|
||||
return {};
|
||||
captureTwitterTokens(details)
|
||||
return {}
|
||||
},
|
||||
{ urls: ["*://x.com/*", "*://twitter.com/*"] },
|
||||
["requestHeaders", "extraHeaders"],
|
||||
);
|
||||
)
|
||||
|
||||
// Handle context menu clicks.
|
||||
browser.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||
|
|
@ -58,27 +58,27 @@ export default defineBackground(() => {
|
|||
try {
|
||||
await browser.tabs.sendMessage(tab.id, {
|
||||
action: MESSAGE_TYPES.SAVE_MEMORY,
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to send message to content script:", error);
|
||||
console.error("Failed to send message to content script:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Send message to current active tab.
|
||||
const sendMessageToCurrentTab = async (message: string) => {
|
||||
const tabs = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
})
|
||||
if (tabs.length > 0 && tabs[0].id) {
|
||||
await browser.tabs.sendMessage(tabs[0].id, {
|
||||
type: MESSAGE_TYPES.IMPORT_UPDATE,
|
||||
importedMessage: message,
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Send import completion message
|
||||
|
|
@ -87,14 +87,14 @@ export default defineBackground(() => {
|
|||
const tabs = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
})
|
||||
if (tabs.length > 0 && tabs[0].id) {
|
||||
await browser.tabs.sendMessage(tabs[0].id, {
|
||||
type: MESSAGE_TYPES.IMPORT_DONE,
|
||||
totalImported,
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save memory to supermemory API
|
||||
|
|
@ -103,48 +103,48 @@ export default defineBackground(() => {
|
|||
data: MemoryData,
|
||||
): Promise<{ success: boolean; data?: unknown; error?: string }> => {
|
||||
try {
|
||||
let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT;
|
||||
let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT
|
||||
try {
|
||||
const defaultProject = await getDefaultProject();
|
||||
const defaultProject = await getDefaultProject()
|
||||
if (defaultProject?.containerTag) {
|
||||
containerTag = defaultProject.containerTag;
|
||||
containerTag = defaultProject.containerTag
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to get default project, using fallback:", error);
|
||||
console.warn("Failed to get default project, using fallback:", error)
|
||||
}
|
||||
|
||||
const payload: MemoryPayload = {
|
||||
containerTags: [containerTag],
|
||||
content: `${data.highlightedText}\n\n${data.html}\n\n${data?.url}`,
|
||||
metadata: { sm_source: "consumer" },
|
||||
};
|
||||
}
|
||||
|
||||
const responseData = await saveMemory(payload);
|
||||
return { success: true, data: responseData };
|
||||
const responseData = await saveMemory(payload)
|
||||
return { success: true, data: responseData }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getRelatedMemories = async (
|
||||
data: string,
|
||||
): Promise<{ success: boolean; data?: unknown; error?: string }> => {
|
||||
try {
|
||||
const responseData = await searchMemories(data);
|
||||
const responseData = await searchMemories(data)
|
||||
const content = (responseData as SearchResponse).results[0].chunks[0]
|
||||
.content;
|
||||
console.log("Content:", content);
|
||||
return { success: true, data: content };
|
||||
.content
|
||||
console.log("Content:", content)
|
||||
return { success: true, data: content }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle extension messages
|
||||
|
|
@ -157,48 +157,48 @@ export default defineBackground(() => {
|
|||
onProgress: sendMessageToCurrentTab,
|
||||
onComplete: sendImportDoneMessage,
|
||||
onError: async (error: Error) => {
|
||||
await sendMessageToCurrentTab(`Error: ${error.message}`);
|
||||
await sendMessageToCurrentTab(`Error: ${error.message}`)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
twitterImporter = new TwitterImporter(importConfig);
|
||||
twitterImporter.startImport().catch(console.error);
|
||||
sendResponse({ success: true });
|
||||
return true;
|
||||
twitterImporter = new TwitterImporter(importConfig)
|
||||
twitterImporter.startImport().catch(console.error)
|
||||
sendResponse({ success: true })
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle regular memory save request
|
||||
if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
|
||||
(async () => {
|
||||
;(async () => {
|
||||
try {
|
||||
const result = await saveMemoryToSupermemory(
|
||||
message.data as MemoryData,
|
||||
);
|
||||
sendResponse(result);
|
||||
)
|
||||
sendResponse(result)
|
||||
} catch (error) {
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
})
|
||||
}
|
||||
})();
|
||||
return true;
|
||||
})()
|
||||
return true
|
||||
}
|
||||
|
||||
if (message.action === MESSAGE_TYPES.GET_RELATED_MEMORIES) {
|
||||
(async () => {
|
||||
;(async () => {
|
||||
try {
|
||||
const result = await getRelatedMemories(message.data as string);
|
||||
sendResponse(result);
|
||||
const result = await getRelatedMemories(message.data as string)
|
||||
sendResponse(result)
|
||||
} catch (error) {
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
})
|
||||
}
|
||||
})();
|
||||
return true;
|
||||
})()
|
||||
return true
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,85 +1,85 @@
|
|||
import { DOMAINS, ELEMENT_IDS, MESSAGE_TYPES } from "../utils/constants";
|
||||
import { DOMAINS, ELEMENT_IDS, MESSAGE_TYPES } from "../utils/constants"
|
||||
import {
|
||||
createChatGPTInputBarElement,
|
||||
createSaveTweetElement,
|
||||
createTwitterImportButton,
|
||||
createTwitterImportUI,
|
||||
DOMUtils,
|
||||
} from "../utils/ui-components";
|
||||
} from "../utils/ui-components"
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["<all_urls>"],
|
||||
main() {
|
||||
let twitterImportUI: HTMLElement | null = null;
|
||||
let isTwitterImportOpen = false;
|
||||
let twitterImportUI: HTMLElement | null = null
|
||||
let isTwitterImportOpen = false
|
||||
|
||||
browser.runtime.onMessage.addListener(async (message) => {
|
||||
if (message.action === MESSAGE_TYPES.SHOW_TOAST) {
|
||||
DOMUtils.showToast(message.state);
|
||||
DOMUtils.showToast(message.state)
|
||||
} else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
|
||||
await saveMemory();
|
||||
await saveMemory()
|
||||
} else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
|
||||
updateTwitterImportUI(message);
|
||||
updateTwitterImportUI(message)
|
||||
} else if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
|
||||
updateTwitterImportUI(message);
|
||||
updateTwitterImportUI(message)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const observeForMemoriesDialog = () => {
|
||||
const observer = new MutationObserver(() => {
|
||||
if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
|
||||
addSupermemoryButtonToMemoriesDialog();
|
||||
addSaveChatGPTElementBeforeComposerBtn();
|
||||
addSupermemoryButtonToMemoriesDialog()
|
||||
addSaveChatGPTElementBeforeComposerBtn()
|
||||
}
|
||||
if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
|
||||
addTwitterImportButton();
|
||||
addTwitterImportButton()
|
||||
//addSaveTweetElement();
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
})
|
||||
|
||||
if (
|
||||
window.location.hostname === "chatgpt.com" ||
|
||||
window.location.hostname === "chat.openai.com"
|
||||
) {
|
||||
addSupermemoryButtonToMemoriesDialog();
|
||||
addSaveChatGPTElementBeforeComposerBtn();
|
||||
addSupermemoryButtonToMemoriesDialog()
|
||||
addSaveChatGPTElementBeforeComposerBtn()
|
||||
}
|
||||
if (
|
||||
window.location.hostname === "x.com" ||
|
||||
window.location.hostname === "twitter.com"
|
||||
) {
|
||||
addTwitterImportButton();
|
||||
addTwitterImportButton()
|
||||
//addSaveTweetElement();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
|
||||
setTimeout(() => {
|
||||
addTwitterImportButton(); // Wait 2 seconds for page to load
|
||||
addTwitterImportButton() // Wait 2 seconds for page to load
|
||||
//addSaveTweetElement();
|
||||
}, 2000);
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", observeForMemoriesDialog);
|
||||
document.addEventListener("DOMContentLoaded", observeForMemoriesDialog)
|
||||
} else {
|
||||
observeForMemoriesDialog();
|
||||
observeForMemoriesDialog()
|
||||
}
|
||||
|
||||
async function saveMemory() {
|
||||
try {
|
||||
DOMUtils.showToast("loading");
|
||||
DOMUtils.showToast("loading")
|
||||
|
||||
const highlightedText = window.getSelection()?.toString() || "";
|
||||
const highlightedText = window.getSelection()?.toString() || ""
|
||||
|
||||
const url = window.location.href;
|
||||
const url = window.location.href
|
||||
|
||||
const html = document.documentElement.outerHTML;
|
||||
const html = document.documentElement.outerHTML
|
||||
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.SAVE_MEMORY,
|
||||
|
|
@ -88,75 +88,75 @@ export default defineContentScript({
|
|||
highlightedText,
|
||||
url,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
console.log("Response from enxtension:", response);
|
||||
console.log("Response from enxtension:", response)
|
||||
if (response.success) {
|
||||
DOMUtils.showToast("success");
|
||||
DOMUtils.showToast("success")
|
||||
} else {
|
||||
DOMUtils.showToast("error");
|
||||
DOMUtils.showToast("error")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving memory:", error);
|
||||
DOMUtils.showToast("error");
|
||||
console.error("Error saving memory:", error)
|
||||
DOMUtils.showToast("error")
|
||||
}
|
||||
}
|
||||
|
||||
async function getRelatedMemories() {
|
||||
try {
|
||||
const userQuery =
|
||||
document.getElementById("prompt-textarea")?.textContent || "";
|
||||
document.getElementById("prompt-textarea")?.textContent || ""
|
||||
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
|
||||
data: userQuery,
|
||||
});
|
||||
})
|
||||
|
||||
if (response.success && response.data) {
|
||||
const promptElement = document.getElementById("prompt-textarea");
|
||||
const promptElement = document.getElementById("prompt-textarea")
|
||||
if (promptElement) {
|
||||
const currentContent = promptElement.innerHTML;
|
||||
promptElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`;
|
||||
const currentContent = promptElement.innerHTML
|
||||
promptElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting related memories:", error);
|
||||
console.error("Error getting related memories:", error)
|
||||
}
|
||||
}
|
||||
|
||||
function addSupermemoryButtonToMemoriesDialog() {
|
||||
const dialogs = document.querySelectorAll('[role="dialog"]');
|
||||
let memoriesDialog: HTMLElement | null = null;
|
||||
const dialogs = document.querySelectorAll('[role="dialog"]')
|
||||
let memoriesDialog: HTMLElement | null = null
|
||||
|
||||
for (const dialog of dialogs) {
|
||||
const headerText = dialog.querySelector("h2");
|
||||
const headerText = dialog.querySelector("h2")
|
||||
if (headerText?.textContent?.includes("Saved memories")) {
|
||||
memoriesDialog = dialog as HTMLElement;
|
||||
break;
|
||||
memoriesDialog = dialog as HTMLElement
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!memoriesDialog) return;
|
||||
if (!memoriesDialog) return
|
||||
|
||||
if (memoriesDialog.querySelector("#supermemory-save-button")) return;
|
||||
if (memoriesDialog.querySelector("#supermemory-save-button")) return
|
||||
|
||||
const deleteAllContainer = memoriesDialog.querySelector(
|
||||
".mt-5.flex.justify-end",
|
||||
);
|
||||
if (!deleteAllContainer) return;
|
||||
)
|
||||
if (!deleteAllContainer) return
|
||||
|
||||
const supermemoryButton = document.createElement("button");
|
||||
supermemoryButton.id = "supermemory-save-button";
|
||||
supermemoryButton.className = "btn relative btn-primary-outline mr-2";
|
||||
const supermemoryButton = document.createElement("button")
|
||||
supermemoryButton.id = "supermemory-save-button"
|
||||
supermemoryButton.className = "btn relative btn-primary-outline mr-2"
|
||||
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png");
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png")
|
||||
|
||||
supermemoryButton.innerHTML = `
|
||||
<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 = `
|
||||
background: #1C2026 !important;
|
||||
|
|
@ -168,101 +168,101 @@ export default defineContentScript({
|
|||
font-size: 14px !important;
|
||||
margin-right: 8px !important;
|
||||
cursor: pointer !important;
|
||||
`;
|
||||
`
|
||||
|
||||
supermemoryButton.addEventListener("mouseenter", () => {
|
||||
supermemoryButton.style.backgroundColor = "#2B2E33";
|
||||
});
|
||||
supermemoryButton.style.backgroundColor = "#2B2E33"
|
||||
})
|
||||
|
||||
supermemoryButton.addEventListener("mouseleave", () => {
|
||||
supermemoryButton.style.backgroundColor = "#1C2026";
|
||||
});
|
||||
supermemoryButton.style.backgroundColor = "#1C2026"
|
||||
})
|
||||
|
||||
supermemoryButton.addEventListener("click", async () => {
|
||||
await saveMemoriesToSupermemory();
|
||||
});
|
||||
await saveMemoriesToSupermemory()
|
||||
})
|
||||
|
||||
deleteAllContainer.insertBefore(
|
||||
supermemoryButton,
|
||||
deleteAllContainer.firstChild,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
async function saveMemoriesToSupermemory() {
|
||||
try {
|
||||
DOMUtils.showToast("loading");
|
||||
DOMUtils.showToast("loading")
|
||||
|
||||
const memoriesTable = document.querySelector(
|
||||
'[role="dialog"] table tbody',
|
||||
);
|
||||
)
|
||||
if (!memoriesTable) {
|
||||
DOMUtils.showToast("error");
|
||||
return;
|
||||
DOMUtils.showToast("error")
|
||||
return
|
||||
}
|
||||
|
||||
const memoryRows = memoriesTable.querySelectorAll("tr");
|
||||
const memories: string[] = [];
|
||||
const memoryRows = memoriesTable.querySelectorAll("tr")
|
||||
const memories: string[] = []
|
||||
|
||||
memoryRows.forEach((row) => {
|
||||
const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap");
|
||||
const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap")
|
||||
if (memoryCell?.textContent) {
|
||||
memories.push(memoryCell.textContent.trim());
|
||||
memories.push(memoryCell.textContent.trim())
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
console.log("Memories:", memories);
|
||||
console.log("Memories:", memories)
|
||||
|
||||
if (memories.length === 0) {
|
||||
DOMUtils.showToast("error");
|
||||
return;
|
||||
DOMUtils.showToast("error")
|
||||
return
|
||||
}
|
||||
|
||||
const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}`;
|
||||
const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}`
|
||||
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: "saveMemory",
|
||||
data: {
|
||||
html: combinedContent,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
DOMUtils.showToast("success");
|
||||
DOMUtils.showToast("success")
|
||||
} else {
|
||||
DOMUtils.showToast("error");
|
||||
DOMUtils.showToast("error")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving memories to supermemory:", error);
|
||||
DOMUtils.showToast("error");
|
||||
console.error("Error saving memories to supermemory:", error)
|
||||
DOMUtils.showToast("error")
|
||||
}
|
||||
}
|
||||
|
||||
function addTwitterImportButton() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const button = createTwitterImportButton(() => {
|
||||
showTwitterImportUI();
|
||||
});
|
||||
showTwitterImportUI()
|
||||
})
|
||||
|
||||
document.body.appendChild(button);
|
||||
document.body.appendChild(button)
|
||||
}
|
||||
|
||||
function showTwitterImportUI() {
|
||||
if (twitterImportUI) {
|
||||
twitterImportUI.remove();
|
||||
twitterImportUI.remove()
|
||||
}
|
||||
|
||||
isTwitterImportOpen = true;
|
||||
isTwitterImportOpen = true
|
||||
|
||||
// Check if user is authenticated
|
||||
browser.storage.local.get(["bearerToken"], ({ bearerToken }) => {
|
||||
const isAuthenticated = !!bearerToken;
|
||||
const isAuthenticated = !!bearerToken
|
||||
|
||||
twitterImportUI = createTwitterImportUI(
|
||||
hideTwitterImportUI,
|
||||
|
|
@ -270,35 +270,35 @@ export default defineContentScript({
|
|||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error starting import:", error);
|
||||
console.error("Error starting import:", error)
|
||||
}
|
||||
},
|
||||
isAuthenticated,
|
||||
);
|
||||
)
|
||||
|
||||
document.body.appendChild(twitterImportUI);
|
||||
});
|
||||
document.body.appendChild(twitterImportUI)
|
||||
})
|
||||
}
|
||||
|
||||
function hideTwitterImportUI() {
|
||||
if (twitterImportUI) {
|
||||
twitterImportUI.remove();
|
||||
twitterImportUI = null;
|
||||
twitterImportUI.remove()
|
||||
twitterImportUI = null
|
||||
}
|
||||
isTwitterImportOpen = false;
|
||||
isTwitterImportOpen = false
|
||||
}
|
||||
|
||||
function updateTwitterImportUI(message: {
|
||||
type: string;
|
||||
importedMessage?: string;
|
||||
totalImported?: number;
|
||||
type: string
|
||||
importedMessage?: string
|
||||
totalImported?: number
|
||||
}) {
|
||||
if (!isTwitterImportOpen || !twitterImportUI) return;
|
||||
if (!isTwitterImportOpen || !twitterImportUI) return
|
||||
|
||||
const statusDiv = twitterImportUI.querySelector("#twitter-import-status");
|
||||
const button = twitterImportUI.querySelector("#twitter-import-button");
|
||||
const statusDiv = twitterImportUI.querySelector("#twitter-import-status")
|
||||
const button = twitterImportUI.querySelector("#twitter-import-button")
|
||||
|
||||
if (message.type === "import-update") {
|
||||
if (statusDiv) {
|
||||
|
|
@ -307,11 +307,11 @@ export default defineContentScript({
|
|||
<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...";
|
||||
;(button as HTMLButtonElement).disabled = true
|
||||
;(button as HTMLButtonElement).textContent = "Importing..."
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -322,100 +322,100 @@ export default defineContentScript({
|
|||
<span style="color: #059669;">✓</span>
|
||||
<span>Successfully imported ${message.totalImported} tweets!</span>
|
||||
</div>
|
||||
`;
|
||||
`
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
hideTwitterImportUI();
|
||||
}, 3000);
|
||||
hideTwitterImportUI()
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
function addSaveChatGPTElementBeforeComposerBtn() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const composerButtons = document.querySelectorAll("button.composer-btn");
|
||||
const composerButtons = document.querySelectorAll("button.composer-btn")
|
||||
|
||||
composerButtons.forEach((button) => {
|
||||
if (button.hasAttribute("data-supermemory-icon-added-before")) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const parent = button.parentElement;
|
||||
if (!parent) return;
|
||||
const parent = button.parentElement
|
||||
if (!parent) return
|
||||
|
||||
const parentSiblings = parent.parentElement?.children;
|
||||
if (!parentSiblings) return;
|
||||
const parentSiblings = parent.parentElement?.children
|
||||
if (!parentSiblings) return
|
||||
|
||||
let hasSpeechButtonSibling = false;
|
||||
let hasSpeechButtonSibling = false
|
||||
for (const sibling of parentSiblings) {
|
||||
if (
|
||||
sibling.getAttribute("data-testid") ===
|
||||
"composer-speech-button-container"
|
||||
) {
|
||||
hasSpeechButtonSibling = true;
|
||||
break;
|
||||
hasSpeechButtonSibling = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasSpeechButtonSibling) return;
|
||||
if (!hasSpeechButtonSibling) return
|
||||
|
||||
const grandParent = parent.parentElement;
|
||||
if (!grandParent) return;
|
||||
const grandParent = parent.parentElement
|
||||
if (!grandParent) return
|
||||
|
||||
const existingIcon = grandParent.querySelector(
|
||||
`#${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer`,
|
||||
);
|
||||
)
|
||||
if (existingIcon) {
|
||||
button.setAttribute("data-supermemory-icon-added-before", "true");
|
||||
return;
|
||||
button.setAttribute("data-supermemory-icon-added-before", "true")
|
||||
return
|
||||
}
|
||||
|
||||
const saveChatGPTElement = createChatGPTInputBarElement(async () => {
|
||||
await getRelatedMemories();
|
||||
});
|
||||
await getRelatedMemories()
|
||||
})
|
||||
|
||||
saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
|
||||
|
||||
button.setAttribute("data-supermemory-icon-added-before", "true");
|
||||
button.setAttribute("data-supermemory-icon-added-before", "true")
|
||||
|
||||
grandParent.insertBefore(saveChatGPTElement, parent);
|
||||
});
|
||||
grandParent.insertBefore(saveChatGPTElement, parent)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Add Tweet Capture Functionality
|
||||
function _addSaveTweetElement() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const targetDivs = document.querySelectorAll(
|
||||
"div.css-175oi2r.r-18u37iz.r-1h0z5md.r-1wron08",
|
||||
);
|
||||
)
|
||||
|
||||
targetDivs.forEach((targetDiv) => {
|
||||
if (targetDiv.hasAttribute("data-supermemory-icon-added")) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const previousElement = targetDiv.previousElementSibling;
|
||||
const previousElement = targetDiv.previousElementSibling
|
||||
if (previousElement?.id?.startsWith(ELEMENT_IDS.SAVE_TWEET_ELEMENT)) {
|
||||
targetDiv.setAttribute("data-supermemory-icon-added", "true");
|
||||
return;
|
||||
targetDiv.setAttribute("data-supermemory-icon-added", "true")
|
||||
return
|
||||
}
|
||||
|
||||
const saveTweetElement = createSaveTweetElement(async () => {
|
||||
await saveMemory();
|
||||
});
|
||||
await saveMemory()
|
||||
})
|
||||
|
||||
saveTweetElement.id = `${ELEMENT_IDS.SAVE_TWEET_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
saveTweetElement.id = `${ELEMENT_IDS.SAVE_TWEET_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
|
||||
|
||||
targetDiv.setAttribute("data-supermemory-icon-added", "true");
|
||||
targetDiv.setAttribute("data-supermemory-icon-added", "true")
|
||||
|
||||
targetDiv.parentNode?.insertBefore(saveTweetElement, targetDiv);
|
||||
});
|
||||
targetDiv.parentNode?.insertBefore(saveTweetElement, targetDiv)
|
||||
})
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", async (event) => {
|
||||
|
|
@ -424,16 +424,16 @@ export default defineContentScript({
|
|||
event.shiftKey &&
|
||||
event.key === "m"
|
||||
) {
|
||||
event.preventDefault();
|
||||
await saveMemory();
|
||||
event.preventDefault()
|
||||
await saveMemory()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const bearerToken = event.data.token;
|
||||
const bearerToken = event.data.token
|
||||
|
||||
if (bearerToken) {
|
||||
if (
|
||||
|
|
@ -445,12 +445,12 @@ export default defineContentScript({
|
|||
) {
|
||||
console.log(
|
||||
"Bearer token is only allowed to be used on localhost or supermemory.ai",
|
||||
);
|
||||
return;
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
chrome.storage.local.set({ bearerToken }, () => {});
|
||||
chrome.storage.local.set({ bearerToken }, () => {})
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,48 +1,50 @@
|
|||
/* Custom Font Definitions */
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-family: "Space Grotesk";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/fonts/SpaceGrotesk-Light.ttf') format('truetype');
|
||||
src: url("/fonts/SpaceGrotesk-Light.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-family: "Space Grotesk";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/fonts/SpaceGrotesk-Regular.ttf') format('truetype');
|
||||
src: url("/fonts/SpaceGrotesk-Regular.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-family: "Space Grotesk";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/fonts/SpaceGrotesk-Medium.ttf') format('truetype');
|
||||
src: url("/fonts/SpaceGrotesk-Medium.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-family: "Space Grotesk";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('/fonts/SpaceGrotesk-SemiBold.ttf') format('truetype');
|
||||
src: url("/fonts/SpaceGrotesk-SemiBold.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-family: "Space Grotesk";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/fonts/SpaceGrotesk-Bold.ttf') format('truetype');
|
||||
src: url("/fonts/SpaceGrotesk-Bold.ttf") format("truetype");
|
||||
}
|
||||
|
||||
.popup-container {
|
||||
width: 320px;
|
||||
padding: 0;
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-family:
|
||||
"Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
sans-serif;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
|
|
@ -81,7 +83,9 @@
|
|||
color: #6c757d;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: color 0.2s ease, background-color 0.2s ease;
|
||||
transition:
|
||||
color 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.header-sign-out:hover {
|
||||
|
|
@ -403,7 +407,9 @@
|
|||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
}
|
||||
|
||||
.secondary-btn:hover {
|
||||
|
|
@ -505,7 +511,9 @@
|
|||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.project-selector-btn:hover .project-selector-content {
|
||||
|
|
@ -599,7 +607,9 @@
|
|||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: color 0.2s ease, background-color 0.2s ease;
|
||||
transition:
|
||||
color 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,181 +1,180 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import "./App.css";
|
||||
import { useEffect, useState } from "react"
|
||||
import "./App.css"
|
||||
import {
|
||||
getDefaultProject,
|
||||
getProjects,
|
||||
setDefaultProject,
|
||||
} from "../../utils/api";
|
||||
import type { Project } from "../../utils/types";
|
||||
} from "../../utils/api"
|
||||
import type { Project } from "../../utils/types"
|
||||
|
||||
function App() {
|
||||
const [userSignedIn, setUserSignedIn] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
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");
|
||||
)
|
||||
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);
|
||||
const result = await chrome.storage.local.get(["bearerToken"])
|
||||
const isSignedIn = !!result.bearerToken
|
||||
setUserSignedIn(isSignedIn)
|
||||
|
||||
if (isSignedIn) {
|
||||
try {
|
||||
const defaultProj = await getDefaultProject();
|
||||
setDefaultProjectState(defaultProj);
|
||||
const defaultProj = await getDefaultProject()
|
||||
setDefaultProjectState(defaultProj)
|
||||
} catch (error) {
|
||||
console.error("Error loading default project:", error);
|
||||
console.error("Error loading default project:", error)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking auth status:", error);
|
||||
setUserSignedIn(false);
|
||||
console.error("Error checking auth status:", error)
|
||||
setUserSignedIn(false)
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getCurrentTab = async () => {
|
||||
try {
|
||||
const tabs = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
})
|
||||
if (tabs.length > 0 && tabs[0].url && tabs[0].title) {
|
||||
setCurrentUrl(tabs[0].url);
|
||||
setCurrentTitle(tabs[0].title);
|
||||
setCurrentUrl(tabs[0].url)
|
||||
setCurrentTitle(tabs[0].title)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting current tab:", error);
|
||||
console.error("Error getting current tab:", error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
checkAuthStatus();
|
||||
getCurrentTab();
|
||||
}, []);
|
||||
checkAuthStatus()
|
||||
getCurrentTab()
|
||||
}, [])
|
||||
|
||||
const loadProjects = async () => {
|
||||
setLoadingProjects(true);
|
||||
setLoadingProjects(true)
|
||||
try {
|
||||
const projectsList = await getProjects();
|
||||
setProjects(projectsList);
|
||||
console.log("Projects:", projectsList);
|
||||
console.log("Default project:", defaultProject);
|
||||
const projectsList = await getProjects()
|
||||
setProjects(projectsList)
|
||||
console.log("Projects:", projectsList)
|
||||
console.log("Default project:", defaultProject)
|
||||
// If no default project is set and projects are available, set first as default
|
||||
if (!defaultProject && projectsList.length > 0) {
|
||||
const firstProject = projectsList[0];
|
||||
await setDefaultProject(firstProject);
|
||||
setDefaultProjectState(firstProject);
|
||||
const firstProject = projectsList[0]
|
||||
await setDefaultProject(firstProject)
|
||||
setDefaultProjectState(firstProject)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading projects:", error);
|
||||
console.error("Error loading projects:", error)
|
||||
} finally {
|
||||
setLoadingProjects(false);
|
||||
setLoadingProjects(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleProjectSelect = async (project: Project) => {
|
||||
try {
|
||||
await setDefaultProject(project);
|
||||
setDefaultProjectState(project);
|
||||
setShowProjectSelector(false);
|
||||
await setDefaultProject(project)
|
||||
setDefaultProjectState(project)
|
||||
setShowProjectSelector(false)
|
||||
} catch (error) {
|
||||
console.error("Error setting default project:", error);
|
||||
console.error("Error setting default project:", error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleShowProjectSelector = () => {
|
||||
console.log("handleShowProjectSelector, projects.length:", projects.length);
|
||||
console.log("handleShowProjectSelector, projects.length:", projects.length)
|
||||
if (projects.length === 0) {
|
||||
loadProjects();
|
||||
loadProjects()
|
||||
}
|
||||
setShowProjectSelector(true);
|
||||
};
|
||||
setShowProjectSelector(true)
|
||||
}
|
||||
|
||||
const handleSaveCurrentPage = async () => {
|
||||
setSaving(true);
|
||||
setSaving(true)
|
||||
try {
|
||||
const tabs = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
})
|
||||
if (tabs.length > 0 && tabs[0].id) {
|
||||
await chrome.tabs.sendMessage(tabs[0].id, {
|
||||
action: "saveMemory",
|
||||
});
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save current page:", error);
|
||||
console.error("Failed to save current page:", error)
|
||||
} finally {
|
||||
setSaving(false);
|
||||
setSaving(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
await chrome.storage.local.remove(["bearerToken"]);
|
||||
setUserSignedIn(false);
|
||||
setDefaultProjectState(null);
|
||||
setProjects([]);
|
||||
await chrome.storage.local.remove(["bearerToken"])
|
||||
setUserSignedIn(false)
|
||||
setDefaultProjectState(null)
|
||||
setProjects([])
|
||||
} catch (error) {
|
||||
console.error("Error signing out:", error);
|
||||
console.error("Error signing out:", error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="popup-container">
|
||||
<div className="header">
|
||||
<img src="/icon-48.png" alt="supermemory" className="logo" />
|
||||
<img alt="supermemory" className="logo" src="/icon-48.png" />
|
||||
<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"
|
||||
src="/logo-trademark.svg"
|
||||
style={{ width: "80%", height: "32px" }}
|
||||
/>
|
||||
{userSignedIn && (
|
||||
<button
|
||||
type="button"
|
||||
className="header-sign-out"
|
||||
onClick={handleSignOut}
|
||||
title="Logout"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
height="16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
>
|
||||
<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" />
|
||||
<line x1="21" x2="9" y1="12" y2="12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
|
@ -186,16 +185,16 @@ function App() {
|
|||
{/* Tab Navigation */}
|
||||
<div className="tab-navigation">
|
||||
<button
|
||||
type="button"
|
||||
className={`tab-btn ${activeTab === "save" ? "active" : ""}`}
|
||||
onClick={() => setActiveTab("save")}
|
||||
type="button"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`tab-btn ${activeTab === "imports" ? "active" : ""}`}
|
||||
onClick={() => setActiveTab("imports")}
|
||||
type="button"
|
||||
>
|
||||
Imports
|
||||
</button>
|
||||
|
|
@ -217,9 +216,9 @@ function App() {
|
|||
{/* Project Selection */}
|
||||
<div className="project-section">
|
||||
<button
|
||||
type="button"
|
||||
className="project-selector-btn"
|
||||
onClick={handleShowProjectSelector}
|
||||
type="button"
|
||||
>
|
||||
<div className="project-selector-content">
|
||||
<span className="project-label">Save to project:</span>
|
||||
|
|
@ -230,16 +229,16 @@ function App() {
|
|||
: "Default Project"}
|
||||
</span>
|
||||
<svg
|
||||
aria-label="Select project"
|
||||
className="project-arrow"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
height="16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-label="Select project"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
>
|
||||
<title>Select project</title>
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
|
|
@ -252,10 +251,10 @@ function App() {
|
|||
{/* Save Button at Bottom */}
|
||||
<div className="save-action">
|
||||
<button
|
||||
type="button"
|
||||
className="login-primary-btn"
|
||||
onClick={handleSaveCurrentPage}
|
||||
disabled={saving}
|
||||
onClick={handleSaveCurrentPage}
|
||||
type="button"
|
||||
>
|
||||
{saving ? "Saving..." : "Save Current Page"}
|
||||
</button>
|
||||
|
|
@ -267,22 +266,23 @@ function App() {
|
|||
<div className="import-actions">
|
||||
<div className="import-item">
|
||||
<button
|
||||
type="button"
|
||||
className="chatgpt-btn"
|
||||
onClick={() => {
|
||||
chrome.tabs.create({
|
||||
url: "https://chatgpt.com/#settings/Personalization",
|
||||
});
|
||||
})
|
||||
}}
|
||||
className="chatgpt-btn"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
className="chatgpt-logo"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="ChatGPT Logo"
|
||||
className="chatgpt-logo"
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>ChatGPT Logo</title>
|
||||
<title>OpenAI</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
|
||||
|
|
@ -291,20 +291,20 @@ function App() {
|
|||
|
||||
<div className="import-item">
|
||||
<button
|
||||
type="button"
|
||||
className="twitter-btn"
|
||||
onClick={() => {
|
||||
chrome.tabs.create({
|
||||
url: "https://x.com/i/bookmarks",
|
||||
});
|
||||
})
|
||||
}}
|
||||
className="twitter-btn"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
className="twitter-logo"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="X Twitter Logo"
|
||||
className="twitter-logo"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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" />
|
||||
|
|
@ -324,9 +324,9 @@ function App() {
|
|||
<div className="project-selector-header">
|
||||
<span>Select the Project</span>
|
||||
<button
|
||||
type="button"
|
||||
className="project-close-btn"
|
||||
onClick={() => setShowProjectSelector(false)}
|
||||
type="button"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
|
@ -337,10 +337,10 @@ function App() {
|
|||
<div className="project-list">
|
||||
{projects.map((project) => (
|
||||
<button
|
||||
key={project.id}
|
||||
type="button"
|
||||
className={`project-item ${defaultProject?.id === project.id ? "selected" : ""}`}
|
||||
key={project.id}
|
||||
onClick={() => handleProjectSelect(project)}
|
||||
type="button"
|
||||
>
|
||||
<div className="project-item-info">
|
||||
<span className="project-item-name">
|
||||
|
|
@ -376,37 +376,37 @@ function App() {
|
|||
|
||||
<div className="login-actions">
|
||||
<p className="login-help">
|
||||
having trouble to login?{" "}
|
||||
Having trouble logging in?{" "}
|
||||
<button
|
||||
type="button"
|
||||
className="help-link"
|
||||
onClick={() => {
|
||||
window.open("mailto:dhravya@supermemory.com", "_blank");
|
||||
window.open("mailto:dhravya@supermemory.com", "_blank")
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
reach out to us
|
||||
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",
|
||||
});
|
||||
})
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Login in
|
||||
login in
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.js";
|
||||
import "./style.css";
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import App from "./App.js"
|
||||
import "./style.css"
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
const rootElement = document.getElementById("root")
|
||||
if (rootElement) {
|
||||
ReactDOM.createRoot(rootElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ function Welcome() {
|
|||
{/* Header */}
|
||||
<div className="welcome-header">
|
||||
<img
|
||||
src="/logo-trademark.svg"
|
||||
alt="supermemory"
|
||||
className="welcome-logo"
|
||||
src="/logo-trademark.svg"
|
||||
/>
|
||||
<p className="welcome-subtitle">
|
||||
Your AI-powered second brain for saving and organizing everything
|
||||
|
|
@ -56,61 +56,21 @@ function Welcome() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Getting Started */}
|
||||
<div className="welcome-getting-started">
|
||||
<h2 className="getting-started-title">
|
||||
Get Started in 2 Simple Steps
|
||||
</h2>
|
||||
|
||||
<div className="steps">
|
||||
<div className="step">
|
||||
<div className="step-number">1</div>
|
||||
<div className="step-content">
|
||||
<h3>Login to Your Account</h3>
|
||||
<p>Connect your supermemory account to start saving content</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="step">
|
||||
<div className="step-number">2</div>
|
||||
<div className="step-content">
|
||||
<h3>Start Saving</h3>
|
||||
<p>Click the extension icon on any page to save it instantly</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="welcome-actions">
|
||||
<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",
|
||||
});
|
||||
})
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Login to Get Started
|
||||
Login to Get started
|
||||
</button>
|
||||
|
||||
<div className="welcome-help">
|
||||
<p className="help-text">
|
||||
Need help getting started?{" "}
|
||||
<button
|
||||
type="button"
|
||||
className="help-link"
|
||||
onClick={() => {
|
||||
window.open("mailto:dhravya@supermemory.com", "_blank");
|
||||
}}
|
||||
>
|
||||
Contact Support
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
|
|
@ -118,10 +78,10 @@ function Welcome() {
|
|||
<p>
|
||||
Learn more at{" "}
|
||||
<a
|
||||
href="https://supermemory.ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="footer-link"
|
||||
href="https://supermemory.ai"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
supermemory.ai
|
||||
</a>
|
||||
|
|
@ -129,7 +89,7 @@ function Welcome() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default Welcome;
|
||||
export default Welcome
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import Welcome from "./Welcome";
|
||||
import "./welcome.css";
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import Welcome from "./Welcome"
|
||||
import "./welcome.css"
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
const rootElement = document.getElementById("root")
|
||||
if (rootElement) {
|
||||
ReactDOM.createRoot(rootElement).render(
|
||||
<React.StrictMode>
|
||||
<Welcome />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,42 @@
|
|||
/* Custom Font Definitions */
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-family: "Space Grotesk";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/fonts/SpaceGrotesk-Light.ttf') format('truetype');
|
||||
src: url("/fonts/SpaceGrotesk-Light.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-family: "Space Grotesk";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/fonts/SpaceGrotesk-Regular.ttf') format('truetype');
|
||||
src: url("/fonts/SpaceGrotesk-Regular.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-family: "Space Grotesk";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/fonts/SpaceGrotesk-Medium.ttf') format('truetype');
|
||||
src: url("/fonts/SpaceGrotesk-Medium.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-family: "Space Grotesk";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('/fonts/SpaceGrotesk-SemiBold.ttf') format('truetype');
|
||||
src: url("/fonts/SpaceGrotesk-SemiBold.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-family: "Space Grotesk";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/fonts/SpaceGrotesk-Bold.ttf') format('truetype');
|
||||
src: url("/fonts/SpaceGrotesk-Bold.ttf") format("truetype");
|
||||
}
|
||||
|
||||
/* Welcome Page Styles */
|
||||
|
|
@ -47,7 +47,9 @@
|
|||
}
|
||||
|
||||
body {
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-family:
|
||||
"Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
sans-serif;
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
line-height: 1.5;
|
||||
|
|
@ -147,18 +149,6 @@ body {
|
|||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Getting Started Section */
|
||||
.welcome-getting-started {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.getting-started-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #000000;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.steps {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
@ -231,31 +221,6 @@ body {
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.welcome-help {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 14px;
|
||||
color: #6c757d;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.help-link {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #4285f4;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.help-link:hover {
|
||||
color: #1a73e8;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.welcome-footer {
|
||||
border-top: 1px solid #e9ecef;
|
||||
|
|
@ -283,28 +248,28 @@ body {
|
|||
.welcome-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
||||
.welcome-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
|
||||
.steps {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.step {
|
||||
max-width: 100%;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
/**
|
||||
* API service for Supermemory browser extension
|
||||
*/
|
||||
import { API_ENDPOINTS, STORAGE_KEYS } from "./constants";
|
||||
import { API_ENDPOINTS, STORAGE_KEYS } from "./constants"
|
||||
import {
|
||||
AuthenticationError,
|
||||
type MemoryPayload,
|
||||
type Project,
|
||||
type ProjectsResponse,
|
||||
SupermemoryAPIError,
|
||||
} from "./types";
|
||||
} from "./types"
|
||||
|
||||
/**
|
||||
* Get bearer token from storage
|
||||
*/
|
||||
async function getBearerToken(): Promise<string> {
|
||||
const result = await chrome.storage.local.get([STORAGE_KEYS.BEARER_TOKEN]);
|
||||
const token = result[STORAGE_KEYS.BEARER_TOKEN];
|
||||
const result = await chrome.storage.local.get([STORAGE_KEYS.BEARER_TOKEN])
|
||||
const token = result[STORAGE_KEYS.BEARER_TOKEN]
|
||||
|
||||
if (!token) {
|
||||
throw new AuthenticationError("Bearer token not found");
|
||||
throw new AuthenticationError("Bearer token not found")
|
||||
}
|
||||
|
||||
return token;
|
||||
return token
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -31,7 +31,7 @@ async function makeAuthenticatedRequest<T>(
|
|||
endpoint: string,
|
||||
options: RequestInit = {},
|
||||
): Promise<T> {
|
||||
const token = await getBearerToken();
|
||||
const token = await getBearerToken()
|
||||
|
||||
const response = await fetch(`${API_ENDPOINTS.SUPERMEMORY_API}${endpoint}`, {
|
||||
...options,
|
||||
|
|
@ -41,19 +41,19 @@ async function makeAuthenticatedRequest<T>(
|
|||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
throw new AuthenticationError("Invalid or expired token");
|
||||
throw new AuthenticationError("Invalid or expired token")
|
||||
}
|
||||
throw new SupermemoryAPIError(
|
||||
`API request failed: ${response.statusText}`,
|
||||
response.status,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return response.json();
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -62,41 +62,39 @@ async function makeAuthenticatedRequest<T>(
|
|||
export async function fetchProjects(): Promise<Project[]> {
|
||||
try {
|
||||
const response =
|
||||
await makeAuthenticatedRequest<ProjectsResponse>("/v3/projects");
|
||||
return response.projects;
|
||||
await makeAuthenticatedRequest<ProjectsResponse>("/v3/projects")
|
||||
return response.projects
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch projects:", error);
|
||||
throw error;
|
||||
console.error("Failed to fetch projects:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get projects from cache or fetch fresh
|
||||
*/
|
||||
export async function getProjects(
|
||||
useCache: boolean = true,
|
||||
): Promise<Project[]> {
|
||||
export async function getProjects(useCache = true): Promise<Project[]> {
|
||||
if (useCache) {
|
||||
try {
|
||||
const cached = await chrome.storage.local.get([
|
||||
STORAGE_KEYS.PROJECTS_CACHE,
|
||||
]);
|
||||
const cachedData = cached[STORAGE_KEYS.PROJECTS_CACHE];
|
||||
])
|
||||
const cachedData = cached[STORAGE_KEYS.PROJECTS_CACHE]
|
||||
|
||||
if (cachedData?.timestamp && cachedData.projects) {
|
||||
// Cache for 5 minutes
|
||||
const cacheAge = Date.now() - cachedData.timestamp;
|
||||
const cacheAge = Date.now() - cachedData.timestamp
|
||||
if (cacheAge < 5 * 60 * 1000) {
|
||||
return cachedData.projects;
|
||||
return cachedData.projects
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to read projects cache:", error);
|
||||
console.warn("Failed to read projects cache:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch fresh data
|
||||
const projects = await fetchProjects();
|
||||
const projects = await fetchProjects()
|
||||
|
||||
// Cache the results
|
||||
try {
|
||||
|
|
@ -105,12 +103,12 @@ export async function getProjects(
|
|||
projects,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn("Failed to cache projects:", error);
|
||||
console.warn("Failed to cache projects:", error)
|
||||
}
|
||||
|
||||
return projects;
|
||||
return projects
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -120,11 +118,11 @@ export async function getDefaultProject(): Promise<Project | null> {
|
|||
try {
|
||||
const result = await chrome.storage.local.get([
|
||||
STORAGE_KEYS.DEFAULT_PROJECT,
|
||||
]);
|
||||
return result[STORAGE_KEYS.DEFAULT_PROJECT] || null;
|
||||
])
|
||||
return result[STORAGE_KEYS.DEFAULT_PROJECT] || null
|
||||
} catch (error) {
|
||||
console.error("Failed to get default project:", error);
|
||||
return null;
|
||||
console.error("Failed to get default project:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,10 +133,10 @@ export async function setDefaultProject(project: Project): Promise<void> {
|
|||
try {
|
||||
await chrome.storage.local.set({
|
||||
[STORAGE_KEYS.DEFAULT_PROJECT]: project,
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to set default project:", error);
|
||||
throw error;
|
||||
console.error("Failed to set default project:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -150,11 +148,11 @@ export async function saveMemory(payload: MemoryPayload): Promise<unknown> {
|
|||
const response = await makeAuthenticatedRequest<unknown>("/v3/memories", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return response;
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error("Failed to save memory:", error);
|
||||
throw error;
|
||||
console.error("Failed to save memory:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,11 +164,11 @@ export async function searchMemories(query: string): Promise<unknown> {
|
|||
const response = await makeAuthenticatedRequest<unknown>("/v3/search", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ q: query }),
|
||||
});
|
||||
return response;
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error("Failed to search memories:", error);
|
||||
throw error;
|
||||
console.error("Failed to search memories:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,20 +178,20 @@ export async function searchMemories(query: string): Promise<unknown> {
|
|||
export async function saveTweet(
|
||||
content: string,
|
||||
metadata: { sm_source: string; [key: string]: unknown },
|
||||
containerTag: string = "sm_project_twitter_bookmarks",
|
||||
containerTag = "sm_project_twitter_bookmarks",
|
||||
): Promise<void> {
|
||||
try {
|
||||
const payload: MemoryPayload = {
|
||||
containerTags: [containerTag],
|
||||
content,
|
||||
metadata,
|
||||
};
|
||||
await saveMemory(payload);
|
||||
}
|
||||
await saveMemory(payload)
|
||||
} catch (error) {
|
||||
if (error instanceof SupermemoryAPIError && error.statusCode === 409) {
|
||||
// Skip if already exists (409 Conflict)
|
||||
return;
|
||||
return
|
||||
}
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export const API_ENDPOINTS = {
|
|||
SUPERMEMORY_WEB: import.meta.env.PROD
|
||||
? "https://app.supermemory.ai"
|
||||
: "http://localhost:3000",
|
||||
} as const;
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Storage Keys
|
||||
|
|
@ -22,7 +22,7 @@ export const STORAGE_KEYS = {
|
|||
TWITTER_AUTH_TOKEN: "auth",
|
||||
DEFAULT_PROJECT: "defaultProject",
|
||||
PROJECTS_CACHE: "projectsCache",
|
||||
} as const;
|
||||
} as const
|
||||
|
||||
/**
|
||||
* DOM Element IDs
|
||||
|
|
@ -37,7 +37,7 @@ export const ELEMENT_IDS = {
|
|||
SUPERMEMORY_SAVE_BUTTON: "supermemory-save-button",
|
||||
SAVE_TWEET_ELEMENT: "supermemory-save-tweet-element",
|
||||
CHATGPT_INPUT_BAR_ELEMENT: "supermemory-chatgpt-input-bar-element",
|
||||
} as const;
|
||||
} as const
|
||||
|
||||
/**
|
||||
* UI Configuration
|
||||
|
|
@ -47,7 +47,7 @@ export const UI_CONFIG = {
|
|||
TOAST_DURATION: 3000, // milliseconds
|
||||
RATE_LIMIT_BASE_WAIT: 60000, // 1 minute
|
||||
PAGINATION_DELAY: 1000, // 1 second between requests
|
||||
} as const;
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Supported Domains
|
||||
|
|
@ -56,7 +56,7 @@ export const DOMAINS = {
|
|||
TWITTER: ["x.com", "twitter.com"],
|
||||
CHATGPT: ["chatgpt.com", "chat.openai.com"],
|
||||
SUPERMEMORY: ["localhost", "supermemory.ai", "app.supermemory.ai"],
|
||||
} as const;
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Container Tags
|
||||
|
|
@ -64,7 +64,7 @@ export const DOMAINS = {
|
|||
export const CONTAINER_TAGS = {
|
||||
TWITTER_BOOKMARKS: "sm_project_twitter_bookmarks",
|
||||
DEFAULT_PROJECT: "sm_project_default",
|
||||
} as const;
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Message Types for extension communication
|
||||
|
|
@ -76,13 +76,13 @@ export const MESSAGE_TYPES = {
|
|||
IMPORT_UPDATE: "import-update",
|
||||
IMPORT_DONE: "import-done",
|
||||
GET_RELATED_MEMORIES: "getRelatedMemories",
|
||||
} as const;
|
||||
} as const
|
||||
|
||||
export const CONTEXT_MENU_IDS = {
|
||||
SAVE_TO_SUPERMEMORY: "save-to-supermemory",
|
||||
} as const;
|
||||
} as const
|
||||
|
||||
export const CSS_CLASSES = {
|
||||
TOAST_STYLES_ID: "supermemory-toast-styles",
|
||||
SPINNER_STYLES_ID: "supermemory-spinner-styles",
|
||||
} as const;
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
*/
|
||||
|
||||
export interface TwitterAuthTokens {
|
||||
cookie: string;
|
||||
csrf: string;
|
||||
auth: string;
|
||||
cookie: string
|
||||
csrf: string
|
||||
auth: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -16,41 +16,41 @@ export interface TwitterAuthTokens {
|
|||
*/
|
||||
export function captureTwitterTokens(
|
||||
details: chrome.webRequest.WebRequestDetails & {
|
||||
requestHeaders?: chrome.webRequest.HttpHeader[];
|
||||
requestHeaders?: chrome.webRequest.HttpHeader[]
|
||||
},
|
||||
): boolean {
|
||||
if (!(details.url.includes("x.com") || details.url.includes("twitter.com"))) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
const authHeader = details.requestHeaders?.find(
|
||||
(header) => header.name.toLowerCase() === "authorization",
|
||||
);
|
||||
)
|
||||
const cookieHeader = details.requestHeaders?.find(
|
||||
(header) => header.name.toLowerCase() === "cookie",
|
||||
);
|
||||
)
|
||||
const csrfHeader = details.requestHeaders?.find(
|
||||
(header) => header.name.toLowerCase() === "x-csrf-token",
|
||||
);
|
||||
)
|
||||
|
||||
if (authHeader?.value && cookieHeader?.value && csrfHeader?.value) {
|
||||
chrome.storage.session.get(["tokens_logged"], (result) => {
|
||||
if (!result.tokens_logged) {
|
||||
console.log("Twitter auth tokens captured successfully");
|
||||
chrome.storage.session.set({ tokens_logged: true });
|
||||
console.log("Twitter auth tokens captured successfully")
|
||||
chrome.storage.session.set({ tokens_logged: true })
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
chrome.storage.session.set({
|
||||
cookie: cookieHeader.value,
|
||||
csrf: csrfHeader.value,
|
||||
auth: authHeader.value,
|
||||
});
|
||||
})
|
||||
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -58,17 +58,17 @@ export function captureTwitterTokens(
|
|||
* @returns Promise resolving to tokens or null if not available
|
||||
*/
|
||||
export async function getTwitterTokens(): Promise<TwitterAuthTokens | null> {
|
||||
const result = await chrome.storage.session.get(["cookie", "csrf", "auth"]);
|
||||
const result = await chrome.storage.session.get(["cookie", "csrf", "auth"])
|
||||
|
||||
if (!result.cookie || !result.csrf || !result.auth) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
cookie: result.cookie,
|
||||
csrf: result.csrf,
|
||||
auth: result.auth,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -77,16 +77,16 @@ export async function getTwitterTokens(): Promise<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");
|
||||
const headers = new Headers()
|
||||
headers.append("Cookie", tokens.cookie)
|
||||
headers.append("X-Csrf-Token", tokens.csrf)
|
||||
headers.append("Authorization", tokens.auth)
|
||||
headers.append("Content-Type", "application/json")
|
||||
headers.append(
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
||||
);
|
||||
headers.append("Accept", "*/*");
|
||||
headers.append("Accept-Language", "en-US,en;q=0.9");
|
||||
return headers;
|
||||
)
|
||||
headers.append("Accept", "*/*")
|
||||
headers.append("Accept-Language", "en-US,en;q=0.9")
|
||||
return headers
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
* Handles the import process for Twitter bookmarks
|
||||
*/
|
||||
|
||||
import { saveTweet } from "./api";
|
||||
import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth";
|
||||
import { saveTweet } from "./api"
|
||||
import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth"
|
||||
import {
|
||||
BOOKMARKS_URL,
|
||||
buildRequestVariables,
|
||||
|
|
@ -13,37 +13,37 @@ import {
|
|||
type Tweet,
|
||||
type TwitterAPIResponse,
|
||||
tweetToMarkdown,
|
||||
} from "./twitter-utils";
|
||||
} from "./twitter-utils"
|
||||
|
||||
export type ImportProgressCallback = (message: string) => Promise<void>;
|
||||
export type ImportProgressCallback = (message: string) => Promise<void>
|
||||
|
||||
export type ImportCompleteCallback = (totalImported: number) => 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
|
||||
private waitTime = 60000 // Start with 1 minute
|
||||
|
||||
async handleRateLimit(onProgress: ImportProgressCallback): Promise<void> {
|
||||
const waitTimeInSeconds = this.waitTime / 1000;
|
||||
const waitTimeInSeconds = this.waitTime / 1000
|
||||
|
||||
await onProgress(
|
||||
`Rate limit reached. Waiting for ${waitTimeInSeconds} seconds before retrying...`,
|
||||
);
|
||||
)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, this.waitTime));
|
||||
this.waitTime *= 2; // Exponential backoff
|
||||
await new Promise((resolve) => setTimeout(resolve, this.waitTime))
|
||||
this.waitTime *= 2 // Exponential backoff
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.waitTime = 60000;
|
||||
this.waitTime = 60000
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,14 +61,14 @@ async function importTweet(tweetMd: string, tweet: Tweet): Promise<void> {
|
|||
created_at: tweet.created_at,
|
||||
likes: tweet.favorite_count,
|
||||
retweets: tweet.retweet_count || 0,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await saveTweet(tweetMd, metadata);
|
||||
await saveTweet(tweetMd, metadata)
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to save tweet: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,8 +76,8 @@ async function importTweet(tweetMd: string, tweet: Tweet): Promise<void> {
|
|||
* Main class for handling Twitter bookmarks import
|
||||
*/
|
||||
export class TwitterImporter {
|
||||
private importInProgress = false;
|
||||
private rateLimiter = new RateLimiter();
|
||||
private importInProgress = false
|
||||
private rateLimiter = new RateLimiter()
|
||||
|
||||
constructor(private config: TwitterImportConfig) {}
|
||||
|
||||
|
|
@ -87,18 +87,18 @@ export class TwitterImporter {
|
|||
*/
|
||||
async startImport(): Promise<void> {
|
||||
if (this.importInProgress) {
|
||||
throw new Error("Import already in progress");
|
||||
throw new Error("Import already in progress")
|
||||
}
|
||||
|
||||
this.importInProgress = true;
|
||||
this.importInProgress = true
|
||||
|
||||
try {
|
||||
await this.batchImportAll("", 0);
|
||||
this.rateLimiter.reset();
|
||||
await this.batchImportAll("", 0)
|
||||
this.rateLimiter.reset()
|
||||
} catch (error) {
|
||||
await this.config.onError(error as Error);
|
||||
await this.config.onError(error as Error)
|
||||
} finally {
|
||||
this.importInProgress = false;
|
||||
this.importInProgress = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,81 +109,84 @@ export class TwitterImporter {
|
|||
*/
|
||||
private async batchImportAll(cursor = "", totalImported = 0): Promise<void> {
|
||||
try {
|
||||
// Use a local variable to track imported count
|
||||
let importedCount = totalImported
|
||||
|
||||
// Get authentication tokens
|
||||
const tokens = await getTwitterTokens();
|
||||
const tokens = await getTwitterTokens()
|
||||
if (!tokens) {
|
||||
await this.config.onProgress(
|
||||
"Please visit Twitter/X first to capture authentication tokens",
|
||||
);
|
||||
return;
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Create headers for API request
|
||||
const headers = createTwitterAPIHeaders(tokens);
|
||||
const headers = createTwitterAPIHeaders(tokens)
|
||||
|
||||
// Build API request with pagination
|
||||
const variables = buildRequestVariables(cursor);
|
||||
const variables = buildRequestVariables(cursor)
|
||||
const urlWithCursor = cursor
|
||||
? `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
|
||||
: BOOKMARKS_URL;
|
||||
: BOOKMARKS_URL
|
||||
|
||||
console.log("Making Twitter API request to:", urlWithCursor);
|
||||
console.log("Request headers:", Object.fromEntries(headers.entries()));
|
||||
console.log("Making Twitter API request to:", urlWithCursor)
|
||||
console.log("Request headers:", Object.fromEntries(headers.entries()))
|
||||
|
||||
const response = await fetch(urlWithCursor, {
|
||||
method: "GET",
|
||||
headers,
|
||||
redirect: "follow",
|
||||
});
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`Twitter API Error ${response.status}:`, errorText);
|
||||
const errorText = await response.text()
|
||||
console.error(`Twitter API Error ${response.status}:`, errorText)
|
||||
|
||||
if (response.status === 429) {
|
||||
await this.rateLimiter.handleRateLimit(this.config.onProgress);
|
||||
return this.batchImportAll(cursor, totalImported);
|
||||
await this.rateLimiter.handleRateLimit(this.config.onProgress)
|
||||
return this.batchImportAll(cursor, totalImported)
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to fetch data: ${response.status} - ${errorText}`,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const data: TwitterAPIResponse = await response.json();
|
||||
const tweets = getAllTweets(data);
|
||||
const data: TwitterAPIResponse = await response.json()
|
||||
const tweets = getAllTweets(data)
|
||||
|
||||
console.log("Tweets:", tweets);
|
||||
console.log("Tweets:", tweets)
|
||||
|
||||
// Process each tweet
|
||||
for (const tweet of tweets) {
|
||||
try {
|
||||
const tweetMd = tweetToMarkdown(tweet);
|
||||
await importTweet(tweetMd, tweet);
|
||||
totalImported++;
|
||||
await this.config.onProgress(`Imported ${totalImported} tweets`);
|
||||
const tweetMd = tweetToMarkdown(tweet)
|
||||
await importTweet(tweetMd, tweet)
|
||||
importedCount++
|
||||
await this.config.onProgress(`Imported ${importedCount} tweets`)
|
||||
} catch (error) {
|
||||
console.error("Error importing tweet:", error);
|
||||
console.error("Error importing tweet:", error)
|
||||
// Continue with next tweet
|
||||
}
|
||||
}
|
||||
|
||||
// Handle pagination
|
||||
const instructions =
|
||||
data.data?.bookmark_timeline_v2?.timeline?.instructions;
|
||||
const nextCursor = extractNextCursor(instructions || []);
|
||||
data.data?.bookmark_timeline_v2?.timeline?.instructions
|
||||
const nextCursor = extractNextCursor(instructions || [])
|
||||
|
||||
console.log("Next cursor:", nextCursor);
|
||||
console.log("Tweets length:", tweets.length);
|
||||
console.log("Next cursor:", nextCursor)
|
||||
console.log("Tweets length:", tweets.length)
|
||||
|
||||
if (nextCursor && tweets.length > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)); // Rate limiting
|
||||
await this.batchImportAll(nextCursor, totalImported);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)) // Rate limiting
|
||||
await this.batchImportAll(nextCursor, importedCount)
|
||||
} else {
|
||||
await this.config.onComplete(totalImported);
|
||||
await this.config.onComplete(importedCount)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Batch import error:", error);
|
||||
await this.config.onError(error as Error);
|
||||
console.error("Batch import error:", error)
|
||||
await this.config.onError(error as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,120 +1,120 @@
|
|||
// Twitter API data structures and transformation utilities
|
||||
|
||||
interface TwitterAPITweet {
|
||||
__typename?: string;
|
||||
__typename?: string
|
||||
legacy: {
|
||||
lang?: string;
|
||||
favorite_count: number;
|
||||
created_at: string;
|
||||
display_text_range?: [number, number];
|
||||
lang?: string
|
||||
favorite_count: number
|
||||
created_at: string
|
||||
display_text_range?: [number, number]
|
||||
entities?: {
|
||||
hashtags?: Array<{ indices: [number, number]; text: string }>;
|
||||
hashtags?: Array<{ indices: [number, number]; text: string }>
|
||||
urls?: Array<{
|
||||
display_url: string;
|
||||
expanded_url: string;
|
||||
indices: [number, number];
|
||||
url: string;
|
||||
}>;
|
||||
display_url: string
|
||||
expanded_url: string
|
||||
indices: [number, number]
|
||||
url: string
|
||||
}>
|
||||
user_mentions?: Array<{
|
||||
id_str: string;
|
||||
indices: [number, number];
|
||||
name: string;
|
||||
screen_name: string;
|
||||
}>;
|
||||
symbols?: Array<{ indices: [number, number]; text: string }>;
|
||||
media?: MediaEntity[];
|
||||
};
|
||||
id_str: string;
|
||||
full_text: string;
|
||||
reply_count?: number;
|
||||
retweet_count?: number;
|
||||
quote_count?: number;
|
||||
};
|
||||
id_str: string
|
||||
indices: [number, number]
|
||||
name: string
|
||||
screen_name: string
|
||||
}>
|
||||
symbols?: Array<{ indices: [number, number]; text: string }>
|
||||
media?: MediaEntity[]
|
||||
}
|
||||
id_str: string
|
||||
full_text: string
|
||||
reply_count?: number
|
||||
retweet_count?: number
|
||||
quote_count?: number
|
||||
}
|
||||
core?: {
|
||||
user_results?: {
|
||||
result?: {
|
||||
legacy?: {
|
||||
id_str: string;
|
||||
name: string;
|
||||
profile_image_url_https: string;
|
||||
screen_name: string;
|
||||
verified: boolean;
|
||||
};
|
||||
is_blue_verified?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
id_str: string
|
||||
name: string
|
||||
profile_image_url_https: string
|
||||
screen_name: string
|
||||
verified: boolean
|
||||
}
|
||||
is_blue_verified?: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface MediaEntity {
|
||||
type: string;
|
||||
media_url_https: string;
|
||||
type: string
|
||||
media_url_https: string
|
||||
sizes?: {
|
||||
large?: {
|
||||
w: number;
|
||||
h: number;
|
||||
};
|
||||
};
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
}
|
||||
video_info?: {
|
||||
variants?: Array<{
|
||||
url: string;
|
||||
}>;
|
||||
duration_millis?: number;
|
||||
};
|
||||
url: string
|
||||
}>
|
||||
duration_millis?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface Tweet {
|
||||
__typename?: string;
|
||||
lang?: string;
|
||||
favorite_count: number;
|
||||
created_at: string;
|
||||
display_text_range?: [number, number];
|
||||
__typename?: string
|
||||
lang?: string
|
||||
favorite_count: number
|
||||
created_at: string
|
||||
display_text_range?: [number, number]
|
||||
entities: {
|
||||
hashtags: Array<{
|
||||
indices: [number, number];
|
||||
text: string;
|
||||
}>;
|
||||
indices: [number, number]
|
||||
text: string
|
||||
}>
|
||||
urls?: Array<{
|
||||
display_url: string;
|
||||
expanded_url: string;
|
||||
indices: [number, number];
|
||||
url: string;
|
||||
}>;
|
||||
display_url: string
|
||||
expanded_url: string
|
||||
indices: [number, number]
|
||||
url: string
|
||||
}>
|
||||
user_mentions: Array<{
|
||||
id_str: string;
|
||||
indices: [number, number];
|
||||
name: string;
|
||||
screen_name: string;
|
||||
}>;
|
||||
id_str: string
|
||||
indices: [number, number]
|
||||
name: string
|
||||
screen_name: string
|
||||
}>
|
||||
symbols: Array<{
|
||||
indices: [number, number];
|
||||
text: string;
|
||||
}>;
|
||||
};
|
||||
id_str: string;
|
||||
text: string;
|
||||
indices: [number, number]
|
||||
text: string
|
||||
}>
|
||||
}
|
||||
id_str: string
|
||||
text: string
|
||||
user: {
|
||||
id_str: string;
|
||||
name: string;
|
||||
profile_image_url_https: string;
|
||||
screen_name: string;
|
||||
verified: boolean;
|
||||
is_blue_verified?: boolean;
|
||||
};
|
||||
conversation_count: number;
|
||||
id_str: string
|
||||
name: string
|
||||
profile_image_url_https: string
|
||||
screen_name: string
|
||||
verified: boolean
|
||||
is_blue_verified?: boolean
|
||||
}
|
||||
conversation_count: number
|
||||
photos?: Array<{
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}>;
|
||||
url: string
|
||||
width: number
|
||||
height: number
|
||||
}>
|
||||
videos?: Array<{
|
||||
url: string;
|
||||
thumbnail_url: string;
|
||||
duration: number;
|
||||
}>;
|
||||
retweet_count?: number;
|
||||
quote_count?: number;
|
||||
reply_count?: number;
|
||||
url: string
|
||||
thumbnail_url: string
|
||||
duration: number
|
||||
}>
|
||||
retweet_count?: number
|
||||
quote_count?: number
|
||||
reply_count?: number
|
||||
}
|
||||
|
||||
export interface TwitterAPIResponse {
|
||||
|
|
@ -122,16 +122,16 @@ export interface TwitterAPIResponse {
|
|||
bookmark_timeline_v2: {
|
||||
timeline: {
|
||||
instructions: Array<{
|
||||
type: string;
|
||||
type: string
|
||||
entries?: Array<{
|
||||
entryId: string;
|
||||
sortIndex: string;
|
||||
content: Record<string, unknown>;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
entryId: string
|
||||
sortIndex: string
|
||||
content: Record<string, unknown>
|
||||
}>
|
||||
}>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Twitter API features configuration
|
||||
|
|
@ -165,9 +165,9 @@ export const TWITTER_API_FEATURES = {
|
|||
articles_preview_enabled: true,
|
||||
rweb_video_timestamps_enabled: true,
|
||||
verified_phone_label_enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
export const BOOKMARKS_URL = `https://x.com/i/api/graphql/xLjCVTqYWz8CGSprLU349w/Bookmarks?features=${encodeURIComponent(JSON.stringify(TWITTER_API_FEATURES))}`;
|
||||
export const BOOKMARKS_URL = `https://x.com/i/api/graphql/xLjCVTqYWz8CGSprLU349w/Bookmarks?features=${encodeURIComponent(JSON.stringify(TWITTER_API_FEATURES))}`
|
||||
|
||||
/**
|
||||
* Transform raw Twitter API response data into standardized Tweet format
|
||||
|
|
@ -177,29 +177,29 @@ export function transformTweetData(
|
|||
): Tweet | null {
|
||||
try {
|
||||
const content = input.content as {
|
||||
itemContent?: { tweet_results?: { result?: unknown } };
|
||||
};
|
||||
const tweetData = content?.itemContent?.tweet_results?.result;
|
||||
itemContent?: { tweet_results?: { result?: unknown } }
|
||||
}
|
||||
const tweetData = content?.itemContent?.tweet_results?.result
|
||||
|
||||
if (!tweetData) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const tweet = tweetData as TwitterAPITweet;
|
||||
const tweet = tweetData as TwitterAPITweet
|
||||
|
||||
if (!tweet.legacy) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Handle media entities
|
||||
const media = (tweet.legacy.entities?.media as MediaEntity[]) || [];
|
||||
const media = (tweet.legacy.entities?.media as MediaEntity[]) || []
|
||||
const photos = media
|
||||
.filter((m) => m.type === "photo")
|
||||
.map((m) => ({
|
||||
url: m.media_url_https,
|
||||
width: m.sizes?.large?.w || 0,
|
||||
height: m.sizes?.large?.h || 0,
|
||||
}));
|
||||
}))
|
||||
|
||||
const videos = media
|
||||
.filter((m) => m.type === "video")
|
||||
|
|
@ -207,7 +207,7 @@ export function transformTweetData(
|
|||
url: m.video_info?.variants?.[0]?.url || "",
|
||||
thumbnail_url: m.media_url_https,
|
||||
duration: m.video_info?.duration_millis || 0,
|
||||
}));
|
||||
}))
|
||||
|
||||
const transformed: Tweet = {
|
||||
__typename: tweet.__typename,
|
||||
|
|
@ -239,20 +239,20 @@ export function transformTweetData(
|
|||
retweet_count: tweet.legacy.retweet_count || 0,
|
||||
quote_count: tweet.legacy.quote_count || 0,
|
||||
reply_count: tweet.legacy.reply_count || 0,
|
||||
};
|
||||
}
|
||||
|
||||
if (photos.length > 0) {
|
||||
transformed.photos = photos;
|
||||
transformed.photos = photos
|
||||
}
|
||||
|
||||
if (videos.length > 0) {
|
||||
transformed.videos = videos;
|
||||
transformed.videos = videos
|
||||
}
|
||||
|
||||
return transformed;
|
||||
return transformed
|
||||
} catch (error) {
|
||||
console.error("Error transforming tweet data:", error);
|
||||
return null;
|
||||
console.error("Error transforming tweet data:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -260,29 +260,29 @@ export function transformTweetData(
|
|||
* Extract all tweets from Twitter API response
|
||||
*/
|
||||
export function getAllTweets(data: TwitterAPIResponse): Tweet[] {
|
||||
const tweets: Tweet[] = [];
|
||||
const tweets: Tweet[] = []
|
||||
|
||||
try {
|
||||
const instructions =
|
||||
data.data?.bookmark_timeline_v2?.timeline?.instructions || [];
|
||||
data.data?.bookmark_timeline_v2?.timeline?.instructions || []
|
||||
|
||||
for (const instruction of instructions) {
|
||||
if (instruction.type === "TimelineAddEntries" && instruction.entries) {
|
||||
for (const entry of instruction.entries) {
|
||||
if (entry.entryId.startsWith("tweet-")) {
|
||||
const tweet = transformTweetData(entry);
|
||||
const tweet = transformTweetData(entry)
|
||||
if (tweet) {
|
||||
tweets.push(tweet);
|
||||
tweets.push(tweet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error extracting tweets:", error);
|
||||
console.error("Error extracting tweets:", error)
|
||||
}
|
||||
|
||||
return tweets;
|
||||
return tweets
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -295,83 +295,83 @@ export function extractNextCursor(
|
|||
for (const instruction of instructions) {
|
||||
if (instruction.type === "TimelineAddEntries" && instruction.entries) {
|
||||
const entries = instruction.entries as Array<{
|
||||
entryId: string;
|
||||
content?: { value?: string };
|
||||
}>;
|
||||
entryId: string
|
||||
content?: { value?: string }
|
||||
}>
|
||||
for (const entry of entries) {
|
||||
if (entry.entryId.startsWith("cursor-bottom-")) {
|
||||
return entry.content?.value || null;
|
||||
return entry.content?.value || null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error extracting cursor:", error);
|
||||
console.error("Error extracting cursor:", error)
|
||||
}
|
||||
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Tweet object to markdown format for storage
|
||||
*/
|
||||
export function tweetToMarkdown(tweet: Tweet): string {
|
||||
const username = tweet.user?.screen_name || "unknown";
|
||||
const displayName = tweet.user?.name || "Unknown User";
|
||||
const date = new Date(tweet.created_at).toLocaleDateString();
|
||||
const time = new Date(tweet.created_at).toLocaleTimeString();
|
||||
const username = tweet.user?.screen_name || "unknown"
|
||||
const displayName = tweet.user?.name || "Unknown User"
|
||||
const date = new Date(tweet.created_at).toLocaleDateString()
|
||||
const time = new Date(tweet.created_at).toLocaleTimeString()
|
||||
|
||||
let markdown = `# Tweet by @${username} (${displayName})\n\n`;
|
||||
markdown += `**Date:** ${date} ${time}\n`;
|
||||
markdown += `**Likes:** ${tweet.favorite_count} | **Retweets:** ${tweet.retweet_count || 0} | **Replies:** ${tweet.reply_count || 0}\n\n`;
|
||||
let markdown = `# Tweet by @${username} (${displayName})\n\n`
|
||||
markdown += `**Date:** ${date} ${time}\n`
|
||||
markdown += `**Likes:** ${tweet.favorite_count} | **Retweets:** ${tweet.retweet_count || 0} | **Replies:** ${tweet.reply_count || 0}\n\n`
|
||||
|
||||
// Add tweet text
|
||||
markdown += `${tweet.text}\n\n`;
|
||||
markdown += `${tweet.text}\n\n`
|
||||
|
||||
// Add media if present
|
||||
if (tweet.photos && tweet.photos.length > 0) {
|
||||
markdown += `**Images:**\n`;
|
||||
markdown += "**Images:**\n"
|
||||
tweet.photos.forEach((photo, index) => {
|
||||
markdown += `\n`;
|
||||
});
|
||||
markdown += "\n";
|
||||
markdown += `\n`
|
||||
})
|
||||
markdown += "\n"
|
||||
}
|
||||
|
||||
if (tweet.videos && tweet.videos.length > 0) {
|
||||
markdown += `**Videos:**\n`;
|
||||
markdown += "**Videos:**\n"
|
||||
tweet.videos.forEach((video, index) => {
|
||||
markdown += `[Video ${index + 1}](${video.url})\n`;
|
||||
});
|
||||
markdown += "\n";
|
||||
markdown += `[Video ${index + 1}](${video.url})\n`
|
||||
})
|
||||
markdown += "\n"
|
||||
}
|
||||
|
||||
// Add hashtags and mentions
|
||||
if (tweet.entities.hashtags.length > 0) {
|
||||
markdown += `**Hashtags:** ${tweet.entities.hashtags.map((h) => `#${h.text}`).join(", ")}\n`;
|
||||
markdown += `**Hashtags:** ${tweet.entities.hashtags.map((h) => `#${h.text}`).join(", ")}\n`
|
||||
}
|
||||
|
||||
if (tweet.entities.user_mentions.length > 0) {
|
||||
markdown += `**Mentions:** ${tweet.entities.user_mentions.map((m) => `@${m.screen_name}`).join(", ")}\n`;
|
||||
markdown += `**Mentions:** ${tweet.entities.user_mentions.map((m) => `@${m.screen_name}`).join(", ")}\n`
|
||||
}
|
||||
|
||||
// Add raw data for reference
|
||||
markdown += `\n---\n<details>\n<summary>Raw Tweet Data</summary>\n\n\`\`\`json\n${JSON.stringify(tweet, null, 2)}\n\`\`\`\n</details>`;
|
||||
markdown += `\n---\n<details>\n<summary>Raw Tweet Data</summary>\n\n\`\`\`json\n${JSON.stringify(tweet, null, 2)}\n\`\`\`\n</details>`
|
||||
|
||||
return markdown;
|
||||
return markdown
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Twitter API request variables for pagination
|
||||
*/
|
||||
export function buildRequestVariables(cursor?: string, count: number = 100) {
|
||||
export function buildRequestVariables(cursor?: string, count = 100) {
|
||||
const variables = {
|
||||
count,
|
||||
includePromotedContent: false,
|
||||
};
|
||||
|
||||
if (cursor) {
|
||||
(variables as Record<string, unknown>).cursor = cursor;
|
||||
}
|
||||
|
||||
return variables;
|
||||
if (cursor) {
|
||||
;(variables as Record<string, unknown>).cursor = cursor
|
||||
}
|
||||
|
||||
return variables
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,99 +5,99 @@
|
|||
/**
|
||||
* Toast states for UI feedback
|
||||
*/
|
||||
export type ToastState = "loading" | "success" | "error";
|
||||
export type ToastState = "loading" | "success" | "error"
|
||||
|
||||
/**
|
||||
* Message types for extension communication
|
||||
*/
|
||||
export interface ExtensionMessage {
|
||||
action?: string;
|
||||
type?: string;
|
||||
data?: unknown;
|
||||
state?: ToastState;
|
||||
importedMessage?: string;
|
||||
totalImported?: number;
|
||||
action?: string
|
||||
type?: string
|
||||
data?: unknown
|
||||
state?: ToastState
|
||||
importedMessage?: string
|
||||
totalImported?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Memory data structure for saving content
|
||||
*/
|
||||
export interface MemoryData {
|
||||
html: string;
|
||||
highlightedText?: string;
|
||||
url?: string;
|
||||
html: string
|
||||
highlightedText?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Supermemory API payload for storing memories
|
||||
*/
|
||||
export interface MemoryPayload {
|
||||
containerTags: string[];
|
||||
content: string;
|
||||
containerTags: string[]
|
||||
content: string
|
||||
metadata: {
|
||||
sm_source: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
sm_source: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Twitter-specific memory metadata
|
||||
*/
|
||||
export interface TwitterMemoryMetadata {
|
||||
sm_source: "twitter_bookmarks";
|
||||
tweet_id: string;
|
||||
author: string;
|
||||
created_at: string;
|
||||
likes: number;
|
||||
retweets: number;
|
||||
sm_source: "twitter_bookmarks"
|
||||
tweet_id: string
|
||||
author: string
|
||||
created_at: string
|
||||
likes: number
|
||||
retweets: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage data structure for Chrome storage
|
||||
*/
|
||||
export interface StorageData {
|
||||
bearerToken?: string;
|
||||
bearerToken?: string
|
||||
twitterAuth?: {
|
||||
cookie: string;
|
||||
csrf: string;
|
||||
auth: string;
|
||||
};
|
||||
tokens_logged?: boolean;
|
||||
cookie?: string;
|
||||
csrf?: string;
|
||||
auth?: string;
|
||||
defaultProject?: Project;
|
||||
cookie: string
|
||||
csrf: string
|
||||
auth: string
|
||||
}
|
||||
tokens_logged?: boolean
|
||||
cookie?: string
|
||||
csrf?: string
|
||||
auth?: string
|
||||
defaultProject?: Project
|
||||
projectsCache?: {
|
||||
projects: Project[];
|
||||
timestamp: number;
|
||||
};
|
||||
projects: Project[]
|
||||
timestamp: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Context menu click info
|
||||
*/
|
||||
export interface ContextMenuClickInfo {
|
||||
menuItemId: string | number;
|
||||
editable?: boolean;
|
||||
frameId?: number;
|
||||
frameUrl?: string;
|
||||
linkUrl?: string;
|
||||
mediaType?: string;
|
||||
pageUrl?: string;
|
||||
parentMenuItemId?: string | number;
|
||||
selectionText?: string;
|
||||
srcUrl?: string;
|
||||
targetElementId?: number;
|
||||
wasChecked?: boolean;
|
||||
menuItemId: string | number
|
||||
editable?: boolean
|
||||
frameId?: number
|
||||
frameUrl?: string
|
||||
linkUrl?: string
|
||||
mediaType?: string
|
||||
pageUrl?: string
|
||||
parentMenuItemId?: string | number
|
||||
selectionText?: string
|
||||
srcUrl?: string
|
||||
targetElementId?: number
|
||||
wasChecked?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* API Response types
|
||||
*/
|
||||
export interface APIResponse<T = unknown> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -109,41 +109,41 @@ export class ExtensionError extends Error {
|
|||
public code?: string,
|
||||
public statusCode?: number,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ExtensionError";
|
||||
super(message)
|
||||
this.name = "ExtensionError"
|
||||
}
|
||||
}
|
||||
|
||||
export class TwitterAPIError extends ExtensionError {
|
||||
constructor(message: string, statusCode?: number) {
|
||||
super(message, "TWITTER_API_ERROR", statusCode);
|
||||
this.name = "TwitterAPIError";
|
||||
super(message, "TWITTER_API_ERROR", statusCode)
|
||||
this.name = "TwitterAPIError"
|
||||
}
|
||||
}
|
||||
|
||||
export class SupermemoryAPIError extends ExtensionError {
|
||||
constructor(message: string, statusCode?: number) {
|
||||
super(message, "SUPERMEMORY_API_ERROR", statusCode);
|
||||
this.name = "SupermemoryAPIError";
|
||||
super(message, "SUPERMEMORY_API_ERROR", statusCode)
|
||||
this.name = "SupermemoryAPIError"
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthenticationError extends ExtensionError {
|
||||
constructor(message: string = "Authentication required") {
|
||||
super(message, "AUTH_ERROR");
|
||||
this.name = "AuthenticationError";
|
||||
constructor(message = "Authentication required") {
|
||||
super(message, "AUTH_ERROR")
|
||||
this.name = "AuthenticationError"
|
||||
}
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
containerTag: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
documentCount: number;
|
||||
id: string
|
||||
name: string
|
||||
containerTag: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
documentCount: number
|
||||
}
|
||||
|
||||
export interface ProjectsResponse {
|
||||
projects: Project[];
|
||||
projects: Project[]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
* Reusable UI components for the browser extension
|
||||
*/
|
||||
|
||||
import { API_ENDPOINTS, ELEMENT_IDS, UI_CONFIG } from "./constants";
|
||||
import type { ToastState } from "./types";
|
||||
import { API_ENDPOINTS, ELEMENT_IDS, UI_CONFIG } from "./constants"
|
||||
import type { ToastState } from "./types"
|
||||
|
||||
/**
|
||||
* Creates a toast notification element
|
||||
|
|
@ -12,8 +12,8 @@ import type { ToastState } from "./types";
|
|||
* @returns HTMLElement - The toast element
|
||||
*/
|
||||
export function createToast(state: ToastState): HTMLElement {
|
||||
const toast = document.createElement("div");
|
||||
toast.id = ELEMENT_IDS.SUPERMEMORY_TOAST;
|
||||
const toast = document.createElement("div")
|
||||
toast.id = ELEMENT_IDS.SUPERMEMORY_TOAST
|
||||
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
|
|
@ -32,12 +32,12 @@ export function createToast(state: ToastState): HTMLElement {
|
|||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
`;
|
||||
`
|
||||
|
||||
// Add keyframe animations and fonts if not already present
|
||||
if (!document.getElementById("supermemory-toast-styles")) {
|
||||
const style = document.createElement("style");
|
||||
style.id = "supermemory-toast-styles";
|
||||
const style = document.createElement("style")
|
||||
style.id = "supermemory-toast-styles"
|
||||
style.textContent = `
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
|
|
@ -86,15 +86,15 @@ export function createToast(state: ToastState): HTMLElement {
|
|||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
|
||||
const icon = document.createElement("div");
|
||||
icon.style.cssText = "width: 20px; height: 20px; flex-shrink: 0;";
|
||||
const icon = document.createElement("div")
|
||||
icon.style.cssText = "width: 20px; height: 20px; flex-shrink: 0;"
|
||||
|
||||
const text = document.createElement("span");
|
||||
text.style.fontWeight = "500";
|
||||
const text = document.createElement("span")
|
||||
text.style.fontWeight = "500"
|
||||
|
||||
// Configure toast based on state
|
||||
switch (state) {
|
||||
|
|
@ -110,16 +110,16 @@ export function createToast(state: ToastState): HTMLElement {
|
|||
<path d="M20.49 15.49L18.36 17.62" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.9"/>
|
||||
<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;
|
||||
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":
|
||||
|
|
@ -129,15 +129,15 @@ export function createToast(state: ToastState): HTMLElement {
|
|||
<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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -146,8 +146,8 @@ export function createToast(state: ToastState): HTMLElement {
|
|||
* @returns HTMLElement - The button element
|
||||
*/
|
||||
export function createTwitterImportButton(onClick: () => void): HTMLElement {
|
||||
const button = document.createElement("div");
|
||||
button.id = ELEMENT_IDS.TWITTER_IMPORT_BUTTON;
|
||||
const button = document.createElement("div")
|
||||
button.id = ELEMENT_IDS.TWITTER_IMPORT_BUTTON
|
||||
button.style.cssText = `
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
|
|
@ -163,26 +163,26 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement {
|
|||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
`;
|
||||
`
|
||||
|
||||
const iconUrl = browser.runtime.getURL("/light-mode-icon.png");
|
||||
const iconUrl = browser.runtime.getURL("/light-mode-icon.png")
|
||||
button.innerHTML = `
|
||||
<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.style.transform = "scale(1.05)"
|
||||
button.style.boxShadow = "0 4px 12px rgba(29, 155, 240, 0.4)"
|
||||
})
|
||||
|
||||
button.addEventListener("mouseleave", () => {
|
||||
button.style.transform = "scale(1)";
|
||||
button.style.boxShadow = "0 2px 8px rgba(29, 155, 240, 0.3)";
|
||||
});
|
||||
button.style.transform = "scale(1)"
|
||||
button.style.boxShadow = "0 2px 8px rgba(29, 155, 240, 0.3)"
|
||||
})
|
||||
|
||||
button.addEventListener("click", onClick);
|
||||
button.addEventListener("click", onClick)
|
||||
|
||||
return button;
|
||||
return button
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -197,7 +197,7 @@ export function createTwitterImportUI(
|
|||
onImport: () => void,
|
||||
isAuthenticated: boolean,
|
||||
): HTMLElement {
|
||||
const container = document.createElement("div");
|
||||
const container = document.createElement("div")
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
|
|
@ -211,7 +211,7 @@ export function createTwitterImportUI(
|
|||
max-width: 400px;
|
||||
border: 1px solid #e1e5e9;
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
`;
|
||||
`
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
|
||||
|
|
@ -261,25 +261,25 @@ export function createTwitterImportUI(
|
|||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
`
|
||||
|
||||
// Add event listeners
|
||||
const closeBtn = container.querySelector(`#${ELEMENT_IDS.TWITTER_CLOSE_BTN}`);
|
||||
closeBtn?.addEventListener("click", onClose);
|
||||
const closeBtn = container.querySelector(`#${ELEMENT_IDS.TWITTER_CLOSE_BTN}`)
|
||||
closeBtn?.addEventListener("click", onClose)
|
||||
|
||||
const importBtn = container.querySelector(
|
||||
`#${ELEMENT_IDS.TWITTER_IMPORT_BTN}`,
|
||||
);
|
||||
importBtn?.addEventListener("click", onImport);
|
||||
)
|
||||
importBtn?.addEventListener("click", onImport)
|
||||
|
||||
const signinBtn = container.querySelector(
|
||||
`#${ELEMENT_IDS.TWITTER_SIGNIN_BTN}`,
|
||||
);
|
||||
)
|
||||
signinBtn?.addEventListener("click", () => {
|
||||
browser.tabs.create({ url: `${API_ENDPOINTS.SUPERMEMORY_WEB}/login` });
|
||||
});
|
||||
browser.tabs.create({ url: `${API_ENDPOINTS.SUPERMEMORY_WEB}/login` })
|
||||
})
|
||||
|
||||
return container;
|
||||
return container
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -288,7 +288,7 @@ export function createTwitterImportUI(
|
|||
* @returns HTMLElement - The save button element
|
||||
*/
|
||||
export function createSaveTweetElement(onClick: () => void): HTMLElement {
|
||||
const iconButton = document.createElement("div");
|
||||
const iconButton = document.createElement("div")
|
||||
iconButton.style.cssText = `
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
|
|
@ -301,36 +301,36 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement {
|
|||
margin-right: 10px;
|
||||
margin-bottom: 2px;
|
||||
z-index: 1000;
|
||||
`;
|
||||
`
|
||||
|
||||
// Check body background color to determine which icon to use
|
||||
const bodyStyle = window.getComputedStyle(document.body);
|
||||
const backgroundColor = bodyStyle.backgroundColor;
|
||||
const isLightMode = backgroundColor === "rgb(255, 255, 255)";
|
||||
const bodyStyle = window.getComputedStyle(document.body)
|
||||
const backgroundColor = bodyStyle.backgroundColor
|
||||
const isLightMode = backgroundColor === "rgb(255, 255, 255)"
|
||||
|
||||
const iconFileName = isLightMode
|
||||
? "/light-mode-icon.png"
|
||||
: "/dark-mode-icon.png";
|
||||
const iconUrl = browser.runtime.getURL(iconFileName);
|
||||
: "/dark-mode-icon.png"
|
||||
const iconUrl = browser.runtime.getURL(iconFileName)
|
||||
iconButton.innerHTML = `
|
||||
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
|
||||
`;
|
||||
`
|
||||
|
||||
iconButton.addEventListener("mouseenter", () => {
|
||||
iconButton.style.opacity = "1";
|
||||
});
|
||||
iconButton.style.opacity = "1"
|
||||
})
|
||||
|
||||
iconButton.addEventListener("mouseleave", () => {
|
||||
iconButton.style.opacity = "0.7";
|
||||
});
|
||||
iconButton.style.opacity = "0.7"
|
||||
})
|
||||
|
||||
iconButton.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
onClick();
|
||||
});
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
onClick()
|
||||
})
|
||||
|
||||
return iconButton;
|
||||
return iconButton
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -339,7 +339,7 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement {
|
|||
* @returns HTMLElement - The save button element
|
||||
*/
|
||||
export function createChatGPTInputBarElement(onClick: () => void): HTMLElement {
|
||||
const iconButton = document.createElement("div");
|
||||
const iconButton = document.createElement("div")
|
||||
iconButton.style.cssText = `
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -349,31 +349,31 @@ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement {
|
|||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
border-radius: 50%;
|
||||
`;
|
||||
`
|
||||
|
||||
// Use appropriate icon based on theme
|
||||
const isDark = DOMUtils.isDarkMode();
|
||||
const iconFileName = isDark ? "/dark-mode-icon.png" : "/light-mode-icon.png";
|
||||
const iconUrl = browser.runtime.getURL(iconFileName);
|
||||
const isDark = DOMUtils.isDarkMode()
|
||||
const iconFileName = isDark ? "/dark-mode-icon.png" : "/light-mode-icon.png"
|
||||
const iconUrl = browser.runtime.getURL(iconFileName)
|
||||
iconButton.innerHTML = `
|
||||
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 50%;" />
|
||||
`;
|
||||
`
|
||||
|
||||
iconButton.addEventListener("mouseenter", () => {
|
||||
iconButton.style.opacity = "0.8";
|
||||
});
|
||||
iconButton.style.opacity = "0.8"
|
||||
})
|
||||
|
||||
iconButton.addEventListener("mouseleave", () => {
|
||||
iconButton.style.opacity = "1";
|
||||
});
|
||||
iconButton.style.opacity = "1"
|
||||
})
|
||||
|
||||
iconButton.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
onClick();
|
||||
});
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
onClick()
|
||||
})
|
||||
|
||||
return iconButton;
|
||||
return iconButton
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -386,7 +386,7 @@ export const DOMUtils = {
|
|||
* @returns boolean
|
||||
*/
|
||||
isOnDomain(domains: readonly string[]): boolean {
|
||||
return domains.includes(window.location.hostname);
|
||||
return domains.includes(window.location.hostname)
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -394,9 +394,9 @@ export const DOMUtils = {
|
|||
* @returns boolean - true if dark mode, false if light mode
|
||||
*/
|
||||
isDarkMode(): boolean {
|
||||
const htmlElement = document.documentElement;
|
||||
const style = htmlElement.getAttribute("style");
|
||||
return style?.includes("color-scheme: dark") || false;
|
||||
const htmlElement = document.documentElement
|
||||
const style = htmlElement.getAttribute("style")
|
||||
return style?.includes("color-scheme: dark") || false
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -405,7 +405,7 @@ export const DOMUtils = {
|
|||
* @returns boolean
|
||||
*/
|
||||
elementExists(id: string): boolean {
|
||||
return !!document.getElementById(id);
|
||||
return !!document.getElementById(id)
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -413,8 +413,8 @@ export const DOMUtils = {
|
|||
* @param id - Element ID to remove
|
||||
*/
|
||||
removeElement(id: string): void {
|
||||
const element = document.getElementById(id);
|
||||
element?.remove();
|
||||
const element = document.getElementById(id)
|
||||
element?.remove()
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -430,28 +430,28 @@ export const DOMUtils = {
|
|||
// Remove all existing toasts more aggressively
|
||||
const existingToasts = document.querySelectorAll(
|
||||
`#${ELEMENT_IDS.SUPERMEMORY_TOAST}`,
|
||||
);
|
||||
)
|
||||
existingToasts.forEach((toast) => {
|
||||
toast.remove();
|
||||
});
|
||||
toast.remove()
|
||||
})
|
||||
|
||||
const toast = createToast(state);
|
||||
document.body.appendChild(toast);
|
||||
const toast = createToast(state)
|
||||
document.body.appendChild(toast)
|
||||
|
||||
// Auto-dismiss for success and error states
|
||||
if (state === "success" || state === "error") {
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(toast)) {
|
||||
toast.style.animation = "fadeOut 0.3s ease-out";
|
||||
toast.style.animation = "fadeOut 0.3s ease-out"
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(toast)) {
|
||||
toast.remove();
|
||||
toast.remove()
|
||||
}
|
||||
}, 300);
|
||||
}, 300)
|
||||
}
|
||||
}, duration);
|
||||
}, duration)
|
||||
}
|
||||
|
||||
return toast;
|
||||
return toast
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { defineConfig } from "wxt";
|
||||
import { defineConfig } from "wxt"
|
||||
|
||||
// See https://wxt.dev/api/config.html
|
||||
export default defineConfig({
|
||||
modules: ["@wxt-dev/module-react"],
|
||||
manifest: {
|
||||
name: "Supermemory",
|
||||
name: "supermemory",
|
||||
homepage_url: "https://supermemory.ai",
|
||||
permissions: [
|
||||
"contextMenus",
|
||||
|
|
@ -25,10 +25,10 @@ export default defineConfig({
|
|||
web_accessible_resources: [
|
||||
{
|
||||
resources: [
|
||||
"icon-16.png",
|
||||
"light-mode-icon.png",
|
||||
"icon-16.png",
|
||||
"light-mode-icon.png",
|
||||
"dark-mode-icon.png",
|
||||
"fonts/*.ttf"
|
||||
"fonts/*.ttf",
|
||||
],
|
||||
matches: ["<all_urls>"],
|
||||
},
|
||||
|
|
@ -37,4 +37,4 @@ export default defineConfig({
|
|||
webExt: {
|
||||
disabled: true,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue