mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
format browser extension
This commit is contained in:
parent
8e784b3889
commit
dd3122a483
24 changed files with 1556 additions and 1521 deletions
|
|
@ -1,52 +1,52 @@
|
|||
import { getDefaultProject, saveMemory, searchMemories } from "../utils/api"
|
||||
import { getDefaultProject, saveMemory, searchMemories } from "../utils/api";
|
||||
import {
|
||||
CONTAINER_TAGS,
|
||||
CONTEXT_MENU_IDS,
|
||||
MESSAGE_TYPES,
|
||||
POSTHOG_EVENT_KEY,
|
||||
} from "../utils/constants"
|
||||
import { trackEvent } from "../utils/posthog"
|
||||
import { captureTwitterTokens } from "../utils/twitter-auth"
|
||||
} from "../utils/constants";
|
||||
import { trackEvent } from "../utils/posthog";
|
||||
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";
|
||||
|
||||
export default defineBackground(() => {
|
||||
let twitterImporter: TwitterImporter | null = null
|
||||
let twitterImporter: TwitterImporter | null = null;
|
||||
|
||||
browser.runtime.onInstalled.addListener(async (details) => {
|
||||
browser.contextMenus.create({
|
||||
id: CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY,
|
||||
title: "sync to supermemory",
|
||||
contexts: ["selection", "page", "link"],
|
||||
})
|
||||
});
|
||||
|
||||
if (details.reason === "install") {
|
||||
await trackEvent("extension_installed", {
|
||||
reason: details.reason,
|
||||
version: browser.runtime.getManifest().version,
|
||||
})
|
||||
});
|
||||
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) => {
|
||||
|
|
@ -56,27 +56,27 @@ export default defineBackground(() => {
|
|||
await browser.tabs.sendMessage(tab.id, {
|
||||
action: MESSAGE_TYPES.SAVE_MEMORY,
|
||||
actionSource: "context_menu",
|
||||
})
|
||||
});
|
||||
} 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
|
||||
|
|
@ -85,14 +85,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
|
||||
|
|
@ -102,14 +102,14 @@ export default defineBackground(() => {
|
|||
actionSource: string,
|
||||
): 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 = {
|
||||
|
|
@ -118,48 +118,48 @@ export default defineBackground(() => {
|
|||
data.content ||
|
||||
`${data.highlightedText}\n\n${data.html}\n\n${data?.url}`,
|
||||
metadata: { sm_source: "consumer" },
|
||||
}
|
||||
};
|
||||
|
||||
const responseData = await saveMemory(payload)
|
||||
const responseData = await saveMemory(payload);
|
||||
|
||||
await trackEvent(POSTHOG_EVENT_KEY.SAVE_MEMORY_ATTEMPTED, {
|
||||
source: `${POSTHOG_EVENT_KEY.SOURCE}_${actionSource}`,
|
||||
has_highlight: !!data.highlightedText,
|
||||
url_domain: data.url ? new URL(data.url).hostname : undefined,
|
||||
})
|
||||
});
|
||||
|
||||
return { success: true, data: responseData }
|
||||
return { success: true, data: responseData };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getRelatedMemories = async (
|
||||
data: string,
|
||||
eventSource: string,
|
||||
): Promise<{ success: boolean; data?: unknown; error?: string }> => {
|
||||
try {
|
||||
const responseData = await searchMemories(data)
|
||||
const responseData = await searchMemories(data);
|
||||
const response = responseData as {
|
||||
results?: Array<{ memory?: string }>
|
||||
}
|
||||
const memories: string[] = []
|
||||
results?: Array<{ memory?: string }>;
|
||||
};
|
||||
const memories: string[] = [];
|
||||
response.results?.forEach((result, index) => {
|
||||
memories.push(`${index + 1}. ${result.memory} \n`)
|
||||
})
|
||||
console.log("Memories:", memories)
|
||||
await trackEvent(eventSource)
|
||||
return { success: true, data: memories }
|
||||
memories.push(`${index + 1}. ${result.memory} \n`);
|
||||
});
|
||||
console.log("Memories:", memories);
|
||||
await trackEvent(eventSource);
|
||||
return { success: true, data: memories };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle extension messages
|
||||
|
|
@ -172,83 +172,83 @@ 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,
|
||||
message.actionSource || "unknown",
|
||||
)
|
||||
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,
|
||||
message.actionSource || "unknown",
|
||||
)
|
||||
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.CAPTURE_PROMPT) {
|
||||
;(async () => {
|
||||
(async () => {
|
||||
try {
|
||||
const messageData = message.data as {
|
||||
prompt: string
|
||||
platform: string
|
||||
source: string
|
||||
}
|
||||
console.log("=== PROMPT CAPTURED ===")
|
||||
console.log(messageData)
|
||||
console.log("========================")
|
||||
prompt: string;
|
||||
platform: string;
|
||||
source: string;
|
||||
};
|
||||
console.log("=== PROMPT CAPTURED ===");
|
||||
console.log(messageData);
|
||||
console.log("========================");
|
||||
|
||||
const memoryData: MemoryData = {
|
||||
content: messageData.prompt,
|
||||
}
|
||||
};
|
||||
|
||||
const result = await saveMemoryToSupermemory(
|
||||
memoryData,
|
||||
`prompt_capture_${messageData.platform}`,
|
||||
)
|
||||
sendResponse(result)
|
||||
);
|
||||
sendResponse(result);
|
||||
} catch (error) {
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
}
|
||||
})()
|
||||
return true
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,78 +5,78 @@ import {
|
|||
POSTHOG_EVENT_KEY,
|
||||
STORAGE_KEYS,
|
||||
UI_CONFIG,
|
||||
} from "../../utils/constants"
|
||||
} from "../../utils/constants";
|
||||
import {
|
||||
createChatGPTInputBarElement,
|
||||
DOMUtils,
|
||||
} from "../../utils/ui-components"
|
||||
} from "../../utils/ui-components";
|
||||
|
||||
let chatGPTDebounceTimeout: NodeJS.Timeout | null = null
|
||||
let chatGPTRouteObserver: MutationObserver | null = null
|
||||
let chatGPTUrlCheckInterval: NodeJS.Timeout | null = null
|
||||
let chatGPTObserverThrottle: NodeJS.Timeout | null = null
|
||||
let chatGPTDebounceTimeout: NodeJS.Timeout | null = null;
|
||||
let chatGPTRouteObserver: MutationObserver | null = null;
|
||||
let chatGPTUrlCheckInterval: NodeJS.Timeout | null = null;
|
||||
let chatGPTObserverThrottle: NodeJS.Timeout | null = null;
|
||||
|
||||
export function initializeChatGPT() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.body.hasAttribute("data-chatgpt-initialized")) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
addSupermemoryButtonToMemoriesDialog()
|
||||
addSaveChatGPTElementBeforeComposerBtn()
|
||||
setupChatGPTAutoFetch()
|
||||
}, 2000)
|
||||
addSupermemoryButtonToMemoriesDialog();
|
||||
addSaveChatGPTElementBeforeComposerBtn();
|
||||
setupChatGPTAutoFetch();
|
||||
}, 2000);
|
||||
|
||||
setupChatGPTPromptCapture()
|
||||
setupChatGPTPromptCapture();
|
||||
|
||||
setupChatGPTRouteChangeDetection()
|
||||
setupChatGPTRouteChangeDetection();
|
||||
|
||||
document.body.setAttribute("data-chatgpt-initialized", "true")
|
||||
document.body.setAttribute("data-chatgpt-initialized", "true");
|
||||
}
|
||||
|
||||
function setupChatGPTRouteChangeDetection() {
|
||||
if (chatGPTRouteObserver) {
|
||||
chatGPTRouteObserver.disconnect()
|
||||
chatGPTRouteObserver.disconnect();
|
||||
}
|
||||
if (chatGPTUrlCheckInterval) {
|
||||
clearInterval(chatGPTUrlCheckInterval)
|
||||
clearInterval(chatGPTUrlCheckInterval);
|
||||
}
|
||||
if (chatGPTObserverThrottle) {
|
||||
clearTimeout(chatGPTObserverThrottle)
|
||||
chatGPTObserverThrottle = null
|
||||
clearTimeout(chatGPTObserverThrottle);
|
||||
chatGPTObserverThrottle = null;
|
||||
}
|
||||
|
||||
let currentUrl = window.location.href
|
||||
let currentUrl = window.location.href;
|
||||
|
||||
const checkForRouteChange = () => {
|
||||
if (window.location.href !== currentUrl) {
|
||||
currentUrl = window.location.href
|
||||
console.log("ChatGPT route changed, re-adding supermemory elements")
|
||||
currentUrl = window.location.href;
|
||||
console.log("ChatGPT route changed, re-adding supermemory elements");
|
||||
setTimeout(() => {
|
||||
addSupermemoryButtonToMemoriesDialog()
|
||||
addSaveChatGPTElementBeforeComposerBtn()
|
||||
setupChatGPTAutoFetch()
|
||||
}, 1000)
|
||||
addSupermemoryButtonToMemoriesDialog();
|
||||
addSaveChatGPTElementBeforeComposerBtn();
|
||||
setupChatGPTAutoFetch();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 2000)
|
||||
chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 2000);
|
||||
|
||||
chatGPTRouteObserver = new MutationObserver((mutations) => {
|
||||
if (chatGPTObserverThrottle) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldRecheck = false
|
||||
let shouldRecheck = false;
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element
|
||||
const element = node as Element;
|
||||
if (
|
||||
element.querySelector?.("#prompt-textarea") ||
|
||||
element.querySelector?.("button.composer-btn") ||
|
||||
|
|
@ -84,65 +84,65 @@ function setupChatGPTRouteChangeDetection() {
|
|||
element.matches?.("#prompt-textarea") ||
|
||||
element.id === "prompt-textarea"
|
||||
) {
|
||||
shouldRecheck = true
|
||||
shouldRecheck = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (shouldRecheck) {
|
||||
chatGPTObserverThrottle = setTimeout(() => {
|
||||
try {
|
||||
chatGPTObserverThrottle = null
|
||||
addSupermemoryButtonToMemoriesDialog()
|
||||
addSaveChatGPTElementBeforeComposerBtn()
|
||||
setupChatGPTAutoFetch()
|
||||
chatGPTObserverThrottle = null;
|
||||
addSupermemoryButtonToMemoriesDialog();
|
||||
addSaveChatGPTElementBeforeComposerBtn();
|
||||
setupChatGPTAutoFetch();
|
||||
} catch (error) {
|
||||
console.error("Error in ChatGPT observer callback:", error)
|
||||
console.error("Error in ChatGPT observer callback:", error);
|
||||
}
|
||||
}, 300)
|
||||
}, 300);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
try {
|
||||
chatGPTRouteObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to set up ChatGPT route observer:", error)
|
||||
console.error("Failed to set up ChatGPT route observer:", error);
|
||||
if (chatGPTUrlCheckInterval) {
|
||||
clearInterval(chatGPTUrlCheckInterval)
|
||||
clearInterval(chatGPTUrlCheckInterval);
|
||||
}
|
||||
chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 1000)
|
||||
chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async function getRelatedMemoriesForChatGPT(actionSource: string) {
|
||||
try {
|
||||
const userQuery =
|
||||
document.getElementById("prompt-textarea")?.textContent || ""
|
||||
document.getElementById("prompt-textarea")?.textContent || "";
|
||||
|
||||
const icon = document.querySelectorAll(
|
||||
'[id*="sm-chatgpt-input-bar-element-before-composer"]',
|
||||
)[0]
|
||||
)[0];
|
||||
|
||||
const iconElement = icon as HTMLElement
|
||||
const iconElement = icon as HTMLElement;
|
||||
|
||||
if (!iconElement) {
|
||||
console.warn("ChatGPT icon element not found, cannot update feedback")
|
||||
return
|
||||
console.warn("ChatGPT icon element not found, cannot update feedback");
|
||||
return;
|
||||
}
|
||||
|
||||
updateChatGPTIconFeedback("Searching memories...", iconElement)
|
||||
updateChatGPTIconFeedback("Searching memories...", iconElement);
|
||||
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error("Memory search timeout")),
|
||||
UI_CONFIG.API_REQUEST_TIMEOUT,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
const response = await Promise.race([
|
||||
browser.runtime.sendMessage({
|
||||
|
|
@ -151,78 +151,78 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) {
|
|||
actionSource: actionSource,
|
||||
}),
|
||||
timeoutPromise,
|
||||
])
|
||||
]);
|
||||
|
||||
if (response?.success && response?.data) {
|
||||
const promptElement = document.getElementById("prompt-textarea")
|
||||
const promptElement = document.getElementById("prompt-textarea");
|
||||
if (promptElement) {
|
||||
promptElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${response.data}</div>`
|
||||
promptElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${response.data}</div>`;
|
||||
console.log(
|
||||
"Prompt element dataset:",
|
||||
promptElement.dataset.supermemories,
|
||||
)
|
||||
);
|
||||
|
||||
iconElement.dataset.memoriesData = response.data
|
||||
iconElement.dataset.memoriesData = response.data;
|
||||
|
||||
updateChatGPTIconFeedback("Included Memories", iconElement)
|
||||
updateChatGPTIconFeedback("Included Memories", iconElement);
|
||||
} else {
|
||||
console.warn(
|
||||
"ChatGPT prompt element not found after successful memory fetch",
|
||||
)
|
||||
updateChatGPTIconFeedback("Memories found", iconElement)
|
||||
);
|
||||
updateChatGPTIconFeedback("Memories found", iconElement);
|
||||
}
|
||||
} else {
|
||||
console.warn("No memories found or API response invalid")
|
||||
updateChatGPTIconFeedback("No memories found", iconElement)
|
||||
console.warn("No memories found or API response invalid");
|
||||
updateChatGPTIconFeedback("No memories found", iconElement);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting related memories:", error)
|
||||
console.error("Error getting related memories:", error);
|
||||
try {
|
||||
const icon = document.querySelectorAll(
|
||||
'[id*="sm-chatgpt-input-bar-element-before-composer"]',
|
||||
)[0] as HTMLElement
|
||||
)[0] as HTMLElement;
|
||||
if (icon) {
|
||||
updateChatGPTIconFeedback("Error fetching memories", icon)
|
||||
updateChatGPTIconFeedback("Error fetching memories", icon);
|
||||
}
|
||||
} catch (feedbackError) {
|
||||
console.error("Failed to update error feedback:", feedbackError)
|
||||
console.error("Failed to update error feedback:", feedbackError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
@ -234,54 +234,54 @@ function addSupermemoryButtonToMemoriesDialog() {
|
|||
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')
|
||||
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: MESSAGE_TYPES.SAVE_MEMORY,
|
||||
|
|
@ -289,18 +289,18 @@ async function saveMemoriesToSupermemory() {
|
|||
html: combinedContent,
|
||||
},
|
||||
actionSource: "chatgpt_memories_dialog",
|
||||
})
|
||||
});
|
||||
|
||||
console.log({ response })
|
||||
console.log({ response });
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -310,10 +310,10 @@ function updateChatGPTIconFeedback(
|
|||
resetAfter = 0,
|
||||
) {
|
||||
if (!iconElement.dataset.originalHtml) {
|
||||
iconElement.dataset.originalHtml = iconElement.innerHTML
|
||||
iconElement.dataset.originalHtml = iconElement.innerHTML;
|
||||
}
|
||||
|
||||
const feedbackDiv = document.createElement("div")
|
||||
const feedbackDiv = document.createElement("div");
|
||||
feedbackDiv.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -326,15 +326,15 @@ function updateChatGPTIconFeedback(
|
|||
font-weight: 500;
|
||||
cursor: ${message === "Included Memories" ? "pointer" : "default"};
|
||||
position: relative;
|
||||
`
|
||||
`;
|
||||
|
||||
feedbackDiv.innerHTML = `
|
||||
<span>✓</span>
|
||||
<span>${message}</span>
|
||||
`
|
||||
`;
|
||||
|
||||
if (message === "Included Memories" && iconElement.dataset.memoriesData) {
|
||||
const popup = document.createElement("div")
|
||||
const popup = document.createElement("div");
|
||||
popup.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
|
|
@ -352,9 +352,9 @@ function updateChatGPTIconFeedback(
|
|||
z-index: 999999;
|
||||
display: none;
|
||||
border: 1px solid #333;
|
||||
`
|
||||
`;
|
||||
|
||||
const header = document.createElement("div")
|
||||
const header = document.createElement("div");
|
||||
header.style.cssText = `
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -362,28 +362,28 @@ function updateChatGPTIconFeedback(
|
|||
padding: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
opacity: 0.8;
|
||||
`
|
||||
`;
|
||||
header.innerHTML = `
|
||||
<span style="font-weight: 600; color: #fff;">Included Memories</span>
|
||||
`
|
||||
`;
|
||||
|
||||
const content = document.createElement("div")
|
||||
const content = document.createElement("div");
|
||||
content.style.cssText = `
|
||||
padding: 0;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
`
|
||||
`;
|
||||
|
||||
const memoriesText = iconElement.dataset.memoriesData || ""
|
||||
console.log("Memories text:", memoriesText)
|
||||
const memoriesText = iconElement.dataset.memoriesData || "";
|
||||
console.log("Memories text:", memoriesText);
|
||||
const individualMemories = memoriesText
|
||||
.split(/[,\n]/)
|
||||
.map((memory) => memory.trim())
|
||||
.filter((memory) => memory.length > 0 && memory !== ",")
|
||||
console.log("Individual memories:", individualMemories)
|
||||
.filter((memory) => memory.length > 0 && memory !== ",");
|
||||
console.log("Individual memories:", individualMemories);
|
||||
|
||||
individualMemories.forEach((memory, index) => {
|
||||
const memoryItem = document.createElement("div")
|
||||
const memoryItem = document.createElement("div");
|
||||
memoryItem.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -391,16 +391,16 @@ function updateChatGPTIconFeedback(
|
|||
padding: 10px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
`
|
||||
`;
|
||||
|
||||
const memoryText = document.createElement("div")
|
||||
const memoryText = document.createElement("div");
|
||||
memoryText.style.cssText = `
|
||||
flex: 1;
|
||||
color: #e5e5e5;
|
||||
`
|
||||
memoryText.textContent = memory.trim()
|
||||
`;
|
||||
memoryText.textContent = memory.trim();
|
||||
|
||||
const removeBtn = document.createElement("button")
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.style.cssText = `
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
|
|
@ -413,252 +413,255 @@ function updateChatGPTIconFeedback(
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`
|
||||
removeBtn.dataset.memoryIndex = index.toString()
|
||||
`;
|
||||
removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`;
|
||||
removeBtn.dataset.memoryIndex = index.toString();
|
||||
|
||||
removeBtn.addEventListener("mouseenter", () => {
|
||||
removeBtn.style.color = "#ef4444"
|
||||
})
|
||||
removeBtn.style.color = "#ef4444";
|
||||
});
|
||||
removeBtn.addEventListener("mouseleave", () => {
|
||||
removeBtn.style.color = "#9ca3af"
|
||||
})
|
||||
removeBtn.style.color = "#9ca3af";
|
||||
});
|
||||
|
||||
memoryItem.appendChild(memoryText)
|
||||
memoryItem.appendChild(removeBtn)
|
||||
content.appendChild(memoryItem)
|
||||
})
|
||||
memoryItem.appendChild(memoryText);
|
||||
memoryItem.appendChild(removeBtn);
|
||||
content.appendChild(memoryItem);
|
||||
});
|
||||
|
||||
popup.appendChild(header)
|
||||
popup.appendChild(content)
|
||||
document.body.appendChild(popup)
|
||||
popup.appendChild(header);
|
||||
popup.appendChild(content);
|
||||
document.body.appendChild(popup);
|
||||
|
||||
feedbackDiv.addEventListener("mouseenter", () => {
|
||||
const textSpan = feedbackDiv.querySelector("span:last-child")
|
||||
const textSpan = feedbackDiv.querySelector("span:last-child");
|
||||
if (textSpan) {
|
||||
textSpan.textContent = "Click to see memories"
|
||||
textSpan.textContent = "Click to see memories";
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
feedbackDiv.addEventListener("mouseleave", () => {
|
||||
const textSpan = feedbackDiv.querySelector("span:last-child")
|
||||
const textSpan = feedbackDiv.querySelector("span:last-child");
|
||||
if (textSpan) {
|
||||
textSpan.textContent = "Included Memories"
|
||||
textSpan.textContent = "Included Memories";
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
feedbackDiv.addEventListener("click", (e) => {
|
||||
e.stopPropagation()
|
||||
popup.style.display = "block"
|
||||
})
|
||||
e.stopPropagation();
|
||||
popup.style.display = "block";
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!popup.contains(e.target as Node)) {
|
||||
popup.style.display = "none"
|
||||
popup.style.display = "none";
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
content.querySelectorAll("button[data-memory-index]").forEach((button) => {
|
||||
const htmlButton = button as HTMLButtonElement
|
||||
const htmlButton = button as HTMLButtonElement;
|
||||
htmlButton.addEventListener("click", () => {
|
||||
const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10)
|
||||
const memoryItem = htmlButton.parentElement
|
||||
const index = Number.parseInt(
|
||||
htmlButton.dataset.memoryIndex || "0",
|
||||
10,
|
||||
);
|
||||
const memoryItem = htmlButton.parentElement;
|
||||
|
||||
if (memoryItem) {
|
||||
content.removeChild(memoryItem)
|
||||
content.removeChild(memoryItem);
|
||||
}
|
||||
|
||||
const currentMemories = (iconElement.dataset.memoriesData || "")
|
||||
.split(/[,\n]/)
|
||||
.map((memory) => memory.trim())
|
||||
.filter((memory) => memory.length > 0 && memory !== ",")
|
||||
currentMemories.splice(index, 1)
|
||||
.filter((memory) => memory.length > 0 && memory !== ",");
|
||||
currentMemories.splice(index, 1);
|
||||
|
||||
const updatedMemories = currentMemories.join(" ,")
|
||||
const updatedMemories = currentMemories.join(" ,");
|
||||
|
||||
iconElement.dataset.memoriesData = updatedMemories
|
||||
iconElement.dataset.memoriesData = updatedMemories;
|
||||
|
||||
const promptElement = document.getElementById("prompt-textarea")
|
||||
const promptElement = document.getElementById("prompt-textarea");
|
||||
if (promptElement) {
|
||||
promptElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${updatedMemories}</div>`
|
||||
promptElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${updatedMemories}</div>`;
|
||||
}
|
||||
|
||||
content
|
||||
.querySelectorAll("button[data-memory-index]")
|
||||
.forEach((btn, newIndex) => {
|
||||
const htmlBtn = btn as HTMLButtonElement
|
||||
htmlBtn.dataset.memoryIndex = newIndex.toString()
|
||||
})
|
||||
const htmlBtn = btn as HTMLButtonElement;
|
||||
htmlBtn.dataset.memoryIndex = newIndex.toString();
|
||||
});
|
||||
|
||||
if (currentMemories.length <= 1) {
|
||||
if (promptElement?.dataset.supermemories) {
|
||||
delete promptElement.dataset.supermemories
|
||||
delete iconElement.dataset.memoriesData
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
|
||||
delete iconElement.dataset.originalHtml
|
||||
delete promptElement.dataset.supermemories;
|
||||
delete iconElement.dataset.memoriesData;
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml || "";
|
||||
delete iconElement.dataset.originalHtml;
|
||||
}
|
||||
popup.style.display = "none"
|
||||
popup.style.display = "none";
|
||||
if (document.body.contains(popup)) {
|
||||
document.body.removeChild(popup)
|
||||
document.body.removeChild(popup);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(popup)) {
|
||||
document.body.removeChild(popup)
|
||||
document.body.removeChild(popup);
|
||||
}
|
||||
}, 300000)
|
||||
}, 300000);
|
||||
}
|
||||
|
||||
iconElement.innerHTML = ""
|
||||
iconElement.appendChild(feedbackDiv)
|
||||
iconElement.innerHTML = "";
|
||||
iconElement.appendChild(feedbackDiv);
|
||||
|
||||
if (resetAfter > 0) {
|
||||
setTimeout(() => {
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
|
||||
delete iconElement.dataset.originalHtml
|
||||
}, resetAfter)
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml || "";
|
||||
delete iconElement.dataset.originalHtml;
|
||||
}, resetAfter);
|
||||
}
|
||||
}
|
||||
|
||||
function addSaveChatGPTElementBeforeComposerBtn() {
|
||||
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 getRelatedMemoriesForChatGPT(
|
||||
POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_SEARCHED,
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
setupChatGPTAutoFetch()
|
||||
})
|
||||
setupChatGPTAutoFetch();
|
||||
});
|
||||
}
|
||||
|
||||
async function setupChatGPTAutoFetch() {
|
||||
const result = await chrome.storage.local.get([
|
||||
STORAGE_KEYS.AUTO_SEARCH_ENABLED,
|
||||
])
|
||||
const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false
|
||||
]);
|
||||
const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false;
|
||||
|
||||
if (!autoSearchEnabled) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const promptTextarea = document.getElementById("prompt-textarea")
|
||||
const promptTextarea = document.getElementById("prompt-textarea");
|
||||
if (
|
||||
!promptTextarea ||
|
||||
promptTextarea.hasAttribute("data-supermemory-auto-fetch")
|
||||
) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
promptTextarea.setAttribute("data-supermemory-auto-fetch", "true")
|
||||
promptTextarea.setAttribute("data-supermemory-auto-fetch", "true");
|
||||
|
||||
const handleInput = () => {
|
||||
if (chatGPTDebounceTimeout) {
|
||||
clearTimeout(chatGPTDebounceTimeout)
|
||||
clearTimeout(chatGPTDebounceTimeout);
|
||||
}
|
||||
|
||||
chatGPTDebounceTimeout = setTimeout(async () => {
|
||||
const content = promptTextarea.textContent?.trim() || ""
|
||||
const content = promptTextarea.textContent?.trim() || "";
|
||||
|
||||
if (content.length > 2) {
|
||||
await getRelatedMemoriesForChatGPT(
|
||||
POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED,
|
||||
)
|
||||
);
|
||||
} else if (content.length === 0) {
|
||||
const icons = document.querySelectorAll(
|
||||
'[id*="sm-chatgpt-input-bar-element-before-composer"]',
|
||||
)
|
||||
);
|
||||
|
||||
icons.forEach((icon) => {
|
||||
const iconElement = icon as HTMLElement
|
||||
const iconElement = icon as HTMLElement;
|
||||
if (iconElement.dataset.originalHtml) {
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml
|
||||
delete iconElement.dataset.originalHtml
|
||||
delete iconElement.dataset.memoriesData
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml;
|
||||
delete iconElement.dataset.originalHtml;
|
||||
delete iconElement.dataset.memoriesData;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (promptTextarea.dataset.supermemories) {
|
||||
delete promptTextarea.dataset.supermemories
|
||||
delete promptTextarea.dataset.supermemories;
|
||||
}
|
||||
}
|
||||
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
|
||||
}
|
||||
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY);
|
||||
};
|
||||
|
||||
promptTextarea.addEventListener("input", handleInput)
|
||||
promptTextarea.addEventListener("input", handleInput);
|
||||
}
|
||||
|
||||
function setupChatGPTPromptCapture() {
|
||||
if (document.body.hasAttribute("data-chatgpt-prompt-capture-setup")) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
document.body.setAttribute("data-chatgpt-prompt-capture-setup", "true")
|
||||
document.body.setAttribute("data-chatgpt-prompt-capture-setup", "true");
|
||||
|
||||
const capturePromptContent = async (source: string) => {
|
||||
const promptTextarea = document.getElementById("prompt-textarea")
|
||||
const promptTextarea = document.getElementById("prompt-textarea");
|
||||
|
||||
let promptContent = ""
|
||||
let promptContent = "";
|
||||
if (promptTextarea) {
|
||||
promptContent = promptTextarea.textContent || ""
|
||||
promptContent = promptTextarea.textContent || "";
|
||||
}
|
||||
|
||||
const storedMemories = promptTextarea?.dataset.supermemories
|
||||
const storedMemories = promptTextarea?.dataset.supermemories;
|
||||
if (
|
||||
storedMemories &&
|
||||
promptTextarea &&
|
||||
!promptContent.includes("Supermemories of user")
|
||||
) {
|
||||
promptTextarea.innerHTML = `${promptTextarea.innerHTML} ${storedMemories}`
|
||||
promptContent = promptTextarea.textContent || ""
|
||||
promptTextarea.innerHTML = `${promptTextarea.innerHTML} ${storedMemories}`;
|
||||
promptContent = promptTextarea.textContent || "";
|
||||
}
|
||||
|
||||
if (promptTextarea && promptContent.trim()) {
|
||||
console.log(`ChatGPT prompt submitted via ${source}:`, promptContent)
|
||||
console.log(`ChatGPT prompt submitted via ${source}:`, promptContent);
|
||||
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
|
|
@ -668,57 +671,57 @@ function setupChatGPTPromptCapture() {
|
|||
platform: "chatgpt",
|
||||
source: source,
|
||||
},
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error sending ChatGPT prompt to background:", error)
|
||||
console.error("Error sending ChatGPT prompt to background:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const icons = document.querySelectorAll(
|
||||
'[id*="sm-chatgpt-input-bar-element-before-composer"]',
|
||||
)
|
||||
);
|
||||
|
||||
icons.forEach((icon) => {
|
||||
const iconElement = icon as HTMLElement
|
||||
const iconElement = icon as HTMLElement;
|
||||
if (iconElement.dataset.originalHtml) {
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml
|
||||
delete iconElement.dataset.originalHtml
|
||||
delete iconElement.dataset.memoriesData
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml;
|
||||
delete iconElement.dataset.originalHtml;
|
||||
delete iconElement.dataset.memoriesData;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (promptTextarea?.dataset.supermemories) {
|
||||
delete promptTextarea.dataset.supermemories
|
||||
delete promptTextarea.dataset.supermemories;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener(
|
||||
"click",
|
||||
async (event) => {
|
||||
const target = event.target as HTMLElement
|
||||
const target = event.target as HTMLElement;
|
||||
if (
|
||||
target.id === "composer-submit-button" ||
|
||||
target.closest("#composer-submit-button")
|
||||
) {
|
||||
await capturePromptContent("button click")
|
||||
await capturePromptContent("button click");
|
||||
}
|
||||
},
|
||||
true,
|
||||
)
|
||||
);
|
||||
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
async (event) => {
|
||||
const target = event.target as HTMLElement
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
if (
|
||||
target.id === "prompt-textarea" &&
|
||||
event.key === "Enter" &&
|
||||
!event.shiftKey
|
||||
) {
|
||||
await capturePromptContent("Enter key")
|
||||
await capturePromptContent("Enter key");
|
||||
}
|
||||
},
|
||||
true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,210 +5,210 @@ import {
|
|||
POSTHOG_EVENT_KEY,
|
||||
STORAGE_KEYS,
|
||||
UI_CONFIG,
|
||||
} from "../../utils/constants"
|
||||
} from "../../utils/constants";
|
||||
import {
|
||||
createClaudeInputBarElement,
|
||||
DOMUtils,
|
||||
} from "../../utils/ui-components"
|
||||
} from "../../utils/ui-components";
|
||||
|
||||
let claudeDebounceTimeout: NodeJS.Timeout | null = null
|
||||
let claudeRouteObserver: MutationObserver | null = null
|
||||
let claudeUrlCheckInterval: NodeJS.Timeout | null = null
|
||||
let claudeObserverThrottle: NodeJS.Timeout | null = null
|
||||
let claudeDebounceTimeout: NodeJS.Timeout | null = null;
|
||||
let claudeRouteObserver: MutationObserver | null = null;
|
||||
let claudeUrlCheckInterval: NodeJS.Timeout | null = null;
|
||||
let claudeObserverThrottle: NodeJS.Timeout | null = null;
|
||||
|
||||
export function initializeClaude() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.body.hasAttribute("data-claude-initialized")) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
addSupermemoryIconToClaudeInput()
|
||||
setupClaudeAutoFetch()
|
||||
}, 2000)
|
||||
addSupermemoryIconToClaudeInput();
|
||||
setupClaudeAutoFetch();
|
||||
}, 2000);
|
||||
|
||||
setupClaudePromptCapture()
|
||||
setupClaudePromptCapture();
|
||||
|
||||
setupClaudeRouteChangeDetection()
|
||||
setupClaudeRouteChangeDetection();
|
||||
|
||||
document.body.setAttribute("data-claude-initialized", "true")
|
||||
document.body.setAttribute("data-claude-initialized", "true");
|
||||
}
|
||||
|
||||
function setupClaudeRouteChangeDetection() {
|
||||
if (claudeRouteObserver) {
|
||||
claudeRouteObserver.disconnect()
|
||||
claudeRouteObserver.disconnect();
|
||||
}
|
||||
if (claudeUrlCheckInterval) {
|
||||
clearInterval(claudeUrlCheckInterval)
|
||||
clearInterval(claudeUrlCheckInterval);
|
||||
}
|
||||
if (claudeObserverThrottle) {
|
||||
clearTimeout(claudeObserverThrottle)
|
||||
claudeObserverThrottle = null
|
||||
clearTimeout(claudeObserverThrottle);
|
||||
claudeObserverThrottle = null;
|
||||
}
|
||||
|
||||
let currentUrl = window.location.href
|
||||
let currentUrl = window.location.href;
|
||||
|
||||
const checkForRouteChange = () => {
|
||||
if (window.location.href !== currentUrl) {
|
||||
currentUrl = window.location.href
|
||||
console.log("Claude route changed, re-adding supermemory icon")
|
||||
currentUrl = window.location.href;
|
||||
console.log("Claude route changed, re-adding supermemory icon");
|
||||
setTimeout(() => {
|
||||
addSupermemoryIconToClaudeInput()
|
||||
setupClaudeAutoFetch()
|
||||
}, 1000)
|
||||
addSupermemoryIconToClaudeInput();
|
||||
setupClaudeAutoFetch();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
claudeUrlCheckInterval = setInterval(checkForRouteChange, 2000)
|
||||
claudeUrlCheckInterval = setInterval(checkForRouteChange, 2000);
|
||||
|
||||
claudeRouteObserver = new MutationObserver((mutations) => {
|
||||
if (claudeObserverThrottle) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldRecheck = false
|
||||
let shouldRecheck = false;
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element
|
||||
const element = node as Element;
|
||||
if (
|
||||
element.querySelector?.('div[contenteditable="true"]') ||
|
||||
element.querySelector?.("textarea") ||
|
||||
element.matches?.('div[contenteditable="true"]') ||
|
||||
element.matches?.("textarea")
|
||||
) {
|
||||
shouldRecheck = true
|
||||
shouldRecheck = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (shouldRecheck) {
|
||||
claudeObserverThrottle = setTimeout(() => {
|
||||
try {
|
||||
claudeObserverThrottle = null
|
||||
addSupermemoryIconToClaudeInput()
|
||||
setupClaudeAutoFetch()
|
||||
claudeObserverThrottle = null;
|
||||
addSupermemoryIconToClaudeInput();
|
||||
setupClaudeAutoFetch();
|
||||
} catch (error) {
|
||||
console.error("Error in Claude observer callback:", error)
|
||||
console.error("Error in Claude observer callback:", error);
|
||||
}
|
||||
}, 300)
|
||||
}, 300);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
try {
|
||||
claudeRouteObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to set up Claude route observer:", error)
|
||||
console.error("Failed to set up Claude route observer:", error);
|
||||
if (claudeUrlCheckInterval) {
|
||||
clearInterval(claudeUrlCheckInterval)
|
||||
clearInterval(claudeUrlCheckInterval);
|
||||
}
|
||||
claudeUrlCheckInterval = setInterval(checkForRouteChange, 1000)
|
||||
claudeUrlCheckInterval = setInterval(checkForRouteChange, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function addSupermemoryIconToClaudeInput() {
|
||||
const targetContainers = document.querySelectorAll(
|
||||
".relative.flex-1.flex.items-center.gap-2.shrink.min-w-0",
|
||||
)
|
||||
);
|
||||
|
||||
targetContainers.forEach((container) => {
|
||||
if (container.hasAttribute("data-supermemory-icon-added")) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const existingIcon = container.querySelector(
|
||||
`#${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}`,
|
||||
)
|
||||
);
|
||||
if (existingIcon) {
|
||||
container.setAttribute("data-supermemory-icon-added", "true")
|
||||
return
|
||||
container.setAttribute("data-supermemory-icon-added", "true");
|
||||
return;
|
||||
}
|
||||
|
||||
const supermemoryIcon = createClaudeInputBarElement(async () => {
|
||||
await getRelatedMemoriesForClaude(
|
||||
POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_SEARCHED,
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
|
||||
supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
|
||||
container.setAttribute("data-supermemory-icon-added", "true")
|
||||
container.setAttribute("data-supermemory-icon-added", "true");
|
||||
|
||||
container.insertBefore(supermemoryIcon, container.firstChild)
|
||||
})
|
||||
container.insertBefore(supermemoryIcon, container.firstChild);
|
||||
});
|
||||
}
|
||||
|
||||
async function getRelatedMemoriesForClaude(actionSource: string) {
|
||||
try {
|
||||
let userQuery = ""
|
||||
let userQuery = "";
|
||||
|
||||
const supermemoryContainer = document.querySelector(
|
||||
'[data-supermemory-icon-added="true"]',
|
||||
)
|
||||
);
|
||||
if (supermemoryContainer?.parentElement?.previousElementSibling) {
|
||||
const pTag =
|
||||
supermemoryContainer.parentElement.previousElementSibling.querySelector(
|
||||
"p",
|
||||
)
|
||||
userQuery = pTag?.innerText || pTag?.textContent || ""
|
||||
);
|
||||
userQuery = pTag?.innerText || pTag?.textContent || "";
|
||||
}
|
||||
|
||||
if (!userQuery.trim()) {
|
||||
const textareaElement = document.querySelector(
|
||||
'div[contenteditable="true"]',
|
||||
) as HTMLElement
|
||||
) as HTMLElement;
|
||||
userQuery =
|
||||
textareaElement?.innerText || textareaElement?.textContent || ""
|
||||
textareaElement?.innerText || textareaElement?.textContent || "";
|
||||
}
|
||||
|
||||
if (!userQuery.trim()) {
|
||||
const inputElements = document.querySelectorAll(
|
||||
'div[contenteditable="true"], textarea, input[type="text"]',
|
||||
)
|
||||
);
|
||||
for (const element of inputElements) {
|
||||
const text =
|
||||
(element as HTMLElement).innerText ||
|
||||
(element as HTMLInputElement).value
|
||||
(element as HTMLInputElement).value;
|
||||
if (text?.trim()) {
|
||||
userQuery = text.trim()
|
||||
break
|
||||
userQuery = text.trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Claude query extracted:", userQuery)
|
||||
console.log("Claude query extracted:", userQuery);
|
||||
|
||||
if (!userQuery.trim()) {
|
||||
console.log("No query text found for Claude")
|
||||
return
|
||||
console.log("No query text found for Claude");
|
||||
return;
|
||||
}
|
||||
|
||||
const icon = document.querySelector('[id*="sm-claude-input-bar-element"]')
|
||||
const icon = document.querySelector('[id*="sm-claude-input-bar-element"]');
|
||||
|
||||
const iconElement = icon as HTMLElement
|
||||
const iconElement = icon as HTMLElement;
|
||||
|
||||
if (!iconElement) {
|
||||
console.warn("Claude icon element not found, cannot update feedback")
|
||||
return
|
||||
console.warn("Claude icon element not found, cannot update feedback");
|
||||
return;
|
||||
}
|
||||
|
||||
updateClaudeIconFeedback("Searching memories...", iconElement)
|
||||
updateClaudeIconFeedback("Searching memories...", iconElement);
|
||||
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error("Memory search timeout")),
|
||||
UI_CONFIG.API_REQUEST_TIMEOUT,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
const response = await Promise.race([
|
||||
browser.runtime.sendMessage({
|
||||
|
|
@ -217,46 +217,46 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
|
|||
actionSource: actionSource,
|
||||
}),
|
||||
timeoutPromise,
|
||||
])
|
||||
]);
|
||||
|
||||
console.log("Claude memories response:", response)
|
||||
console.log("Claude memories response:", response);
|
||||
|
||||
if (response?.success && response?.data) {
|
||||
const textareaElement = document.querySelector(
|
||||
'div[contenteditable="true"]',
|
||||
) as HTMLElement
|
||||
) as HTMLElement;
|
||||
|
||||
if (textareaElement) {
|
||||
textareaElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${response.data}</div>`
|
||||
textareaElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${response.data}</div>`;
|
||||
console.log(
|
||||
"Text element dataset:",
|
||||
textareaElement.dataset.supermemories,
|
||||
)
|
||||
);
|
||||
|
||||
iconElement.dataset.memoriesData = response.data
|
||||
iconElement.dataset.memoriesData = response.data;
|
||||
|
||||
updateClaudeIconFeedback("Included Memories", iconElement)
|
||||
updateClaudeIconFeedback("Included Memories", iconElement);
|
||||
} else {
|
||||
console.warn(
|
||||
"Claude input area not found after successful memory fetch",
|
||||
)
|
||||
updateClaudeIconFeedback("Memories found", iconElement)
|
||||
);
|
||||
updateClaudeIconFeedback("Memories found", iconElement);
|
||||
}
|
||||
} else {
|
||||
console.warn("No memories found or API response invalid for Claude")
|
||||
updateClaudeIconFeedback("No memories found", iconElement)
|
||||
console.warn("No memories found or API response invalid for Claude");
|
||||
updateClaudeIconFeedback("No memories found", iconElement);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting related memories for Claude:", error)
|
||||
console.error("Error getting related memories for Claude:", error);
|
||||
try {
|
||||
const icon = document.querySelector(
|
||||
'[id*="sm-claude-input-bar-element"]',
|
||||
) as HTMLElement
|
||||
) as HTMLElement;
|
||||
if (icon) {
|
||||
updateClaudeIconFeedback("Error fetching memories", icon)
|
||||
updateClaudeIconFeedback("Error fetching memories", icon);
|
||||
}
|
||||
} catch (feedbackError) {
|
||||
console.error("Failed to update Claude error feedback:", feedbackError)
|
||||
console.error("Failed to update Claude error feedback:", feedbackError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -267,10 +267,10 @@ function updateClaudeIconFeedback(
|
|||
resetAfter = 0,
|
||||
) {
|
||||
if (!iconElement.dataset.originalHtml) {
|
||||
iconElement.dataset.originalHtml = iconElement.innerHTML
|
||||
iconElement.dataset.originalHtml = iconElement.innerHTML;
|
||||
}
|
||||
|
||||
const feedbackDiv = document.createElement("div")
|
||||
const feedbackDiv = document.createElement("div");
|
||||
feedbackDiv.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -283,15 +283,15 @@ function updateClaudeIconFeedback(
|
|||
font-weight: 500;
|
||||
cursor: ${message === "Included Memories" ? "pointer" : "default"};
|
||||
position: relative;
|
||||
`
|
||||
`;
|
||||
|
||||
feedbackDiv.innerHTML = `
|
||||
<span>✓</span>
|
||||
<span>${message}</span>
|
||||
`
|
||||
`;
|
||||
|
||||
if (message === "Included Memories" && iconElement.dataset.memoriesData) {
|
||||
const popup = document.createElement("div")
|
||||
const popup = document.createElement("div");
|
||||
popup.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
|
|
@ -309,9 +309,9 @@ function updateClaudeIconFeedback(
|
|||
z-index: 999999;
|
||||
display: none;
|
||||
border: 1px solid #333;
|
||||
`
|
||||
`;
|
||||
|
||||
const header = document.createElement("div")
|
||||
const header = document.createElement("div");
|
||||
header.style.cssText = `
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -319,28 +319,28 @@ function updateClaudeIconFeedback(
|
|||
padding: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
opacity: 0.8;
|
||||
`
|
||||
`;
|
||||
header.innerHTML = `
|
||||
<span style="font-weight: 600; color: #fff;">Included Memories</span>
|
||||
`
|
||||
`;
|
||||
|
||||
const content = document.createElement("div")
|
||||
const content = document.createElement("div");
|
||||
content.style.cssText = `
|
||||
padding: 0;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
`
|
||||
`;
|
||||
|
||||
const memoriesText = iconElement.dataset.memoriesData || ""
|
||||
console.log("Memories text:", memoriesText)
|
||||
const memoriesText = iconElement.dataset.memoriesData || "";
|
||||
console.log("Memories text:", memoriesText);
|
||||
const individualMemories = memoriesText
|
||||
.split(/[,\n]/)
|
||||
.map((memory) => memory.trim())
|
||||
.filter((memory) => memory.length > 0 && memory !== ",")
|
||||
console.log("Individual memories:", individualMemories)
|
||||
.filter((memory) => memory.length > 0 && memory !== ",");
|
||||
console.log("Individual memories:", individualMemories);
|
||||
|
||||
individualMemories.forEach((memory, index) => {
|
||||
const memoryItem = document.createElement("div")
|
||||
const memoryItem = document.createElement("div");
|
||||
memoryItem.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -348,16 +348,16 @@ function updateClaudeIconFeedback(
|
|||
padding: 10px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
`
|
||||
`;
|
||||
|
||||
const memoryText = document.createElement("div")
|
||||
const memoryText = document.createElement("div");
|
||||
memoryText.style.cssText = `
|
||||
flex: 1;
|
||||
color: #e5e5e5;
|
||||
`
|
||||
memoryText.textContent = memory.trim()
|
||||
`;
|
||||
memoryText.textContent = memory.trim();
|
||||
|
||||
const removeBtn = document.createElement("button")
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.style.cssText = `
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
|
|
@ -370,154 +370,159 @@ function updateClaudeIconFeedback(
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`
|
||||
removeBtn.dataset.memoryIndex = index.toString()
|
||||
`;
|
||||
removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`;
|
||||
removeBtn.dataset.memoryIndex = index.toString();
|
||||
|
||||
removeBtn.addEventListener("mouseenter", () => {
|
||||
removeBtn.style.color = "#ef4444"
|
||||
})
|
||||
removeBtn.style.color = "#ef4444";
|
||||
});
|
||||
removeBtn.addEventListener("mouseleave", () => {
|
||||
removeBtn.style.color = "#9ca3af"
|
||||
})
|
||||
removeBtn.style.color = "#9ca3af";
|
||||
});
|
||||
|
||||
memoryItem.appendChild(memoryText)
|
||||
memoryItem.appendChild(removeBtn)
|
||||
content.appendChild(memoryItem)
|
||||
})
|
||||
memoryItem.appendChild(memoryText);
|
||||
memoryItem.appendChild(removeBtn);
|
||||
content.appendChild(memoryItem);
|
||||
});
|
||||
|
||||
popup.appendChild(header)
|
||||
popup.appendChild(content)
|
||||
document.body.appendChild(popup)
|
||||
popup.appendChild(header);
|
||||
popup.appendChild(content);
|
||||
document.body.appendChild(popup);
|
||||
|
||||
feedbackDiv.addEventListener("mouseenter", () => {
|
||||
const textSpan = feedbackDiv.querySelector("span:last-child")
|
||||
const textSpan = feedbackDiv.querySelector("span:last-child");
|
||||
if (textSpan) {
|
||||
textSpan.textContent = "Click to see memories"
|
||||
textSpan.textContent = "Click to see memories";
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
feedbackDiv.addEventListener("mouseleave", () => {
|
||||
const textSpan = feedbackDiv.querySelector("span:last-child")
|
||||
const textSpan = feedbackDiv.querySelector("span:last-child");
|
||||
if (textSpan) {
|
||||
textSpan.textContent = "Included Memories"
|
||||
textSpan.textContent = "Included Memories";
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
feedbackDiv.addEventListener("click", (e) => {
|
||||
e.stopPropagation()
|
||||
popup.style.display = "block"
|
||||
})
|
||||
e.stopPropagation();
|
||||
popup.style.display = "block";
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!popup.contains(e.target as Node)) {
|
||||
popup.style.display = "none"
|
||||
popup.style.display = "none";
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
content.querySelectorAll("button[data-memory-index]").forEach((button) => {
|
||||
const htmlButton = button as HTMLButtonElement
|
||||
const htmlButton = button as HTMLButtonElement;
|
||||
htmlButton.addEventListener("click", () => {
|
||||
const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10)
|
||||
const memoryItem = htmlButton.parentElement
|
||||
const index = Number.parseInt(
|
||||
htmlButton.dataset.memoryIndex || "0",
|
||||
10,
|
||||
);
|
||||
const memoryItem = htmlButton.parentElement;
|
||||
|
||||
if (memoryItem) {
|
||||
content.removeChild(memoryItem)
|
||||
content.removeChild(memoryItem);
|
||||
}
|
||||
|
||||
const currentMemories = (iconElement.dataset.memoriesData || "")
|
||||
.split(/[,\n]/)
|
||||
.map((memory) => memory.trim())
|
||||
.filter((memory) => memory.length > 0 && memory !== ",")
|
||||
currentMemories.splice(index, 1)
|
||||
.filter((memory) => memory.length > 0 && memory !== ",");
|
||||
currentMemories.splice(index, 1);
|
||||
|
||||
const updatedMemories = currentMemories.join(" ,")
|
||||
const updatedMemories = currentMemories.join(" ,");
|
||||
|
||||
iconElement.dataset.memoriesData = updatedMemories
|
||||
iconElement.dataset.memoriesData = updatedMemories;
|
||||
|
||||
const textareaElement = document.querySelector(
|
||||
'div[contenteditable="true"]',
|
||||
) as HTMLElement
|
||||
) as HTMLElement;
|
||||
if (textareaElement) {
|
||||
textareaElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${updatedMemories}</div>`
|
||||
textareaElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${updatedMemories}</div>`;
|
||||
}
|
||||
|
||||
content
|
||||
.querySelectorAll("button[data-memory-index]")
|
||||
.forEach((btn, newIndex) => {
|
||||
const htmlBtn = btn as HTMLButtonElement
|
||||
htmlBtn.dataset.memoryIndex = newIndex.toString()
|
||||
})
|
||||
const htmlBtn = btn as HTMLButtonElement;
|
||||
htmlBtn.dataset.memoryIndex = newIndex.toString();
|
||||
});
|
||||
|
||||
if (currentMemories.length <= 1) {
|
||||
if (textareaElement?.dataset.supermemories) {
|
||||
delete textareaElement.dataset.supermemories
|
||||
delete iconElement.dataset.memoriesData
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
|
||||
delete iconElement.dataset.originalHtml
|
||||
delete textareaElement.dataset.supermemories;
|
||||
delete iconElement.dataset.memoriesData;
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml || "";
|
||||
delete iconElement.dataset.originalHtml;
|
||||
}
|
||||
popup.style.display = "none"
|
||||
popup.style.display = "none";
|
||||
if (document.body.contains(popup)) {
|
||||
document.body.removeChild(popup)
|
||||
document.body.removeChild(popup);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(popup)) {
|
||||
document.body.removeChild(popup)
|
||||
document.body.removeChild(popup);
|
||||
}
|
||||
}, 300000)
|
||||
}, 300000);
|
||||
}
|
||||
|
||||
iconElement.innerHTML = ""
|
||||
iconElement.appendChild(feedbackDiv)
|
||||
iconElement.innerHTML = "";
|
||||
iconElement.appendChild(feedbackDiv);
|
||||
|
||||
if (resetAfter > 0) {
|
||||
setTimeout(() => {
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
|
||||
delete iconElement.dataset.originalHtml
|
||||
}, resetAfter)
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml || "";
|
||||
delete iconElement.dataset.originalHtml;
|
||||
}, resetAfter);
|
||||
}
|
||||
}
|
||||
|
||||
function setupClaudePromptCapture() {
|
||||
if (document.body.hasAttribute("data-claude-prompt-capture-setup")) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
document.body.setAttribute("data-claude-prompt-capture-setup", "true")
|
||||
document.body.setAttribute("data-claude-prompt-capture-setup", "true");
|
||||
const captureClaudePromptContent = async (source: string) => {
|
||||
let promptContent = ""
|
||||
let promptContent = "";
|
||||
|
||||
const contentEditableDiv = document.querySelector(
|
||||
'div[contenteditable="true"]',
|
||||
) as HTMLElement
|
||||
) as HTMLElement;
|
||||
if (contentEditableDiv) {
|
||||
promptContent =
|
||||
contentEditableDiv.textContent || contentEditableDiv.innerText || ""
|
||||
contentEditableDiv.textContent || contentEditableDiv.innerText || "";
|
||||
}
|
||||
|
||||
if (!promptContent) {
|
||||
const textarea = document.querySelector("textarea") as HTMLTextAreaElement
|
||||
const textarea = document.querySelector(
|
||||
"textarea",
|
||||
) as HTMLTextAreaElement;
|
||||
if (textarea) {
|
||||
promptContent = textarea.value || ""
|
||||
promptContent = textarea.value || "";
|
||||
}
|
||||
}
|
||||
|
||||
const storedMemories = contentEditableDiv?.dataset.supermemories
|
||||
const storedMemories = contentEditableDiv?.dataset.supermemories;
|
||||
if (
|
||||
storedMemories &&
|
||||
contentEditableDiv &&
|
||||
!promptContent.includes("Supermemories of user")
|
||||
) {
|
||||
contentEditableDiv.innerHTML = `${contentEditableDiv.innerHTML} ${storedMemories}`
|
||||
contentEditableDiv.innerHTML = `${contentEditableDiv.innerHTML} ${storedMemories}`;
|
||||
promptContent =
|
||||
contentEditableDiv.textContent || contentEditableDiv.innerText || ""
|
||||
contentEditableDiv.textContent || contentEditableDiv.innerText || "";
|
||||
}
|
||||
|
||||
if (promptContent.trim()) {
|
||||
console.log(`Claude prompt submitted via ${source}:`, promptContent)
|
||||
console.log(`Claude prompt submitted via ${source}:`, promptContent);
|
||||
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
|
|
@ -527,52 +532,52 @@ function setupClaudePromptCapture() {
|
|||
platform: "claude",
|
||||
source: source,
|
||||
},
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error sending Claude prompt to background:", error)
|
||||
console.error("Error sending Claude prompt to background:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const icons = document.querySelectorAll(
|
||||
'[id*="sm-claude-input-bar-element"]',
|
||||
)
|
||||
);
|
||||
|
||||
icons.forEach((icon) => {
|
||||
const iconElement = icon as HTMLElement
|
||||
const iconElement = icon as HTMLElement;
|
||||
if (iconElement.dataset.originalHtml) {
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml
|
||||
delete iconElement.dataset.originalHtml
|
||||
delete iconElement.dataset.memoriesData
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml;
|
||||
delete iconElement.dataset.originalHtml;
|
||||
delete iconElement.dataset.memoriesData;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (contentEditableDiv?.dataset.supermemories) {
|
||||
delete contentEditableDiv.dataset.supermemories
|
||||
delete contentEditableDiv.dataset.supermemories;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener(
|
||||
"click",
|
||||
async (event) => {
|
||||
const target = event.target as HTMLElement
|
||||
const target = event.target as HTMLElement;
|
||||
const sendButton =
|
||||
target.closest(
|
||||
"button.inline-flex.items-center.justify-center.relative.shrink-0.can-focus.select-none",
|
||||
) ||
|
||||
target.closest('button[class*="bg-accent-main-000"]') ||
|
||||
target.closest('button[class*="rounded-lg"]')
|
||||
target.closest('button[class*="rounded-lg"]');
|
||||
|
||||
if (sendButton) {
|
||||
await captureClaudePromptContent("button click")
|
||||
await captureClaudePromptContent("button click");
|
||||
}
|
||||
},
|
||||
true,
|
||||
)
|
||||
);
|
||||
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
async (event) => {
|
||||
const target = event.target as HTMLElement
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
if (
|
||||
(target.matches('div[contenteditable="true"]') ||
|
||||
|
|
@ -582,67 +587,67 @@ function setupClaudePromptCapture() {
|
|||
event.key === "Enter" &&
|
||||
!event.shiftKey
|
||||
) {
|
||||
await captureClaudePromptContent("Enter key")
|
||||
await captureClaudePromptContent("Enter key");
|
||||
}
|
||||
},
|
||||
true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function setupClaudeAutoFetch() {
|
||||
const result = await chrome.storage.local.get([
|
||||
STORAGE_KEYS.AUTO_SEARCH_ENABLED,
|
||||
])
|
||||
const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false
|
||||
]);
|
||||
const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false;
|
||||
if (!autoSearchEnabled) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const textareaElement = document.querySelector(
|
||||
'div[contenteditable="true"]',
|
||||
) as HTMLElement
|
||||
) as HTMLElement;
|
||||
|
||||
if (
|
||||
!textareaElement ||
|
||||
textareaElement.hasAttribute("data-supermemory-auto-fetch")
|
||||
) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
textareaElement.setAttribute("data-supermemory-auto-fetch", "true")
|
||||
textareaElement.setAttribute("data-supermemory-auto-fetch", "true");
|
||||
|
||||
const handleInput = () => {
|
||||
if (claudeDebounceTimeout) {
|
||||
clearTimeout(claudeDebounceTimeout)
|
||||
clearTimeout(claudeDebounceTimeout);
|
||||
}
|
||||
|
||||
claudeDebounceTimeout = setTimeout(async () => {
|
||||
const content = textareaElement.textContent?.trim() || ""
|
||||
const content = textareaElement.textContent?.trim() || "";
|
||||
|
||||
if (content.length > 2) {
|
||||
await getRelatedMemoriesForClaude(
|
||||
POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED,
|
||||
)
|
||||
);
|
||||
} else if (content.length === 0) {
|
||||
const icons = document.querySelectorAll(
|
||||
'[id*="sm-claude-input-bar-element"]',
|
||||
)
|
||||
);
|
||||
|
||||
icons.forEach((icon) => {
|
||||
const iconElement = icon as HTMLElement
|
||||
const iconElement = icon as HTMLElement;
|
||||
if (iconElement.dataset.originalHtml) {
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml
|
||||
delete iconElement.dataset.originalHtml
|
||||
delete iconElement.dataset.memoriesData
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml;
|
||||
delete iconElement.dataset.originalHtml;
|
||||
delete iconElement.dataset.memoriesData;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (textareaElement.dataset.supermemories) {
|
||||
delete textareaElement.dataset.supermemories
|
||||
delete textareaElement.dataset.supermemories;
|
||||
}
|
||||
}
|
||||
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
|
||||
}
|
||||
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY);
|
||||
};
|
||||
|
||||
textareaElement.addEventListener("input", handleInput)
|
||||
textareaElement.addEventListener("input", handleInput);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
import { DOMAINS, MESSAGE_TYPES } from "../../utils/constants"
|
||||
import { DOMUtils } from "../../utils/ui-components"
|
||||
import { initializeChatGPT } from "./chatgpt"
|
||||
import { initializeClaude } from "./claude"
|
||||
import { saveMemory, setupGlobalKeyboardShortcut, setupStorageListener } from "./shared"
|
||||
import { initializeT3 } from "./t3"
|
||||
import { handleTwitterNavigation, initializeTwitter, updateTwitterImportUI } from "./twitter"
|
||||
import { DOMAINS, MESSAGE_TYPES } from "../../utils/constants";
|
||||
import { DOMUtils } from "../../utils/ui-components";
|
||||
import { initializeChatGPT } from "./chatgpt";
|
||||
import { initializeClaude } from "./claude";
|
||||
import {
|
||||
saveMemory,
|
||||
setupGlobalKeyboardShortcut,
|
||||
setupStorageListener,
|
||||
} from "./shared";
|
||||
import { initializeT3 } from "./t3";
|
||||
import {
|
||||
handleTwitterNavigation,
|
||||
initializeTwitter,
|
||||
updateTwitterImportUI,
|
||||
} from "./twitter";
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["<all_urls>"],
|
||||
|
|
@ -12,56 +20,56 @@ export default defineContentScript({
|
|||
// Setup global event listeners
|
||||
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);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Setup global keyboard shortcuts
|
||||
setupGlobalKeyboardShortcut()
|
||||
setupGlobalKeyboardShortcut();
|
||||
|
||||
// Setup storage listener
|
||||
setupStorageListener()
|
||||
setupStorageListener();
|
||||
|
||||
// Observer for dynamic content changes
|
||||
const observeForDynamicChanges = () => {
|
||||
const observer = new MutationObserver(() => {
|
||||
if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
|
||||
initializeChatGPT()
|
||||
initializeChatGPT();
|
||||
}
|
||||
if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
|
||||
initializeClaude()
|
||||
initializeClaude();
|
||||
}
|
||||
if (DOMUtils.isOnDomain(DOMAINS.T3)) {
|
||||
initializeT3()
|
||||
initializeT3();
|
||||
}
|
||||
if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
|
||||
handleTwitterNavigation()
|
||||
handleTwitterNavigation();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize platform-specific functionality
|
||||
initializeChatGPT()
|
||||
initializeClaude()
|
||||
initializeT3()
|
||||
initializeTwitter()
|
||||
initializeChatGPT();
|
||||
initializeClaude();
|
||||
initializeT3();
|
||||
initializeTwitter();
|
||||
|
||||
// Start observing for dynamic changes
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", observeForDynamicChanges)
|
||||
document.addEventListener("DOMContentLoaded", observeForDynamicChanges);
|
||||
} else {
|
||||
observeForDynamicChanges()
|
||||
observeForDynamicChanges();
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { MESSAGE_TYPES, STORAGE_KEYS } from "../../utils/constants"
|
||||
import { DOMUtils } from "../../utils/ui-components"
|
||||
import { MESSAGE_TYPES, STORAGE_KEYS } from "../../utils/constants";
|
||||
import { DOMUtils } from "../../utils/ui-components";
|
||||
|
||||
export async function saveMemory() {
|
||||
try {
|
||||
DOMUtils.showToast("loading")
|
||||
DOMUtils.showToast("loading");
|
||||
|
||||
const highlightedText = window.getSelection()?.toString() || ""
|
||||
const url = window.location.href
|
||||
const html = document.documentElement.outerHTML
|
||||
const highlightedText = window.getSelection()?.toString() || "";
|
||||
const url = window.location.href;
|
||||
const html = document.documentElement.outerHTML;
|
||||
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.SAVE_MEMORY,
|
||||
|
|
@ -17,17 +17,17 @@ export async function saveMemory() {
|
|||
url,
|
||||
},
|
||||
actionSource: "context_menu",
|
||||
})
|
||||
});
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,19 +38,19 @@ export function setupGlobalKeyboardShortcut() {
|
|||
event.shiftKey &&
|
||||
event.key === "m"
|
||||
) {
|
||||
event.preventDefault()
|
||||
await saveMemory()
|
||||
event.preventDefault();
|
||||
await saveMemory();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export function setupStorageListener() {
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.source !== window) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const bearerToken = event.data.token
|
||||
const userData = event.data.userData
|
||||
const bearerToken = event.data.token;
|
||||
const userData = event.data.userData;
|
||||
if (bearerToken && userData) {
|
||||
if (
|
||||
!(
|
||||
|
|
@ -61,8 +61,8 @@ export function setupStorageListener() {
|
|||
) {
|
||||
console.log(
|
||||
"Bearer token and user data is only allowed to be used on localhost or supermemory.ai",
|
||||
)
|
||||
return
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.local.set(
|
||||
|
|
@ -71,7 +71,7 @@ export function setupStorageListener() {
|
|||
[STORAGE_KEYS.USER_DATA]: userData,
|
||||
},
|
||||
() => {},
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,204 +5,206 @@ import {
|
|||
POSTHOG_EVENT_KEY,
|
||||
STORAGE_KEYS,
|
||||
UI_CONFIG,
|
||||
} from "../../utils/constants"
|
||||
import { createT3InputBarElement, DOMUtils } from "../../utils/ui-components"
|
||||
} from "../../utils/constants";
|
||||
import { createT3InputBarElement, DOMUtils } from "../../utils/ui-components";
|
||||
|
||||
let t3DebounceTimeout: NodeJS.Timeout | null = null
|
||||
let t3RouteObserver: MutationObserver | null = null
|
||||
let t3UrlCheckInterval: NodeJS.Timeout | null = null
|
||||
let t3ObserverThrottle: NodeJS.Timeout | null = null
|
||||
let t3DebounceTimeout: NodeJS.Timeout | null = null;
|
||||
let t3RouteObserver: MutationObserver | null = null;
|
||||
let t3UrlCheckInterval: NodeJS.Timeout | null = null;
|
||||
let t3ObserverThrottle: NodeJS.Timeout | null = null;
|
||||
|
||||
export function initializeT3() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.T3)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.body.hasAttribute("data-t3-initialized")) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
console.log("Adding supermemory icon to T3 input")
|
||||
addSupermemoryIconToT3Input()
|
||||
setupT3AutoFetch()
|
||||
}, 2000)
|
||||
console.log("Adding supermemory icon to T3 input");
|
||||
addSupermemoryIconToT3Input();
|
||||
setupT3AutoFetch();
|
||||
}, 2000);
|
||||
|
||||
setupT3PromptCapture()
|
||||
setupT3PromptCapture();
|
||||
|
||||
setupT3RouteChangeDetection()
|
||||
setupT3RouteChangeDetection();
|
||||
|
||||
document.body.setAttribute("data-t3-initialized", "true")
|
||||
document.body.setAttribute("data-t3-initialized", "true");
|
||||
}
|
||||
|
||||
function setupT3RouteChangeDetection() {
|
||||
if (t3RouteObserver) {
|
||||
t3RouteObserver.disconnect()
|
||||
t3RouteObserver.disconnect();
|
||||
}
|
||||
if (t3UrlCheckInterval) {
|
||||
clearInterval(t3UrlCheckInterval)
|
||||
clearInterval(t3UrlCheckInterval);
|
||||
}
|
||||
if (t3ObserverThrottle) {
|
||||
clearTimeout(t3ObserverThrottle)
|
||||
t3ObserverThrottle = null
|
||||
clearTimeout(t3ObserverThrottle);
|
||||
t3ObserverThrottle = null;
|
||||
}
|
||||
|
||||
let currentUrl = window.location.href
|
||||
let currentUrl = window.location.href;
|
||||
|
||||
const checkForRouteChange = () => {
|
||||
if (window.location.href !== currentUrl) {
|
||||
currentUrl = window.location.href
|
||||
console.log("T3 route changed, re-adding supermemory icon")
|
||||
currentUrl = window.location.href;
|
||||
console.log("T3 route changed, re-adding supermemory icon");
|
||||
setTimeout(() => {
|
||||
addSupermemoryIconToT3Input()
|
||||
setupT3AutoFetch()
|
||||
}, 1000)
|
||||
addSupermemoryIconToT3Input();
|
||||
setupT3AutoFetch();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
t3UrlCheckInterval = setInterval(checkForRouteChange, 2000)
|
||||
t3UrlCheckInterval = setInterval(checkForRouteChange, 2000);
|
||||
|
||||
t3RouteObserver = new MutationObserver((mutations) => {
|
||||
if (t3ObserverThrottle) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldRecheck = false
|
||||
let shouldRecheck = false;
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element
|
||||
const element = node as Element;
|
||||
if (
|
||||
element.querySelector?.("textarea") ||
|
||||
element.querySelector?.('div[contenteditable="true"]') ||
|
||||
element.matches?.("textarea") ||
|
||||
element.matches?.('div[contenteditable="true"]')
|
||||
) {
|
||||
shouldRecheck = true
|
||||
shouldRecheck = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (shouldRecheck) {
|
||||
t3ObserverThrottle = setTimeout(() => {
|
||||
try {
|
||||
t3ObserverThrottle = null
|
||||
addSupermemoryIconToT3Input()
|
||||
setupT3AutoFetch()
|
||||
t3ObserverThrottle = null;
|
||||
addSupermemoryIconToT3Input();
|
||||
setupT3AutoFetch();
|
||||
} catch (error) {
|
||||
console.error("Error in T3 observer callback:", error)
|
||||
console.error("Error in T3 observer callback:", error);
|
||||
}
|
||||
}, 300)
|
||||
}, 300);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
try {
|
||||
t3RouteObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to set up T3 route observer:", error)
|
||||
console.error("Failed to set up T3 route observer:", error);
|
||||
if (t3UrlCheckInterval) {
|
||||
clearInterval(t3UrlCheckInterval)
|
||||
clearInterval(t3UrlCheckInterval);
|
||||
}
|
||||
t3UrlCheckInterval = setInterval(checkForRouteChange, 1000)
|
||||
t3UrlCheckInterval = setInterval(checkForRouteChange, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function addSupermemoryIconToT3Input() {
|
||||
const targetContainers = document.querySelectorAll(
|
||||
".flex.min-w-0.items-center.gap-2.overflow-hidden",
|
||||
)
|
||||
);
|
||||
|
||||
targetContainers.forEach((container) => {
|
||||
if (container.hasAttribute("data-supermemory-icon-added")) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const existingIcon = container.querySelector(
|
||||
`#${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}`,
|
||||
)
|
||||
);
|
||||
if (existingIcon) {
|
||||
container.setAttribute("data-supermemory-icon-added", "true")
|
||||
return
|
||||
container.setAttribute("data-supermemory-icon-added", "true");
|
||||
return;
|
||||
}
|
||||
|
||||
const supermemoryIcon = createT3InputBarElement(async () => {
|
||||
await getRelatedMemoriesForT3(POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_SEARCHED)
|
||||
})
|
||||
await getRelatedMemoriesForT3(
|
||||
POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_SEARCHED,
|
||||
);
|
||||
});
|
||||
|
||||
supermemoryIcon.id = `${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
|
||||
supermemoryIcon.id = `${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
|
||||
container.setAttribute("data-supermemory-icon-added", "true")
|
||||
container.setAttribute("data-supermemory-icon-added", "true");
|
||||
|
||||
container.insertBefore(supermemoryIcon, container.firstChild)
|
||||
})
|
||||
container.insertBefore(supermemoryIcon, container.firstChild);
|
||||
});
|
||||
}
|
||||
|
||||
async function getRelatedMemoriesForT3(actionSource: string) {
|
||||
try {
|
||||
let userQuery = ""
|
||||
let userQuery = "";
|
||||
|
||||
const supermemoryContainer = document.querySelector(
|
||||
'[data-supermemory-icon-added="true"]',
|
||||
)
|
||||
);
|
||||
if (
|
||||
supermemoryContainer?.parentElement?.parentElement?.previousElementSibling
|
||||
) {
|
||||
const textareaElement =
|
||||
supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector(
|
||||
"textarea",
|
||||
)
|
||||
userQuery = textareaElement?.value || ""
|
||||
);
|
||||
userQuery = textareaElement?.value || "";
|
||||
}
|
||||
|
||||
if (!userQuery.trim()) {
|
||||
const textareaElement = document.querySelector(
|
||||
'div[contenteditable="true"]',
|
||||
) as HTMLElement
|
||||
) as HTMLElement;
|
||||
userQuery =
|
||||
textareaElement?.innerText || textareaElement?.textContent || ""
|
||||
textareaElement?.innerText || textareaElement?.textContent || "";
|
||||
}
|
||||
|
||||
if (!userQuery.trim()) {
|
||||
const textareas = document.querySelectorAll("textarea")
|
||||
const textareas = document.querySelectorAll("textarea");
|
||||
for (const textarea of textareas) {
|
||||
const text = (textarea as HTMLTextAreaElement).value
|
||||
const text = (textarea as HTMLTextAreaElement).value;
|
||||
if (text?.trim()) {
|
||||
userQuery = text.trim()
|
||||
break
|
||||
userQuery = text.trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("T3 query extracted:", userQuery)
|
||||
console.log("T3 query extracted:", userQuery);
|
||||
|
||||
if (!userQuery.trim()) {
|
||||
console.log("No query text found for T3")
|
||||
return
|
||||
console.log("No query text found for T3");
|
||||
return;
|
||||
}
|
||||
|
||||
const icon = document.querySelector('[id*="sm-t3-input-bar-element"]')
|
||||
const icon = document.querySelector('[id*="sm-t3-input-bar-element"]');
|
||||
|
||||
const iconElement = icon as HTMLElement
|
||||
const iconElement = icon as HTMLElement;
|
||||
|
||||
if (!iconElement) {
|
||||
console.warn("T3 icon element not found, cannot update feedback")
|
||||
return
|
||||
console.warn("T3 icon element not found, cannot update feedback");
|
||||
return;
|
||||
}
|
||||
|
||||
updateT3IconFeedback("Searching memories...", iconElement)
|
||||
updateT3IconFeedback("Searching memories...", iconElement);
|
||||
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error("Memory search timeout")),
|
||||
UI_CONFIG.API_REQUEST_TIMEOUT,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
const response = await Promise.race([
|
||||
browser.runtime.sendMessage({
|
||||
|
|
@ -211,15 +213,15 @@ async function getRelatedMemoriesForT3(actionSource: string) {
|
|||
actionSource: actionSource,
|
||||
}),
|
||||
timeoutPromise,
|
||||
])
|
||||
]);
|
||||
|
||||
console.log("T3 memories response:", response)
|
||||
console.log("T3 memories response:", response);
|
||||
|
||||
if (response?.success && response?.data) {
|
||||
let textareaElement = null
|
||||
let textareaElement = null;
|
||||
const supermemoryContainer = document.querySelector(
|
||||
'[data-supermemory-icon-added="true"]',
|
||||
)
|
||||
);
|
||||
if (
|
||||
supermemoryContainer?.parentElement?.parentElement
|
||||
?.previousElementSibling
|
||||
|
|
@ -227,46 +229,46 @@ async function getRelatedMemoriesForT3(actionSource: string) {
|
|||
textareaElement =
|
||||
supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector(
|
||||
"textarea",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!textareaElement) {
|
||||
textareaElement = document.querySelector(
|
||||
'div[contenteditable="true"]',
|
||||
) as HTMLElement
|
||||
) as HTMLElement;
|
||||
}
|
||||
|
||||
if (textareaElement) {
|
||||
if (textareaElement.tagName === "TEXTAREA") {
|
||||
;(textareaElement as HTMLTextAreaElement).dataset.supermemories =
|
||||
`<br>Supermemories of user (only for the reference): ${response.data}</br>`
|
||||
(textareaElement as HTMLTextAreaElement).dataset.supermemories =
|
||||
`<br>Supermemories of user (only for the reference): ${response.data}</br>`;
|
||||
} else {
|
||||
;(textareaElement as HTMLElement).dataset.supermemories =
|
||||
`<br>Supermemories of user (only for the reference): ${response.data}</br>`
|
||||
(textareaElement as HTMLElement).dataset.supermemories =
|
||||
`<br>Supermemories of user (only for the reference): ${response.data}</br>`;
|
||||
}
|
||||
|
||||
iconElement.dataset.memoriesData = response.data
|
||||
iconElement.dataset.memoriesData = response.data;
|
||||
|
||||
updateT3IconFeedback("Included Memories", iconElement)
|
||||
updateT3IconFeedback("Included Memories", iconElement);
|
||||
} else {
|
||||
console.warn("T3 input area not found after successful memory fetch")
|
||||
updateT3IconFeedback("Memories found", iconElement)
|
||||
console.warn("T3 input area not found after successful memory fetch");
|
||||
updateT3IconFeedback("Memories found", iconElement);
|
||||
}
|
||||
} else {
|
||||
console.warn("No memories found or API response invalid for T3")
|
||||
updateT3IconFeedback("No memories found", iconElement)
|
||||
console.warn("No memories found or API response invalid for T3");
|
||||
updateT3IconFeedback("No memories found", iconElement);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting related memories for T3:", error)
|
||||
console.error("Error getting related memories for T3:", error);
|
||||
try {
|
||||
const icon = document.querySelector(
|
||||
'[id*="sm-t3-input-bar-element"]',
|
||||
) as HTMLElement
|
||||
) as HTMLElement;
|
||||
if (icon) {
|
||||
updateT3IconFeedback("Error fetching memories", icon)
|
||||
updateT3IconFeedback("Error fetching memories", icon);
|
||||
}
|
||||
} catch (feedbackError) {
|
||||
console.error("Failed to update T3 error feedback:", feedbackError)
|
||||
console.error("Failed to update T3 error feedback:", feedbackError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -277,10 +279,10 @@ function updateT3IconFeedback(
|
|||
resetAfter = 0,
|
||||
) {
|
||||
if (!iconElement.dataset.originalHtml) {
|
||||
iconElement.dataset.originalHtml = iconElement.innerHTML
|
||||
iconElement.dataset.originalHtml = iconElement.innerHTML;
|
||||
}
|
||||
|
||||
const feedbackDiv = document.createElement("div")
|
||||
const feedbackDiv = document.createElement("div");
|
||||
feedbackDiv.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -293,15 +295,15 @@ function updateT3IconFeedback(
|
|||
font-weight: 500;
|
||||
cursor: ${message === "Included Memories" ? "pointer" : "default"};
|
||||
position: relative;
|
||||
`
|
||||
`;
|
||||
|
||||
feedbackDiv.innerHTML = `
|
||||
<span>✓</span>
|
||||
<span>${message}</span>
|
||||
`
|
||||
`;
|
||||
|
||||
if (message === "Included Memories" && iconElement.dataset.memoriesData) {
|
||||
const popup = document.createElement("div")
|
||||
const popup = document.createElement("div");
|
||||
popup.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
|
|
@ -319,9 +321,9 @@ function updateT3IconFeedback(
|
|||
z-index: 999999;
|
||||
display: none;
|
||||
border: 1px solid #333;
|
||||
`
|
||||
`;
|
||||
|
||||
const header = document.createElement("div")
|
||||
const header = document.createElement("div");
|
||||
header.style.cssText = `
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -329,28 +331,28 @@ function updateT3IconFeedback(
|
|||
padding: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
opacity: 0.8;
|
||||
`
|
||||
`;
|
||||
header.innerHTML = `
|
||||
<span style="font-weight: 600; color: #fff;">Included Memories</span>
|
||||
`
|
||||
`;
|
||||
|
||||
const content = document.createElement("div")
|
||||
const content = document.createElement("div");
|
||||
content.style.cssText = `
|
||||
padding: 0;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
`
|
||||
`;
|
||||
|
||||
const memoriesText = iconElement.dataset.memoriesData || ""
|
||||
console.log("Memories text:", memoriesText)
|
||||
const memoriesText = iconElement.dataset.memoriesData || "";
|
||||
console.log("Memories text:", memoriesText);
|
||||
const individualMemories = memoriesText
|
||||
.split(/[,\n]/)
|
||||
.map((memory) => memory.trim())
|
||||
.filter((memory) => memory.length > 0 && memory !== ",")
|
||||
console.log("Individual memories:", individualMemories)
|
||||
.filter((memory) => memory.length > 0 && memory !== ",");
|
||||
console.log("Individual memories:", individualMemories);
|
||||
|
||||
individualMemories.forEach((memory, index) => {
|
||||
const memoryItem = document.createElement("div")
|
||||
const memoryItem = document.createElement("div");
|
||||
memoryItem.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -358,16 +360,16 @@ function updateT3IconFeedback(
|
|||
padding: 10px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
`
|
||||
`;
|
||||
|
||||
const memoryText = document.createElement("div")
|
||||
const memoryText = document.createElement("div");
|
||||
memoryText.style.cssText = `
|
||||
flex: 1;
|
||||
color: #e5e5e5;
|
||||
`
|
||||
memoryText.textContent = memory.trim()
|
||||
`;
|
||||
memoryText.textContent = memory.trim();
|
||||
|
||||
const removeBtn = document.createElement("button")
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.style.cssText = `
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
|
|
@ -380,164 +382,169 @@ function updateT3IconFeedback(
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`
|
||||
removeBtn.dataset.memoryIndex = index.toString()
|
||||
`;
|
||||
removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`;
|
||||
removeBtn.dataset.memoryIndex = index.toString();
|
||||
|
||||
removeBtn.addEventListener("mouseenter", () => {
|
||||
removeBtn.style.color = "#ef4444"
|
||||
})
|
||||
removeBtn.style.color = "#ef4444";
|
||||
});
|
||||
removeBtn.addEventListener("mouseleave", () => {
|
||||
removeBtn.style.color = "#9ca3af"
|
||||
})
|
||||
removeBtn.style.color = "#9ca3af";
|
||||
});
|
||||
|
||||
memoryItem.appendChild(memoryText)
|
||||
memoryItem.appendChild(removeBtn)
|
||||
content.appendChild(memoryItem)
|
||||
})
|
||||
memoryItem.appendChild(memoryText);
|
||||
memoryItem.appendChild(removeBtn);
|
||||
content.appendChild(memoryItem);
|
||||
});
|
||||
|
||||
popup.appendChild(header)
|
||||
popup.appendChild(content)
|
||||
document.body.appendChild(popup)
|
||||
popup.appendChild(header);
|
||||
popup.appendChild(content);
|
||||
document.body.appendChild(popup);
|
||||
|
||||
feedbackDiv.addEventListener("mouseenter", () => {
|
||||
const textSpan = feedbackDiv.querySelector("span:last-child")
|
||||
const textSpan = feedbackDiv.querySelector("span:last-child");
|
||||
if (textSpan) {
|
||||
textSpan.textContent = "Click to see memories"
|
||||
textSpan.textContent = "Click to see memories";
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
feedbackDiv.addEventListener("mouseleave", () => {
|
||||
const textSpan = feedbackDiv.querySelector("span:last-child")
|
||||
const textSpan = feedbackDiv.querySelector("span:last-child");
|
||||
if (textSpan) {
|
||||
textSpan.textContent = "Included Memories"
|
||||
textSpan.textContent = "Included Memories";
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
feedbackDiv.addEventListener("click", (e) => {
|
||||
e.stopPropagation()
|
||||
popup.style.display = "block"
|
||||
})
|
||||
e.stopPropagation();
|
||||
popup.style.display = "block";
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!popup.contains(e.target as Node)) {
|
||||
popup.style.display = "none"
|
||||
popup.style.display = "none";
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
content.querySelectorAll("button[data-memory-index]").forEach((button) => {
|
||||
const htmlButton = button as HTMLButtonElement
|
||||
const htmlButton = button as HTMLButtonElement;
|
||||
htmlButton.addEventListener("click", () => {
|
||||
const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10)
|
||||
const memoryItem = htmlButton.parentElement
|
||||
const index = Number.parseInt(
|
||||
htmlButton.dataset.memoryIndex || "0",
|
||||
10,
|
||||
);
|
||||
const memoryItem = htmlButton.parentElement;
|
||||
|
||||
if (memoryItem) {
|
||||
content.removeChild(memoryItem)
|
||||
content.removeChild(memoryItem);
|
||||
}
|
||||
|
||||
const currentMemories = (iconElement.dataset.memoriesData || "")
|
||||
.split(/[,\n]/)
|
||||
.map((memory) => memory.trim())
|
||||
.filter((memory) => memory.length > 0 && memory !== ",")
|
||||
currentMemories.splice(index, 1)
|
||||
.filter((memory) => memory.length > 0 && memory !== ",");
|
||||
currentMemories.splice(index, 1);
|
||||
|
||||
const updatedMemories = currentMemories.join(" ,")
|
||||
const updatedMemories = currentMemories.join(" ,");
|
||||
|
||||
iconElement.dataset.memoriesData = updatedMemories
|
||||
iconElement.dataset.memoriesData = updatedMemories;
|
||||
|
||||
const textareaElement =
|
||||
(document.querySelector("textarea") as HTMLTextAreaElement) ||
|
||||
(document.querySelector('div[contenteditable="true"]') as HTMLElement)
|
||||
(document.querySelector(
|
||||
'div[contenteditable="true"]',
|
||||
) as HTMLElement);
|
||||
if (textareaElement) {
|
||||
textareaElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${updatedMemories}</div>`
|
||||
textareaElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${updatedMemories}</div>`;
|
||||
}
|
||||
|
||||
content
|
||||
.querySelectorAll("button[data-memory-index]")
|
||||
.forEach((btn, newIndex) => {
|
||||
const htmlBtn = btn as HTMLButtonElement
|
||||
htmlBtn.dataset.memoryIndex = newIndex.toString()
|
||||
})
|
||||
const htmlBtn = btn as HTMLButtonElement;
|
||||
htmlBtn.dataset.memoryIndex = newIndex.toString();
|
||||
});
|
||||
|
||||
if (currentMemories.length <= 1) {
|
||||
if (textareaElement?.dataset.supermemories) {
|
||||
delete textareaElement.dataset.supermemories
|
||||
delete iconElement.dataset.memoriesData
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
|
||||
delete iconElement.dataset.originalHtml
|
||||
delete textareaElement.dataset.supermemories;
|
||||
delete iconElement.dataset.memoriesData;
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml || "";
|
||||
delete iconElement.dataset.originalHtml;
|
||||
}
|
||||
popup.style.display = "none"
|
||||
popup.style.display = "none";
|
||||
if (document.body.contains(popup)) {
|
||||
document.body.removeChild(popup)
|
||||
document.body.removeChild(popup);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(popup)) {
|
||||
document.body.removeChild(popup)
|
||||
document.body.removeChild(popup);
|
||||
}
|
||||
}, 300000)
|
||||
}, 300000);
|
||||
}
|
||||
|
||||
iconElement.innerHTML = ""
|
||||
iconElement.appendChild(feedbackDiv)
|
||||
iconElement.innerHTML = "";
|
||||
iconElement.appendChild(feedbackDiv);
|
||||
|
||||
if (resetAfter > 0) {
|
||||
setTimeout(() => {
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
|
||||
delete iconElement.dataset.originalHtml
|
||||
}, resetAfter)
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml || "";
|
||||
delete iconElement.dataset.originalHtml;
|
||||
}, resetAfter);
|
||||
}
|
||||
}
|
||||
|
||||
function setupT3PromptCapture() {
|
||||
if (document.body.hasAttribute("data-t3-prompt-capture-setup")) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
document.body.setAttribute("data-t3-prompt-capture-setup", "true")
|
||||
document.body.setAttribute("data-t3-prompt-capture-setup", "true");
|
||||
|
||||
const captureT3PromptContent = async (source: string) => {
|
||||
let promptContent = ""
|
||||
let promptContent = "";
|
||||
|
||||
const textarea = document.querySelector("textarea") as HTMLTextAreaElement
|
||||
const textarea = document.querySelector("textarea") as HTMLTextAreaElement;
|
||||
if (textarea) {
|
||||
promptContent = textarea.value || ""
|
||||
promptContent = textarea.value || "";
|
||||
}
|
||||
|
||||
if (!promptContent) {
|
||||
const contentEditableDiv = document.querySelector(
|
||||
'div[contenteditable="true"]',
|
||||
) as HTMLElement
|
||||
) as HTMLElement;
|
||||
if (contentEditableDiv) {
|
||||
promptContent =
|
||||
contentEditableDiv.textContent || contentEditableDiv.innerText || ""
|
||||
contentEditableDiv.textContent || contentEditableDiv.innerText || "";
|
||||
}
|
||||
}
|
||||
|
||||
const textareaElement =
|
||||
textarea ||
|
||||
(document.querySelector('div[contenteditable="true"]') as HTMLElement)
|
||||
const storedMemories = textareaElement?.dataset.supermemories
|
||||
(document.querySelector('div[contenteditable="true"]') as HTMLElement);
|
||||
const storedMemories = textareaElement?.dataset.supermemories;
|
||||
if (
|
||||
storedMemories &&
|
||||
textareaElement &&
|
||||
!promptContent.includes("Supermemories of user")
|
||||
) {
|
||||
if (textareaElement.tagName === "TEXTAREA") {
|
||||
;(textareaElement as HTMLTextAreaElement).value =
|
||||
`${promptContent} ${storedMemories}`
|
||||
promptContent = (textareaElement as HTMLTextAreaElement).value
|
||||
(textareaElement as HTMLTextAreaElement).value =
|
||||
`${promptContent} ${storedMemories}`;
|
||||
promptContent = (textareaElement as HTMLTextAreaElement).value;
|
||||
} else {
|
||||
textareaElement.innerHTML = `${textareaElement.innerHTML} ${storedMemories}`
|
||||
textareaElement.innerHTML = `${textareaElement.innerHTML} ${storedMemories}`;
|
||||
promptContent =
|
||||
textareaElement.textContent || textareaElement.innerText || ""
|
||||
textareaElement.textContent || textareaElement.innerText || "";
|
||||
}
|
||||
}
|
||||
|
||||
if (promptContent.trim()) {
|
||||
console.log(`T3 prompt submitted via ${source}:`, promptContent)
|
||||
console.log(`T3 prompt submitted via ${source}:`, promptContent);
|
||||
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
|
|
@ -547,48 +554,48 @@ function setupT3PromptCapture() {
|
|||
platform: "t3",
|
||||
source: source,
|
||||
},
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error sending T3 prompt to background:", error)
|
||||
console.error("Error sending T3 prompt to background:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const icons = document.querySelectorAll('[id*="sm-t3-input-bar-element"]')
|
||||
const icons = document.querySelectorAll('[id*="sm-t3-input-bar-element"]');
|
||||
|
||||
icons.forEach((icon) => {
|
||||
const iconElement = icon as HTMLElement
|
||||
const iconElement = icon as HTMLElement;
|
||||
if (iconElement.dataset.originalHtml) {
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml
|
||||
delete iconElement.dataset.originalHtml
|
||||
delete iconElement.dataset.memoriesData
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml;
|
||||
delete iconElement.dataset.originalHtml;
|
||||
delete iconElement.dataset.memoriesData;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (textareaElement?.dataset.supermemories) {
|
||||
delete textareaElement.dataset.supermemories
|
||||
delete textareaElement.dataset.supermemories;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener(
|
||||
"click",
|
||||
async (event) => {
|
||||
const target = event.target as HTMLElement
|
||||
const target = event.target as HTMLElement;
|
||||
const sendButton =
|
||||
target.closest("button.focus-visible\\:ring-ring") ||
|
||||
target.closest('button[class*="bg-[rgb(162,59,103)]"]') ||
|
||||
target.closest('button[class*="rounded-lg"]')
|
||||
target.closest('button[class*="rounded-lg"]');
|
||||
|
||||
if (sendButton) {
|
||||
await captureT3PromptContent("button click")
|
||||
await captureT3PromptContent("button click");
|
||||
}
|
||||
},
|
||||
true,
|
||||
)
|
||||
);
|
||||
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
async (event) => {
|
||||
const target = event.target as HTMLElement
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
if (
|
||||
(target.matches("textarea") ||
|
||||
|
|
@ -597,9 +604,9 @@ function setupT3PromptCapture() {
|
|||
!event.shiftKey
|
||||
) {
|
||||
if (target.matches("textarea")) {
|
||||
const promptContent = (target as HTMLTextAreaElement).value || ""
|
||||
const promptContent = (target as HTMLTextAreaElement).value || "";
|
||||
if (promptContent.trim()) {
|
||||
console.log("T3 prompt submitted via Enter key:", promptContent)
|
||||
console.log("T3 prompt submitted via Enter key:", promptContent);
|
||||
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
|
|
@ -610,83 +617,83 @@ function setupT3PromptCapture() {
|
|||
source: "Enter key",
|
||||
},
|
||||
actionSource: "t3",
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Error sending T3 textarea prompt to background:",
|
||||
error,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await captureT3PromptContent("Enter key")
|
||||
await captureT3PromptContent("Enter key");
|
||||
}
|
||||
}
|
||||
},
|
||||
true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function setupT3AutoFetch() {
|
||||
const result = await chrome.storage.local.get([
|
||||
STORAGE_KEYS.AUTO_SEARCH_ENABLED,
|
||||
])
|
||||
const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false
|
||||
]);
|
||||
const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false;
|
||||
|
||||
if (!autoSearchEnabled) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const textareaElement =
|
||||
(document.querySelector("textarea") as HTMLTextAreaElement) ||
|
||||
(document.querySelector('div[contenteditable="true"]') as HTMLElement)
|
||||
(document.querySelector('div[contenteditable="true"]') as HTMLElement);
|
||||
|
||||
if (
|
||||
!textareaElement ||
|
||||
textareaElement.hasAttribute("data-supermemory-auto-fetch")
|
||||
) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
textareaElement.setAttribute("data-supermemory-auto-fetch", "true")
|
||||
textareaElement.setAttribute("data-supermemory-auto-fetch", "true");
|
||||
|
||||
const handleInput = () => {
|
||||
if (t3DebounceTimeout) {
|
||||
clearTimeout(t3DebounceTimeout)
|
||||
clearTimeout(t3DebounceTimeout);
|
||||
}
|
||||
|
||||
t3DebounceTimeout = setTimeout(async () => {
|
||||
let content = ""
|
||||
let content = "";
|
||||
if (textareaElement.tagName === "TEXTAREA") {
|
||||
content = (textareaElement as HTMLTextAreaElement).value?.trim() || ""
|
||||
content = (textareaElement as HTMLTextAreaElement).value?.trim() || "";
|
||||
} else {
|
||||
content = textareaElement.textContent?.trim() || ""
|
||||
content = textareaElement.textContent?.trim() || "";
|
||||
}
|
||||
|
||||
if (content.length > 2) {
|
||||
await getRelatedMemoriesForT3(
|
||||
POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_AUTO_SEARCHED,
|
||||
)
|
||||
);
|
||||
} else if (content.length === 0) {
|
||||
const icons = document.querySelectorAll(
|
||||
'[id*="sm-t3-input-bar-element"]',
|
||||
)
|
||||
);
|
||||
|
||||
icons.forEach((icon) => {
|
||||
const iconElement = icon as HTMLElement
|
||||
const iconElement = icon as HTMLElement;
|
||||
if (iconElement.dataset.originalHtml) {
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml
|
||||
delete iconElement.dataset.originalHtml
|
||||
delete iconElement.dataset.memoriesData
|
||||
iconElement.innerHTML = iconElement.dataset.originalHtml;
|
||||
delete iconElement.dataset.originalHtml;
|
||||
delete iconElement.dataset.memoriesData;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (textareaElement.dataset.supermemories) {
|
||||
delete textareaElement.dataset.supermemories
|
||||
delete textareaElement.dataset.supermemories;
|
||||
}
|
||||
}
|
||||
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
|
||||
}
|
||||
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY);
|
||||
};
|
||||
|
||||
textareaElement.addEventListener("input", handleInput)
|
||||
textareaElement.addEventListener("input", handleInput);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,24 +3,24 @@ import {
|
|||
ELEMENT_IDS,
|
||||
MESSAGE_TYPES,
|
||||
POSTHOG_EVENT_KEY,
|
||||
} from "../../utils/constants"
|
||||
import { trackEvent } from "../../utils/posthog"
|
||||
import { createTwitterImportButton, DOMUtils } from "../../utils/ui-components"
|
||||
} from "../../utils/constants";
|
||||
import { trackEvent } from "../../utils/posthog";
|
||||
import { createTwitterImportButton, DOMUtils } from "../../utils/ui-components";
|
||||
|
||||
export function initializeTwitter() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// Initial setup
|
||||
if (window.location.pathname === "/i/bookmarks") {
|
||||
setTimeout(() => {
|
||||
addTwitterImportButton()
|
||||
}, 2000)
|
||||
addTwitterImportButton();
|
||||
}, 2000);
|
||||
} else {
|
||||
// Remove button if not on bookmarks page
|
||||
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
|
||||
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
|
||||
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,75 +28,75 @@ export function initializeTwitter() {
|
|||
function addTwitterImportButton() {
|
||||
// Only show the import button on the bookmarks page
|
||||
if (window.location.pathname !== "/i/bookmarks") {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const button = createTwitterImportButton(async () => {
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
|
||||
})
|
||||
});
|
||||
await trackEvent(POSTHOG_EVENT_KEY.TWITTER_IMPORT_STARTED, {
|
||||
source: `${POSTHOG_EVENT_KEY.SOURCE}_content_script`,
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error starting import:", error)
|
||||
console.error("Error starting import:", error);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
document.body.appendChild(button)
|
||||
document.body.appendChild(button);
|
||||
}
|
||||
|
||||
export function updateTwitterImportUI(message: {
|
||||
type: string
|
||||
importedMessage?: string
|
||||
totalImported?: number
|
||||
type: string;
|
||||
importedMessage?: string;
|
||||
totalImported?: number;
|
||||
}) {
|
||||
const importButton = document.getElementById(
|
||||
ELEMENT_IDS.TWITTER_IMPORT_BUTTON,
|
||||
)
|
||||
if (!importButton) return
|
||||
);
|
||||
if (!importButton) return;
|
||||
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png")
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png");
|
||||
|
||||
if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
|
||||
importButton.innerHTML = `
|
||||
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
|
||||
<span style="font-weight: 500; font-size: 14px;">${message.importedMessage}</span>
|
||||
`
|
||||
importButton.style.cursor = "default"
|
||||
`;
|
||||
importButton.style.cursor = "default";
|
||||
}
|
||||
|
||||
if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
|
||||
importButton.innerHTML = `
|
||||
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
|
||||
<span style="font-weight: 500; font-size: 14px; color: #059669;">✓ Imported ${message.totalImported} tweets!</span>
|
||||
`
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
importButton.innerHTML = `
|
||||
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
|
||||
<span style="font-weight: 500; font-size: 14px;">Import Bookmarks</span>
|
||||
`
|
||||
importButton.style.cursor = "pointer"
|
||||
}, 3000)
|
||||
`;
|
||||
importButton.style.cursor = "pointer";
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleTwitterNavigation() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.location.pathname === "/i/bookmarks") {
|
||||
addTwitterImportButton()
|
||||
addTwitterImportButton();
|
||||
} else {
|
||||
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
|
||||
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
|
||||
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,35 @@
|
|||
import { useQueryClient } from "@tanstack/react-query"
|
||||
import { useEffect, useState } from "react"
|
||||
import "./App.css"
|
||||
import { MESSAGE_TYPES, STORAGE_KEYS } from "../../utils/constants"
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect, useState } from "react";
|
||||
import "./App.css";
|
||||
import { MESSAGE_TYPES, STORAGE_KEYS } from "../../utils/constants";
|
||||
import {
|
||||
useDefaultProject,
|
||||
useProjects,
|
||||
useSetDefaultProject,
|
||||
} from "../../utils/query-hooks"
|
||||
import type { Project } from "../../utils/types"
|
||||
} from "../../utils/query-hooks";
|
||||
import type { Project } from "../../utils/types";
|
||||
|
||||
function App() {
|
||||
const [userSignedIn, setUserSignedIn] = useState<boolean>(false)
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [showProjectSelector, setShowProjectSelector] = useState<boolean>(false)
|
||||
const [currentUrl, setCurrentUrl] = useState<string>("")
|
||||
const [currentTitle, setCurrentTitle] = useState<string>("")
|
||||
const [saving, setSaving] = useState<boolean>(false)
|
||||
const [userSignedIn, setUserSignedIn] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
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" | "settings">(
|
||||
"save",
|
||||
)
|
||||
const [autoSearchEnabled, setAutoSearchEnabled] = useState<boolean>(false)
|
||||
);
|
||||
const [autoSearchEnabled, setAutoSearchEnabled] = useState<boolean>(false);
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const queryClient = useQueryClient();
|
||||
const { data: projects = [], isLoading: loadingProjects } = useProjects({
|
||||
enabled: userSignedIn,
|
||||
})
|
||||
});
|
||||
const { data: defaultProject } = useDefaultProject({
|
||||
enabled: userSignedIn,
|
||||
})
|
||||
const setDefaultProjectMutation = useSetDefaultProject()
|
||||
});
|
||||
const setDefaultProjectMutation = useSetDefaultProject();
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuthStatus = async () => {
|
||||
|
|
@ -36,131 +37,131 @@ function App() {
|
|||
const result = await chrome.storage.local.get([
|
||||
STORAGE_KEYS.BEARER_TOKEN,
|
||||
STORAGE_KEYS.AUTO_SEARCH_ENABLED,
|
||||
])
|
||||
const isSignedIn = !!result[STORAGE_KEYS.BEARER_TOKEN]
|
||||
setUserSignedIn(isSignedIn)
|
||||
]);
|
||||
const isSignedIn = !!result[STORAGE_KEYS.BEARER_TOKEN];
|
||||
setUserSignedIn(isSignedIn);
|
||||
|
||||
const autoSearchSetting =
|
||||
result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false
|
||||
setAutoSearchEnabled(autoSearchSetting)
|
||||
result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false;
|
||||
setAutoSearchEnabled(autoSearchSetting);
|
||||
} 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 handleProjectSelect = (project: Project) => {
|
||||
setDefaultProjectMutation.mutate(project, {
|
||||
onSuccess: () => {
|
||||
setShowProjectSelector(false)
|
||||
setShowProjectSelector(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error setting default project:", error)
|
||||
console.error("Error setting default project:", error);
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleShowProjectSelector = () => {
|
||||
setShowProjectSelector(true)
|
||||
}
|
||||
setShowProjectSelector(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!defaultProject && projects.length > 0) {
|
||||
const firstProject = projects[0]
|
||||
setDefaultProjectMutation.mutate(firstProject)
|
||||
const firstProject = projects[0];
|
||||
setDefaultProjectMutation.mutate(firstProject);
|
||||
}
|
||||
}, [defaultProject, projects, setDefaultProjectMutation])
|
||||
}, [defaultProject, projects, setDefaultProjectMutation]);
|
||||
|
||||
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) {
|
||||
const response = await chrome.tabs.sendMessage(tabs[0].id, {
|
||||
action: MESSAGE_TYPES.SAVE_MEMORY,
|
||||
actionSource: "popup",
|
||||
})
|
||||
});
|
||||
|
||||
if (response?.success) {
|
||||
await chrome.tabs.sendMessage(tabs[0].id, {
|
||||
action: MESSAGE_TYPES.SHOW_TOAST,
|
||||
state: "success",
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
window.close()
|
||||
window.close();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save current page:", error)
|
||||
console.error("Failed to save current page:", error);
|
||||
|
||||
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: MESSAGE_TYPES.SHOW_TOAST,
|
||||
state: "error",
|
||||
})
|
||||
});
|
||||
}
|
||||
} catch (toastError) {
|
||||
console.error("Failed to show error toast:", toastError)
|
||||
console.error("Failed to show error toast:", toastError);
|
||||
}
|
||||
|
||||
window.close()
|
||||
window.close();
|
||||
} finally {
|
||||
setSaving(false)
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAutoSearchToggle = async (enabled: boolean) => {
|
||||
try {
|
||||
await chrome.storage.local.set({
|
||||
[STORAGE_KEYS.AUTO_SEARCH_ENABLED]: enabled,
|
||||
})
|
||||
setAutoSearchEnabled(enabled)
|
||||
});
|
||||
setAutoSearchEnabled(enabled);
|
||||
} catch (error) {
|
||||
console.error("Error updating auto search setting:", error)
|
||||
console.error("Error updating auto search setting:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
await chrome.storage.local.remove([STORAGE_KEYS.BEARER_TOKEN])
|
||||
await chrome.storage.local.remove([STORAGE_KEYS.USER_DATA])
|
||||
await chrome.storage.local.remove([STORAGE_KEYS.DEFAULT_PROJECT])
|
||||
setUserSignedIn(false)
|
||||
queryClient.clear()
|
||||
await chrome.storage.local.remove([STORAGE_KEYS.BEARER_TOKEN]);
|
||||
await chrome.storage.local.remove([STORAGE_KEYS.USER_DATA]);
|
||||
await chrome.storage.local.remove([STORAGE_KEYS.DEFAULT_PROJECT]);
|
||||
setUserSignedIn(false);
|
||||
queryClient.clear();
|
||||
} catch (error) {
|
||||
console.error("Error signing out:", error)
|
||||
console.error("Error signing out:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
|
@ -179,7 +180,7 @@ function App() {
|
|||
<div>Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -330,7 +331,7 @@ function App() {
|
|||
onClick={() => {
|
||||
chrome.tabs.create({
|
||||
url: "https://chatgpt.com/#settings/Personalization",
|
||||
})
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -361,17 +362,17 @@ function App() {
|
|||
const [activeTab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
})
|
||||
});
|
||||
|
||||
const targetUrl = "https://x.com/i/bookmarks"
|
||||
const targetUrl = "https://x.com/i/bookmarks";
|
||||
|
||||
if (activeTab?.url === targetUrl) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
await chrome.tabs.create({
|
||||
url: targetUrl,
|
||||
})
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -504,7 +505,7 @@ function App() {
|
|||
<button
|
||||
className="bg-transparent border-none text-blue-500 cursor-pointer underline text-sm p-0 hover:text-blue-700"
|
||||
onClick={() => {
|
||||
window.open("mailto:dhravya@supermemory.com", "_blank")
|
||||
window.open("mailto:dhravya@supermemory.com", "_blank");
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -519,7 +520,7 @@ function App() {
|
|||
url: import.meta.env.PROD
|
||||
? "https://app.supermemory.ai/login"
|
||||
: "http://localhost:3000/login",
|
||||
})
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -530,7 +531,7 @@ function App() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { QueryClientProvider } from "@tanstack/react-query"
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { queryClient } from "../../utils/query-client"
|
||||
import App from "./App.js"
|
||||
import "./style.css"
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { queryClient } from "../../utils/query-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>
|
||||
|
|
@ -13,5 +13,5 @@ if (rootElement) {
|
|||
<App />
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ function Welcome() {
|
|||
url: import.meta.env.PROD
|
||||
? "https://app.supermemory.ai/login"
|
||||
: "http://localhost:3000/login",
|
||||
})
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -101,7 +101,7 @@ function Welcome() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Welcome
|
||||
export default Welcome;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { QueryClientProvider } from "@tanstack/react-query"
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { queryClient } from "../../utils/query-client"
|
||||
import Welcome from "./Welcome"
|
||||
import "./welcome.css"
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { queryClient } from "../../utils/query-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>
|
||||
|
|
@ -13,5 +13,5 @@ if (rootElement) {
|
|||
<Welcome />
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,11 +62,11 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,11 +77,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,10 +92,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,11 +107,11 @@ export async function saveMemory(payload: MemoryPayload): Promise<unknown> {
|
|||
const response = await makeAuthenticatedRequest<unknown>("/v3/documents", {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,11 +123,11 @@ export async function searchMemories(query: string): Promise<unknown> {
|
|||
const response = await makeAuthenticatedRequest<unknown>("/v4/search", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ q: query, include: { relatedMemories: true } }),
|
||||
})
|
||||
return response
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Failed to search memories:", error)
|
||||
throw error
|
||||
console.error("Failed to search memories:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -150,13 +150,13 @@ export async function saveAllTweets(
|
|||
},
|
||||
}),
|
||||
},
|
||||
)
|
||||
return response
|
||||
);
|
||||
return response;
|
||||
} 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: "twitter-auth-token",
|
||||
DEFAULT_PROJECT: "sm-default-project",
|
||||
AUTO_SEARCH_ENABLED: "sm-auto-search-enabled",
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* DOM Element IDs
|
||||
|
|
@ -35,7 +35,7 @@ export const ELEMENT_IDS = {
|
|||
CHATGPT_INPUT_BAR_ELEMENT: "sm-chatgpt-input-bar-element",
|
||||
CLAUDE_INPUT_BAR_ELEMENT: "sm-claude-input-bar-element",
|
||||
T3_INPUT_BAR_ELEMENT: "sm-t3-input-bar-element",
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* UI Configuration
|
||||
|
|
@ -49,7 +49,7 @@ export const UI_CONFIG = {
|
|||
OBSERVER_THROTTLE_DELAY: 300, // milliseconds between observer callback executions
|
||||
ROUTE_CHECK_INTERVAL: 2000, // milliseconds between route change checks
|
||||
API_REQUEST_TIMEOUT: 10000, // milliseconds for API request timeout
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Supported Domains
|
||||
|
|
@ -60,7 +60,7 @@ export const DOMAINS = {
|
|||
CLAUDE: ["claude.ai"],
|
||||
T3: ["t3.chat"],
|
||||
SUPERMEMORY: ["localhost", "supermemory.ai", "app.supermemory.ai"],
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Container Tags
|
||||
|
|
@ -68,7 +68,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
|
||||
|
|
@ -81,11 +81,11 @@ export const MESSAGE_TYPES = {
|
|||
IMPORT_DONE: "sm-import-done",
|
||||
GET_RELATED_MEMORIES: "sm-get-related-memories",
|
||||
CAPTURE_PROMPT: "sm-capture-prompt",
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
export const CONTEXT_MENU_IDS = {
|
||||
SAVE_TO_SUPERMEMORY: "sm-save-to-supermemory",
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
export const POSTHOG_EVENT_KEY = {
|
||||
TWITTER_IMPORT_STARTED: "twitter_import_started",
|
||||
|
|
@ -98,4 +98,4 @@ export const POSTHOG_EVENT_KEY = {
|
|||
CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED: "claude_chat_memories_auto_searched",
|
||||
CHATGPT_CHAT_MEMORIES_SEARCHED: "chatgpt_chat_memories_searched",
|
||||
CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED: "chatgpt_chat_memories_auto_searched",
|
||||
} as const
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@
|
|||
*/
|
||||
|
||||
export interface MemoryPopupConfig {
|
||||
memoriesData: string
|
||||
onClose: () => void
|
||||
onRemove?: () => void
|
||||
memoriesData: string;
|
||||
onClose: () => void;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
export function createMemoryPopup(config: MemoryPopupConfig): HTMLElement {
|
||||
const popup = document.createElement("div")
|
||||
const popup = document.createElement("div");
|
||||
popup.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
|
|
@ -28,9 +28,9 @@ export function createMemoryPopup(config: MemoryPopupConfig): HTMLElement {
|
|||
z-index: 999999;
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
`
|
||||
`;
|
||||
|
||||
const header = document.createElement("div")
|
||||
const header = document.createElement("div");
|
||||
header.style.cssText = `
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -38,56 +38,56 @@ export function createMemoryPopup(config: MemoryPopupConfig): HTMLElement {
|
|||
padding: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
opacity: 0.8;
|
||||
`
|
||||
`;
|
||||
header.innerHTML = `
|
||||
<span style="font-size: 11px; font-weight: 600; letter-spacing: 0.5px;">INCLUDED MEMORIES</span>
|
||||
<div style="display: flex; gap: 4px;">
|
||||
${config.onRemove ? '<button id="remove-memories-btn" style="background: none; border: none; color: #ff4444; cursor: pointer; font-size: 14px; padding: 2px; border-radius: 2px;" title="Remove memories">✕</button>' : ""}
|
||||
<button id="close-popup-btn" style="background: none; border: none; color: white; cursor: pointer; font-size: 14px; padding: 2px; border-radius: 2px;">✕</button>
|
||||
</div>
|
||||
`
|
||||
`;
|
||||
|
||||
const content = document.createElement("div")
|
||||
const content = document.createElement("div");
|
||||
content.style.cssText = `
|
||||
padding: 8px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
line-height: 1.4;
|
||||
`
|
||||
content.textContent = config.memoriesData
|
||||
`;
|
||||
content.textContent = config.memoriesData;
|
||||
|
||||
const closeBtn = header.querySelector("#close-popup-btn")
|
||||
closeBtn?.addEventListener("click", config.onClose)
|
||||
const closeBtn = header.querySelector("#close-popup-btn");
|
||||
closeBtn?.addEventListener("click", config.onClose);
|
||||
|
||||
const removeBtn = header.querySelector("#remove-memories-btn")
|
||||
const removeBtn = header.querySelector("#remove-memories-btn");
|
||||
if (removeBtn && config.onRemove) {
|
||||
removeBtn.addEventListener("click", config.onRemove)
|
||||
removeBtn.addEventListener("click", config.onRemove);
|
||||
}
|
||||
|
||||
popup.appendChild(header)
|
||||
popup.appendChild(content)
|
||||
popup.appendChild(header);
|
||||
popup.appendChild(content);
|
||||
|
||||
return popup
|
||||
return popup;
|
||||
}
|
||||
|
||||
export function showMemoryPopup(popup: HTMLElement): void {
|
||||
popup.style.display = "block"
|
||||
popup.style.display = "block";
|
||||
|
||||
setTimeout(() => {
|
||||
if (popup.style.display === "block") {
|
||||
hideMemoryPopup(popup)
|
||||
hideMemoryPopup(popup);
|
||||
}
|
||||
}, 10000)
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
export function hideMemoryPopup(popup: HTMLElement): void {
|
||||
popup.style.display = "none"
|
||||
popup.style.display = "none";
|
||||
}
|
||||
|
||||
export function toggleMemoryPopup(popup: HTMLElement): void {
|
||||
if (popup.style.display === "none" || popup.style.display === "") {
|
||||
showMemoryPopup(popup)
|
||||
showMemoryPopup(popup);
|
||||
} else {
|
||||
hideMemoryPopup(popup)
|
||||
hideMemoryPopup(popup);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import { PostHog } from "posthog-js/dist/module.no-external"
|
||||
import { STORAGE_KEYS } from "./constants"
|
||||
import { PostHog } from "posthog-js/dist/module.no-external";
|
||||
import { STORAGE_KEYS } from "./constants";
|
||||
|
||||
export async function identifyUser(posthog: PostHog): Promise<void> {
|
||||
const stored = await chrome.storage.local.get([STORAGE_KEYS.USER_DATA])
|
||||
const userData = stored[STORAGE_KEYS.USER_DATA]
|
||||
const stored = await chrome.storage.local.get([STORAGE_KEYS.USER_DATA]);
|
||||
const userData = stored[STORAGE_KEYS.USER_DATA];
|
||||
|
||||
if (userData?.userId) {
|
||||
posthog.identify(userData.userId, {
|
||||
email: userData.email,
|
||||
name: userData.name,
|
||||
userId: userData.userId,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let posthogInstance: PostHog | null = null
|
||||
let initializationPromise: Promise<PostHog> | null = null
|
||||
let posthogInstance: PostHog | null = null;
|
||||
let initializationPromise: Promise<PostHog> | null = null;
|
||||
|
||||
export const POSTHOG_CONFIG = {
|
||||
api_host: "https://api.supermemory.ai/orange",
|
||||
|
|
@ -24,40 +24,40 @@ export const POSTHOG_CONFIG = {
|
|||
persistence: "localStorage",
|
||||
capture_pageview: false,
|
||||
autocapture: false,
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
export async function getPostHogInstance(): Promise<PostHog> {
|
||||
if (posthogInstance) {
|
||||
return posthogInstance
|
||||
return posthogInstance;
|
||||
}
|
||||
|
||||
if (initializationPromise) {
|
||||
return initializationPromise
|
||||
return initializationPromise;
|
||||
}
|
||||
|
||||
initializationPromise = initializePostHog()
|
||||
return initializationPromise
|
||||
initializationPromise = initializePostHog();
|
||||
return initializationPromise;
|
||||
}
|
||||
|
||||
async function initializePostHog(): Promise<PostHog> {
|
||||
try {
|
||||
const posthog = new PostHog()
|
||||
const posthog = new PostHog();
|
||||
|
||||
if (!import.meta.env.WXT_POSTHOG_API_KEY) {
|
||||
console.error("PostHog API key not configured")
|
||||
throw new Error("PostHog API key not configured")
|
||||
console.error("PostHog API key not configured");
|
||||
throw new Error("PostHog API key not configured");
|
||||
}
|
||||
|
||||
posthog.init(import.meta.env.WXT_POSTHOG_API_KEY || "", POSTHOG_CONFIG)
|
||||
posthog.init(import.meta.env.WXT_POSTHOG_API_KEY || "", POSTHOG_CONFIG);
|
||||
|
||||
await identifyUser(posthog)
|
||||
await identifyUser(posthog);
|
||||
|
||||
posthogInstance = posthog
|
||||
return posthog
|
||||
posthogInstance = posthog;
|
||||
return posthog;
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize PostHog:", error)
|
||||
initializationPromise = null
|
||||
throw error
|
||||
console.error("Failed to initialize PostHog:", error);
|
||||
initializationPromise = null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,9 +66,9 @@ export async function trackEvent(
|
|||
properties?: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const posthog = await getPostHogInstance()
|
||||
posthog.capture(eventName, properties)
|
||||
const posthog = await getPostHogInstance();
|
||||
posthog.capture(eventName, properties);
|
||||
} catch (error) {
|
||||
console.error(`Failed to track event ${eventName}:`, error)
|
||||
console.error(`Failed to track event ${eventName}:`, error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* React Query configuration for supermemory browser extension
|
||||
*/
|
||||
import { QueryClient } from "@tanstack/react-query"
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
|
|
@ -11,9 +11,9 @@ export const queryClient = new QueryClient({
|
|||
retry: (failureCount, error) => {
|
||||
// Don't retry on authentication errors
|
||||
if (error?.constructor?.name === "AuthenticationError") {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
return failureCount < 3
|
||||
return failureCount < 3;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
|
|
@ -21,4 +21,4 @@ export const queryClient = new QueryClient({
|
|||
retry: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
/**
|
||||
* React Query hooks for supermemory API
|
||||
*/
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
fetchProjects,
|
||||
getDefaultProject,
|
||||
saveMemory,
|
||||
searchMemories,
|
||||
setDefaultProject,
|
||||
} from "./api"
|
||||
import type { MemoryPayload } from "./types"
|
||||
} from "./api";
|
||||
import type { MemoryPayload } from "./types";
|
||||
|
||||
// Query Keys
|
||||
export const queryKeys = {
|
||||
projects: ["projects"] as const,
|
||||
defaultProject: ["defaultProject"] as const,
|
||||
}
|
||||
};
|
||||
|
||||
// Projects Query
|
||||
export function useProjects(options?: { enabled?: boolean }) {
|
||||
|
|
@ -24,7 +24,7 @@ export function useProjects(options?: { enabled?: boolean }) {
|
|||
queryFn: fetchProjects,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
enabled: options?.enabled ?? true,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Default Project Query
|
||||
|
|
@ -34,31 +34,31 @@ export function useDefaultProject(options?: { enabled?: boolean }) {
|
|||
queryFn: getDefaultProject,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
enabled: options?.enabled ?? true,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Set Default Project Mutation
|
||||
export function useSetDefaultProject() {
|
||||
const queryClient = useQueryClient()
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: setDefaultProject,
|
||||
onSuccess: (_, project) => {
|
||||
queryClient.setQueryData(queryKeys.defaultProject, project)
|
||||
queryClient.setQueryData(queryKeys.defaultProject, project);
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Save Memory Mutation
|
||||
export function useSaveMemory() {
|
||||
return useMutation({
|
||||
mutationFn: (payload: MemoryPayload) => saveMemory(payload),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Search Memories Mutation
|
||||
export function useSearchMemories() {
|
||||
return useMutation({
|
||||
mutationFn: (query: string) => searchMemories(query),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,20 +3,20 @@
|
|||
* Shared logic for detecting route changes across different AI chat platforms
|
||||
*/
|
||||
|
||||
import { UI_CONFIG } from "./constants"
|
||||
import { UI_CONFIG } from "./constants";
|
||||
|
||||
export interface RouteDetectionConfig {
|
||||
platform: string
|
||||
selectors: string[]
|
||||
reinitCallback: () => void
|
||||
checkInterval?: number
|
||||
observerThrottleDelay?: number
|
||||
platform: string;
|
||||
selectors: string[];
|
||||
reinitCallback: () => void;
|
||||
checkInterval?: number;
|
||||
observerThrottleDelay?: number;
|
||||
}
|
||||
|
||||
export interface RouteDetectionCleanup {
|
||||
observer: MutationObserver | null
|
||||
urlCheckInterval: NodeJS.Timeout | null
|
||||
observerThrottle: NodeJS.Timeout | null
|
||||
observer: MutationObserver | null;
|
||||
urlCheckInterval: NodeJS.Timeout | null;
|
||||
observerThrottle: NodeJS.Timeout | null;
|
||||
}
|
||||
|
||||
export function createRouteDetection(
|
||||
|
|
@ -24,94 +24,97 @@ export function createRouteDetection(
|
|||
cleanup: RouteDetectionCleanup,
|
||||
): void {
|
||||
if (cleanup.observer) {
|
||||
cleanup.observer.disconnect()
|
||||
cleanup.observer.disconnect();
|
||||
}
|
||||
if (cleanup.urlCheckInterval) {
|
||||
clearInterval(cleanup.urlCheckInterval)
|
||||
clearInterval(cleanup.urlCheckInterval);
|
||||
}
|
||||
if (cleanup.observerThrottle) {
|
||||
clearTimeout(cleanup.observerThrottle)
|
||||
cleanup.observerThrottle = null
|
||||
clearTimeout(cleanup.observerThrottle);
|
||||
cleanup.observerThrottle = null;
|
||||
}
|
||||
|
||||
let currentUrl = window.location.href
|
||||
let currentUrl = window.location.href;
|
||||
|
||||
const checkForRouteChange = () => {
|
||||
if (window.location.href !== currentUrl) {
|
||||
currentUrl = window.location.href
|
||||
console.log(`${config.platform} route changed, re-initializing`)
|
||||
setTimeout(config.reinitCallback, 1000)
|
||||
currentUrl = window.location.href;
|
||||
console.log(`${config.platform} route changed, re-initializing`);
|
||||
setTimeout(config.reinitCallback, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cleanup.urlCheckInterval = setInterval(
|
||||
checkForRouteChange,
|
||||
config.checkInterval || UI_CONFIG.ROUTE_CHECK_INTERVAL,
|
||||
)
|
||||
);
|
||||
|
||||
cleanup.observer = new MutationObserver((mutations) => {
|
||||
if (cleanup.observerThrottle) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldRecheck = false
|
||||
let shouldRecheck = false;
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element
|
||||
const element = node as Element;
|
||||
|
||||
for (const selector of config.selectors) {
|
||||
if (
|
||||
element.querySelector?.(selector) ||
|
||||
element.matches?.(selector)
|
||||
) {
|
||||
shouldRecheck = true
|
||||
break
|
||||
shouldRecheck = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (shouldRecheck) {
|
||||
cleanup.observerThrottle = setTimeout(() => {
|
||||
try {
|
||||
cleanup.observerThrottle = null
|
||||
config.reinitCallback()
|
||||
cleanup.observerThrottle = null;
|
||||
config.reinitCallback();
|
||||
} catch (error) {
|
||||
console.error(`Error in ${config.platform} observer callback:`, error)
|
||||
console.error(
|
||||
`Error in ${config.platform} observer callback:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}, config.observerThrottleDelay || UI_CONFIG.OBSERVER_THROTTLE_DELAY)
|
||||
}, config.observerThrottleDelay || UI_CONFIG.OBSERVER_THROTTLE_DELAY);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
try {
|
||||
cleanup.observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to set up ${config.platform} route observer:`, error)
|
||||
console.error(`Failed to set up ${config.platform} route observer:`, error);
|
||||
if (cleanup.urlCheckInterval) {
|
||||
clearInterval(cleanup.urlCheckInterval)
|
||||
clearInterval(cleanup.urlCheckInterval);
|
||||
}
|
||||
cleanup.urlCheckInterval = setInterval(checkForRouteChange, 1000)
|
||||
cleanup.urlCheckInterval = setInterval(checkForRouteChange, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanupRouteDetection(cleanup: RouteDetectionCleanup): void {
|
||||
if (cleanup.observer) {
|
||||
cleanup.observer.disconnect()
|
||||
cleanup.observer = null
|
||||
cleanup.observer.disconnect();
|
||||
cleanup.observer = null;
|
||||
}
|
||||
if (cleanup.urlCheckInterval) {
|
||||
clearInterval(cleanup.urlCheckInterval)
|
||||
cleanup.urlCheckInterval = null
|
||||
clearInterval(cleanup.urlCheckInterval);
|
||||
cleanup.urlCheckInterval = null;
|
||||
}
|
||||
if (cleanup.observerThrottle) {
|
||||
clearTimeout(cleanup.observerThrottle)
|
||||
cleanup.observerThrottle = null
|
||||
clearTimeout(cleanup.observerThrottle);
|
||||
cleanup.observerThrottle = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
* Twitter Authentication Module
|
||||
* Handles token capture and storage for Twitter API access
|
||||
*/
|
||||
import { STORAGE_KEYS } from "./constants"
|
||||
import { STORAGE_KEYS } from "./constants";
|
||||
|
||||
export interface TwitterAuthTokens {
|
||||
cookie: string
|
||||
csrf: string
|
||||
auth: string
|
||||
cookie: string;
|
||||
csrf: string;
|
||||
auth: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,41 +17,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([STORAGE_KEYS.TOKENS_LOGGED], (result) => {
|
||||
if (!result[STORAGE_KEYS.TOKENS_LOGGED]) {
|
||||
console.log("Twitter auth tokens captured successfully")
|
||||
chrome.storage.session.set({ [STORAGE_KEYS.TOKENS_LOGGED]: true })
|
||||
console.log("Twitter auth tokens captured successfully");
|
||||
chrome.storage.session.set({ [STORAGE_KEYS.TOKENS_LOGGED]: true });
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
chrome.storage.session.set({
|
||||
[STORAGE_KEYS.TWITTER_COOKIE]: cookieHeader.value,
|
||||
[STORAGE_KEYS.TWITTER_CSRF]: csrfHeader.value,
|
||||
[STORAGE_KEYS.TWITTER_AUTH_TOKEN]: authHeader.value,
|
||||
})
|
||||
});
|
||||
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -63,21 +63,21 @@ export async function getTwitterTokens(): Promise<TwitterAuthTokens | null> {
|
|||
STORAGE_KEYS.TWITTER_COOKIE,
|
||||
STORAGE_KEYS.TWITTER_CSRF,
|
||||
STORAGE_KEYS.TWITTER_AUTH_TOKEN,
|
||||
])
|
||||
]);
|
||||
|
||||
if (
|
||||
!result[STORAGE_KEYS.TWITTER_COOKIE] ||
|
||||
!result[STORAGE_KEYS.TWITTER_CSRF] ||
|
||||
!result[STORAGE_KEYS.TWITTER_AUTH_TOKEN]
|
||||
) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
cookie: result[STORAGE_KEYS.TWITTER_COOKIE],
|
||||
csrf: result[STORAGE_KEYS.TWITTER_CSRF],
|
||||
auth: result[STORAGE_KEYS.TWITTER_AUTH_TOKEN],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -86,16 +86,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,45 +3,45 @@
|
|||
* Handles the import process for Twitter bookmarks
|
||||
*/
|
||||
|
||||
import { saveAllTweets } from "./api"
|
||||
import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth"
|
||||
import { saveAllTweets } from "./api";
|
||||
import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth";
|
||||
import {
|
||||
BOOKMARKS_URL,
|
||||
buildRequestVariables,
|
||||
extractNextCursor,
|
||||
getAllTweets,
|
||||
type TwitterAPIResponse,
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -49,8 +49,8 @@ class RateLimiter {
|
|||
* 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) {}
|
||||
|
||||
|
|
@ -60,19 +60,19 @@ 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
|
||||
const uniqueGroupId = crypto.randomUUID()
|
||||
this.importInProgress = true;
|
||||
const uniqueGroupId = crypto.randomUUID();
|
||||
|
||||
try {
|
||||
await this.batchImportAll("", 0, uniqueGroupId)
|
||||
this.rateLimiter.reset()
|
||||
await this.batchImportAll("", 0, uniqueGroupId);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -81,52 +81,56 @@ export class TwitterImporter {
|
|||
* @param cursor - Pagination cursor for Twitter API
|
||||
* @param totalImported - Number of tweets imported so far
|
||||
*/
|
||||
private async batchImportAll(cursor = "", totalImported = 0, uniqueGroupId = "twitter_bookmarks"): Promise<void> {
|
||||
private async batchImportAll(
|
||||
cursor = "",
|
||||
totalImported = 0,
|
||||
uniqueGroupId = "twitter_bookmarks",
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Use a local variable to track imported count
|
||||
let importedCount = totalImported
|
||||
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;
|
||||
|
||||
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, uniqueGroupId)
|
||||
await this.rateLimiter.handleRateLimit(this.config.onProgress);
|
||||
return this.batchImportAll(cursor, totalImported, uniqueGroupId);
|
||||
}
|
||||
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);
|
||||
|
||||
const documents: MemoryPayload[] = []
|
||||
const documents: MemoryPayload[] = [];
|
||||
|
||||
// Convert tweets to MemoryPayload
|
||||
for (const tweet of tweets) {
|
||||
|
|
@ -139,49 +143,51 @@ export class TwitterImporter {
|
|||
likes: tweet.favorite_count,
|
||||
retweets: tweet.retweet_count || 0,
|
||||
sm_internal_group_id: uniqueGroupId,
|
||||
}
|
||||
};
|
||||
documents.push({
|
||||
containerTags: ["sm_project_twitter_bookmarks"],
|
||||
content: `https://x.com/${tweet.user.screen_name}/status/${tweet.id_str}`,
|
||||
metadata,
|
||||
customId: tweet.id_str,
|
||||
})
|
||||
importedCount++
|
||||
await this.config.onProgress(`Imported ${importedCount} tweets, so far...`)
|
||||
});
|
||||
importedCount++;
|
||||
await this.config.onProgress(
|
||||
`Imported ${importedCount} tweets, so far...`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error importing tweet:", error)
|
||||
console.error("Error importing tweet:", error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (documents.length > 0) {
|
||||
await saveAllTweets(documents)
|
||||
await saveAllTweets(documents);
|
||||
}
|
||||
console.log("Tweets saved")
|
||||
console.log("Documents:", documents)
|
||||
console.log("Tweets saved");
|
||||
console.log("Documents:", documents);
|
||||
} catch (error) {
|
||||
console.error("Error saving tweets batch:", error)
|
||||
await this.config.onError(error as Error)
|
||||
return
|
||||
console.error("Error saving tweets batch:", error);
|
||||
await this.config.onError(error as Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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, importedCount, uniqueGroupId)
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)); // Rate limiting
|
||||
await this.batchImportAll(nextCursor, importedCount, uniqueGroupId);
|
||||
} else {
|
||||
await this.config.onComplete(importedCount)
|
||||
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,69 +295,69 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -367,11 +367,11 @@ export function buildRequestVariables(cursor?: string, count = 100) {
|
|||
const variables = {
|
||||
count,
|
||||
includePromotedContent: false,
|
||||
}
|
||||
};
|
||||
|
||||
if (cursor) {
|
||||
;(variables as Record<string, unknown>).cursor = cursor
|
||||
(variables as Record<string, unknown>).cursor = cursor;
|
||||
}
|
||||
|
||||
return variables
|
||||
return variables;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,102 +5,102 @@
|
|||
/**
|
||||
* 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
|
||||
actionSource?: string
|
||||
action?: string;
|
||||
type?: string;
|
||||
data?: unknown;
|
||||
state?: ToastState;
|
||||
importedMessage?: string;
|
||||
totalImported?: number;
|
||||
actionSource?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Memory data structure for saving content
|
||||
*/
|
||||
export interface MemoryData {
|
||||
html?: string
|
||||
content?: string
|
||||
highlightedText?: string
|
||||
url?: string
|
||||
html?: string;
|
||||
content?: 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
|
||||
}
|
||||
customId?: string
|
||||
sm_source: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
customId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -112,41 +112,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 = "Authentication required") {
|
||||
super(message, "AUTH_ERROR")
|
||||
this.name = "AuthenticationError"
|
||||
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 { ELEMENT_IDS, UI_CONFIG } from "./constants"
|
||||
import type { ToastState } from "./types"
|
||||
import { 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;
|
||||
|
|
@ -33,12 +33,12 @@ export function createToast(state: ToastState): HTMLElement {
|
|||
max-width: 300px;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18), 0 1.5px 6px 0 rgba(0,0,0,0.12);
|
||||
`
|
||||
`;
|
||||
|
||||
// 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';
|
||||
|
|
@ -87,15 +87,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) {
|
||||
|
|
@ -111,16 +111,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":
|
||||
|
|
@ -130,15 +130,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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -147,8 +147,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;
|
||||
|
|
@ -164,27 +164,27 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement {
|
|||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
`
|
||||
`;
|
||||
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png")
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png");
|
||||
button.innerHTML = `
|
||||
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
|
||||
<span style="font-weight: 500; font-size: 12px;">Import Bookmarks</span>
|
||||
`
|
||||
`;
|
||||
|
||||
button.addEventListener("mouseenter", () => {
|
||||
button.style.opacity = "0.8"
|
||||
button.style.boxShadow = "0 4px 12px rgba(29, 155, 240, 0.4)"
|
||||
})
|
||||
button.style.opacity = "0.8";
|
||||
button.style.boxShadow = "0 4px 12px rgba(29, 155, 240, 0.4)";
|
||||
});
|
||||
|
||||
button.addEventListener("mouseleave", () => {
|
||||
button.style.opacity = "1"
|
||||
button.style.boxShadow = "0 2px 8px rgba(29, 155, 240, 0.3)"
|
||||
})
|
||||
button.style.opacity = "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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -193,7 +193,7 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement {
|
|||
* @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;
|
||||
|
|
@ -206,29 +206,29 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement {
|
|||
margin-right: 10px;
|
||||
margin-bottom: 2px;
|
||||
z-index: 1000;
|
||||
`
|
||||
`;
|
||||
|
||||
const iconFileName = "/icon-16.png"
|
||||
const iconUrl = browser.runtime.getURL(iconFileName)
|
||||
const iconFileName = "/icon-16.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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -237,7 +237,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;
|
||||
|
|
@ -247,30 +247,30 @@ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement {
|
|||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
border-radius: 50%;
|
||||
`
|
||||
`;
|
||||
|
||||
// Use appropriate icon based on theme
|
||||
const iconFileName = "/icon-16.png"
|
||||
const iconUrl = browser.runtime.getURL(iconFileName)
|
||||
const iconFileName = "/icon-16.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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -279,7 +279,7 @@ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement {
|
|||
* @returns HTMLElement - The save button element
|
||||
*/
|
||||
export function createClaudeInputBarElement(onClick: () => void): HTMLElement {
|
||||
const iconButton = document.createElement("div")
|
||||
const iconButton = document.createElement("div");
|
||||
iconButton.style.cssText = `
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -290,31 +290,31 @@ export function createClaudeInputBarElement(onClick: () => void): HTMLElement {
|
|||
transition: all 0.2s ease;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
`
|
||||
`;
|
||||
|
||||
const iconFileName = "/icon-16.png"
|
||||
const iconUrl = browser.runtime.getURL(iconFileName)
|
||||
const iconFileName = "/icon-16.png";
|
||||
const iconUrl = browser.runtime.getURL(iconFileName);
|
||||
iconButton.innerHTML = `
|
||||
<img src="${iconUrl}" width="20" height="20" alt="Get Related Memories from supermemory" style="border-radius: 4px;" />
|
||||
`
|
||||
`;
|
||||
|
||||
iconButton.addEventListener("mouseenter", () => {
|
||||
iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)"
|
||||
iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)"
|
||||
})
|
||||
iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
|
||||
iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)";
|
||||
});
|
||||
|
||||
iconButton.addEventListener("mouseleave", () => {
|
||||
iconButton.style.backgroundColor = "transparent"
|
||||
iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)"
|
||||
})
|
||||
iconButton.style.backgroundColor = "transparent";
|
||||
iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)";
|
||||
});
|
||||
|
||||
iconButton.addEventListener("click", (event) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
onClick()
|
||||
})
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
onClick();
|
||||
});
|
||||
|
||||
return iconButton
|
||||
return iconButton;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -323,7 +323,7 @@ export function createClaudeInputBarElement(onClick: () => void): HTMLElement {
|
|||
* @returns HTMLElement - The save button element
|
||||
*/
|
||||
export function createT3InputBarElement(onClick: () => void): HTMLElement {
|
||||
const iconButton = document.createElement("div")
|
||||
const iconButton = document.createElement("div");
|
||||
iconButton.style.cssText = `
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -334,31 +334,31 @@ export function createT3InputBarElement(onClick: () => void): HTMLElement {
|
|||
transition: all 0.2s ease;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
`
|
||||
`;
|
||||
|
||||
const iconFileName = "/icon-16.png"
|
||||
const iconUrl = browser.runtime.getURL(iconFileName)
|
||||
const iconFileName = "/icon-16.png";
|
||||
const iconUrl = browser.runtime.getURL(iconFileName);
|
||||
iconButton.innerHTML = `
|
||||
<img src="${iconUrl}" width="20" height="20" alt="Get Related Memories from supermemory" style="border-radius: 4px;" />
|
||||
`
|
||||
`;
|
||||
|
||||
iconButton.addEventListener("mouseenter", () => {
|
||||
iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)"
|
||||
iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)"
|
||||
})
|
||||
iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
|
||||
iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)";
|
||||
});
|
||||
|
||||
iconButton.addEventListener("mouseleave", () => {
|
||||
iconButton.style.backgroundColor = "transparent"
|
||||
iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)"
|
||||
})
|
||||
iconButton.style.backgroundColor = "transparent";
|
||||
iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)";
|
||||
});
|
||||
|
||||
iconButton.addEventListener("click", (event) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
onClick()
|
||||
})
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
onClick();
|
||||
});
|
||||
|
||||
return iconButton
|
||||
return iconButton;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -371,7 +371,7 @@ export const DOMUtils = {
|
|||
* @returns boolean
|
||||
*/
|
||||
isOnDomain(domains: readonly string[]): boolean {
|
||||
return domains.includes(window.location.hostname)
|
||||
return domains.includes(window.location.hostname);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -379,9 +379,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;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -390,7 +390,7 @@ export const DOMUtils = {
|
|||
* @returns boolean
|
||||
*/
|
||||
elementExists(id: string): boolean {
|
||||
return !!document.getElementById(id)
|
||||
return !!document.getElementById(id);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -398,8 +398,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();
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -412,18 +412,20 @@ export const DOMUtils = {
|
|||
state: ToastState,
|
||||
duration: number = UI_CONFIG.TOAST_DURATION,
|
||||
): HTMLElement {
|
||||
const existingToast = document.getElementById(ELEMENT_IDS.SUPERMEMORY_TOAST)
|
||||
const existingToast = document.getElementById(
|
||||
ELEMENT_IDS.SUPERMEMORY_TOAST,
|
||||
);
|
||||
|
||||
if ((state === "success" || state === "error") && existingToast) {
|
||||
const icon = existingToast.querySelector("div")
|
||||
const text = existingToast.querySelector("span")
|
||||
const icon = existingToast.querySelector("div");
|
||||
const text = existingToast.querySelector("span");
|
||||
|
||||
if (icon && text) {
|
||||
if (state === "success") {
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png")
|
||||
icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`
|
||||
icon.style.animation = ""
|
||||
text.textContent = "Added to Memory"
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png");
|
||||
icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`;
|
||||
icon.style.animation = "";
|
||||
text.textContent = "Added to Memory";
|
||||
} else if (state === "error") {
|
||||
icon.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
|
@ -431,52 +433,52 @@ export const DOMUtils = {
|
|||
<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>
|
||||
`
|
||||
icon.style.animation = ""
|
||||
`;
|
||||
icon.style.animation = "";
|
||||
text.textContent =
|
||||
"Failed to save memory / Make sure you are logged in"
|
||||
"Failed to save memory / Make sure you are logged in";
|
||||
}
|
||||
|
||||
// Auto-dismiss
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(existingToast)) {
|
||||
existingToast.style.animation = "fadeOut 0.3s ease-out"
|
||||
existingToast.style.animation = "fadeOut 0.3s ease-out";
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(existingToast)) {
|
||||
existingToast.remove()
|
||||
existingToast.remove();
|
||||
}
|
||||
}, 300)
|
||||
}, 300);
|
||||
}
|
||||
}, duration)
|
||||
}, duration);
|
||||
|
||||
return existingToast
|
||||
return existingToast;
|
||||
}
|
||||
}
|
||||
|
||||
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,5 +1,5 @@
|
|||
import tailwindcss from "@tailwindcss/vite"
|
||||
import { defineConfig, type WxtViteConfig } from "wxt"
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { defineConfig, type WxtViteConfig } from "wxt";
|
||||
|
||||
// See https://wxt.dev/api/config.html
|
||||
export default defineConfig({
|
||||
|
|
@ -32,4 +32,4 @@ export default defineConfig({
|
|||
webExt: {
|
||||
chromiumArgs: ["--user-data-dir=./.wxt/chrome-data"],
|
||||
},
|
||||
})
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue