changes to chrome extension (#402)
### TL;DR Added proper semicolons throughout the codebase, improved Twitter import functionality, enhanced memory search results display, and added various icon sizes for the browser extension. ### What changed? - Added semicolons at the end of statements throughout the browser extension codebase for consistent code style - Fixed the Twitter import button to only appear on the bookmarks page - Updated the saveMemoriesToSupermemory function to use the correct action type - Enhanced the memory search functionality to include related memories in search results - Added an expandable UI component in the chat interface to display memory search results with details - Updated the welcome page text to better describe supermemory's capabilities - Added multiple icon sizes (48x48 to 512x512) for better browser extension display across platforms - Updated the sign-in button text from "login in" to "Sign in or create account" - Added turbo flag to the Next.js dev script for faster development ### How to test? 1. Test the Twitter import functionality by visiting the Twitter bookmarks page 2. Verify that the memory search functionality works correctly in chat interfaces 3. Check that the expandable memory results UI displays properly with memory details 4. Confirm that the browser extension icons display correctly at different sizes 5. Verify the sign-in flow with the updated button text ### Why make this change? This change improves code consistency by standardizing semicolon usage throughout the codebase. It enhances the user experience by making the Twitter import feature more targeted and fixing the memory saving functionality. The expandable memory results UI provides users with more detailed information about their memories, making the search functionality more useful. The additional icon sizes ensure the extension displays properly across different platforms and contexts. Overall, these changes improve both the developer experience through consistent code style and the user experience through enhanced functionality and clearer UI elements.
|
|
@ -1,47 +1,47 @@
|
|||
import { getDefaultProject, saveMemory, searchMemories } from "../utils/api"
|
||||
import { getDefaultProject, saveMemory, searchMemories } from "../utils/api";
|
||||
import {
|
||||
CONTAINER_TAGS,
|
||||
CONTEXT_MENU_IDS,
|
||||
MESSAGE_TYPES,
|
||||
} from "../utils/constants"
|
||||
import { captureTwitterTokens } from "../utils/twitter-auth"
|
||||
} from "../utils/constants";
|
||||
import { captureTwitterTokens } from "../utils/twitter-auth";
|
||||
import {
|
||||
type TwitterImportConfig,
|
||||
TwitterImporter,
|
||||
} from "../utils/twitter-import"
|
||||
} from "../utils/twitter-import";
|
||||
import type {
|
||||
ExtensionMessage,
|
||||
MemoryData,
|
||||
MemoryPayload,
|
||||
} from "../utils/types"
|
||||
} from "../utils/types";
|
||||
|
||||
export default defineBackground(() => {
|
||||
let twitterImporter: TwitterImporter | null = null
|
||||
let twitterImporter: TwitterImporter | null = null;
|
||||
|
||||
browser.runtime.onInstalled.addListener((details) => {
|
||||
browser.contextMenus.create({
|
||||
id: CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY,
|
||||
title: "Save to supermemory",
|
||||
contexts: ["selection", "page", "link"],
|
||||
})
|
||||
});
|
||||
|
||||
// Open welcome tab on first install
|
||||
if (details.reason === "install") {
|
||||
browser.tabs.create({
|
||||
url: browser.runtime.getURL("/welcome.html"),
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Intercept Twitter requests to capture authentication headers.
|
||||
browser.webRequest.onBeforeSendHeaders.addListener(
|
||||
(details) => {
|
||||
captureTwitterTokens(details)
|
||||
return {}
|
||||
captureTwitterTokens(details);
|
||||
return {};
|
||||
},
|
||||
{ urls: ["*://x.com/*", "*://twitter.com/*"] },
|
||||
["requestHeaders", "extraHeaders"],
|
||||
)
|
||||
);
|
||||
|
||||
// Handle context menu clicks.
|
||||
browser.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||
|
|
@ -50,27 +50,27 @@ export default defineBackground(() => {
|
|||
try {
|
||||
await browser.tabs.sendMessage(tab.id, {
|
||||
action: MESSAGE_TYPES.SAVE_MEMORY,
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to send message to content script:", error)
|
||||
console.error("Failed to send message to content script:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Send message to current active tab.
|
||||
const sendMessageToCurrentTab = async (message: string) => {
|
||||
const tabs = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
})
|
||||
});
|
||||
if (tabs.length > 0 && tabs[0].id) {
|
||||
await browser.tabs.sendMessage(tabs[0].id, {
|
||||
type: MESSAGE_TYPES.IMPORT_UPDATE,
|
||||
importedMessage: message,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Send import completion message
|
||||
|
|
@ -79,14 +79,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
|
||||
|
|
@ -95,31 +95,31 @@ export default defineBackground(() => {
|
|||
data: MemoryData,
|
||||
): Promise<{ success: boolean; data?: unknown; error?: string }> => {
|
||||
try {
|
||||
let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT
|
||||
let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT;
|
||||
try {
|
||||
const defaultProject = await getDefaultProject()
|
||||
const defaultProject = await getDefaultProject();
|
||||
if (defaultProject?.containerTag) {
|
||||
containerTag = defaultProject.containerTag
|
||||
containerTag = defaultProject.containerTag;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to get default project, using fallback:", error)
|
||||
console.warn("Failed to get default project, using fallback:", error);
|
||||
}
|
||||
|
||||
const payload: MemoryPayload = {
|
||||
containerTags: [containerTag],
|
||||
content: `${data.highlightedText}\n\n${data.html}\n\n${data?.url}`,
|
||||
metadata: { sm_source: "consumer" },
|
||||
}
|
||||
};
|
||||
|
||||
const responseData = await saveMemory(payload)
|
||||
return { success: true, data: responseData }
|
||||
const responseData = await saveMemory(payload);
|
||||
return { success: true, data: responseData };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getRelatedMemories = async (
|
||||
data: string,
|
||||
|
|
@ -141,7 +141,7 @@ export default defineBackground(() => {
|
|||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle extension messages
|
||||
|
|
@ -154,48 +154,48 @@ export default defineBackground(() => {
|
|||
onProgress: sendMessageToCurrentTab,
|
||||
onComplete: sendImportDoneMessage,
|
||||
onError: async (error: Error) => {
|
||||
await sendMessageToCurrentTab(`Error: ${error.message}`)
|
||||
await sendMessageToCurrentTab(`Error: ${error.message}`);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
twitterImporter = new TwitterImporter(importConfig)
|
||||
twitterImporter.startImport().catch(console.error)
|
||||
sendResponse({ success: true })
|
||||
return true
|
||||
twitterImporter = new TwitterImporter(importConfig);
|
||||
twitterImporter.startImport().catch(console.error);
|
||||
sendResponse({ success: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle regular memory save request
|
||||
if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
|
||||
;(async () => {
|
||||
(async () => {
|
||||
try {
|
||||
const result = await saveMemoryToSupermemory(
|
||||
message.data as MemoryData,
|
||||
)
|
||||
sendResponse(result)
|
||||
);
|
||||
sendResponse(result);
|
||||
} catch (error) {
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
}
|
||||
})()
|
||||
return true
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.action === MESSAGE_TYPES.GET_RELATED_MEMORIES) {
|
||||
;(async () => {
|
||||
(async () => {
|
||||
try {
|
||||
const result = await getRelatedMemories(message.data as string)
|
||||
sendResponse(result)
|
||||
const result = await getRelatedMemories(message.data as string);
|
||||
sendResponse(result);
|
||||
} catch (error) {
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
}
|
||||
})()
|
||||
return true
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,93 +3,93 @@ import {
|
|||
ELEMENT_IDS,
|
||||
MESSAGE_TYPES,
|
||||
STORAGE_KEYS,
|
||||
} from "../utils/constants"
|
||||
} from "../utils/constants";
|
||||
import {
|
||||
createChatGPTInputBarElement,
|
||||
createClaudeInputBarElement,
|
||||
createT3InputBarElement,
|
||||
createSaveTweetElement,
|
||||
createT3InputBarElement,
|
||||
createTwitterImportButton,
|
||||
createTwitterImportUI,
|
||||
DOMUtils,
|
||||
} from "../utils/ui-components"
|
||||
} from "../utils/ui-components";
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["<all_urls>"],
|
||||
main() {
|
||||
let twitterImportUI: HTMLElement | null = null
|
||||
let isTwitterImportOpen = false
|
||||
let twitterImportUI: HTMLElement | null = null;
|
||||
let isTwitterImportOpen = false;
|
||||
|
||||
browser.runtime.onMessage.addListener(async (message) => {
|
||||
if (message.action === MESSAGE_TYPES.SHOW_TOAST) {
|
||||
DOMUtils.showToast(message.state)
|
||||
DOMUtils.showToast(message.state);
|
||||
} else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
|
||||
await saveMemory()
|
||||
await saveMemory();
|
||||
} else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
|
||||
updateTwitterImportUI(message)
|
||||
updateTwitterImportUI(message);
|
||||
} else if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
|
||||
updateTwitterImportUI(message)
|
||||
updateTwitterImportUI(message);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const observeForMemoriesDialog = () => {
|
||||
const observer = new MutationObserver(() => {
|
||||
if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
|
||||
addSupermemoryButtonToMemoriesDialog()
|
||||
addSaveChatGPTElementBeforeComposerBtn()
|
||||
addSupermemoryButtonToMemoriesDialog();
|
||||
addSaveChatGPTElementBeforeComposerBtn();
|
||||
}
|
||||
if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
|
||||
addSupermemoryIconToClaudeInput()
|
||||
addSupermemoryIconToClaudeInput();
|
||||
}
|
||||
if (DOMUtils.isOnDomain(DOMAINS.T3)) {
|
||||
addSupermemoryIconToT3Input()
|
||||
addSupermemoryIconToT3Input();
|
||||
}
|
||||
if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
|
||||
addTwitterImportButton()
|
||||
addTwitterImportButton();
|
||||
//addSaveTweetElement();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
|
||||
setTimeout(() => {
|
||||
addTwitterImportButton() // Wait 2 seconds for page to load
|
||||
addTwitterImportButton(); // Wait 2 seconds for page to load
|
||||
//addSaveTweetElement();
|
||||
}, 2000)
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
|
||||
setTimeout(() => {
|
||||
addSupermemoryIconToClaudeInput() // Wait 2 seconds for Claude page to load
|
||||
}, 2000)
|
||||
addSupermemoryIconToClaudeInput(); // Wait 2 seconds for Claude page to load
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
if (DOMUtils.isOnDomain(DOMAINS.T3)) {
|
||||
setTimeout(() => {
|
||||
addSupermemoryIconToT3Input() // Wait 2 seconds for T3 page to load
|
||||
}, 2000)
|
||||
addSupermemoryIconToT3Input(); // Wait 2 seconds for T3 page to load
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", observeForMemoriesDialog)
|
||||
document.addEventListener("DOMContentLoaded", observeForMemoriesDialog);
|
||||
} else {
|
||||
observeForMemoriesDialog()
|
||||
observeForMemoriesDialog();
|
||||
}
|
||||
|
||||
async function saveMemory() {
|
||||
try {
|
||||
DOMUtils.showToast("loading")
|
||||
DOMUtils.showToast("loading");
|
||||
|
||||
const highlightedText = window.getSelection()?.toString() || ""
|
||||
const highlightedText = window.getSelection()?.toString() || "";
|
||||
|
||||
const url = window.location.href
|
||||
const url = window.location.href;
|
||||
|
||||
const html = document.documentElement.outerHTML
|
||||
const html = document.documentElement.outerHTML;
|
||||
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.SAVE_MEMORY,
|
||||
|
|
@ -98,75 +98,75 @@ export default defineContentScript({
|
|||
highlightedText,
|
||||
url,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
console.log("Response from enxtension:", response)
|
||||
console.log("Response from enxtension:", response);
|
||||
if (response.success) {
|
||||
DOMUtils.showToast("success")
|
||||
DOMUtils.showToast("success");
|
||||
} else {
|
||||
DOMUtils.showToast("error")
|
||||
DOMUtils.showToast("error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving memory:", error)
|
||||
DOMUtils.showToast("error")
|
||||
console.error("Error saving memory:", error);
|
||||
DOMUtils.showToast("error");
|
||||
}
|
||||
}
|
||||
|
||||
async function getRelatedMemories() {
|
||||
try {
|
||||
const userQuery =
|
||||
document.getElementById("prompt-textarea")?.textContent || ""
|
||||
document.getElementById("prompt-textarea")?.textContent || "";
|
||||
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
|
||||
data: userQuery,
|
||||
})
|
||||
});
|
||||
|
||||
if (response.success && response.data) {
|
||||
const promptElement = document.getElementById("prompt-textarea")
|
||||
const promptElement = document.getElementById("prompt-textarea");
|
||||
if (promptElement) {
|
||||
const currentContent = promptElement.innerHTML
|
||||
promptElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`
|
||||
const currentContent = promptElement.innerHTML;
|
||||
promptElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting related memories:", error)
|
||||
console.error("Error getting related memories:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function addSupermemoryButtonToMemoriesDialog() {
|
||||
const dialogs = document.querySelectorAll('[role="dialog"]')
|
||||
let memoriesDialog: HTMLElement | null = null
|
||||
const dialogs = document.querySelectorAll('[role="dialog"]');
|
||||
let memoriesDialog: HTMLElement | null = null;
|
||||
|
||||
for (const dialog of dialogs) {
|
||||
const headerText = dialog.querySelector("h2")
|
||||
const headerText = dialog.querySelector("h2");
|
||||
if (headerText?.textContent?.includes("Saved memories")) {
|
||||
memoriesDialog = dialog as HTMLElement
|
||||
break
|
||||
memoriesDialog = dialog as HTMLElement;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!memoriesDialog) return
|
||||
if (!memoriesDialog) return;
|
||||
|
||||
if (memoriesDialog.querySelector("#supermemory-save-button")) return
|
||||
if (memoriesDialog.querySelector("#supermemory-save-button")) return;
|
||||
|
||||
const deleteAllContainer = memoriesDialog.querySelector(
|
||||
".mt-5.flex.justify-end",
|
||||
)
|
||||
if (!deleteAllContainer) return
|
||||
);
|
||||
if (!deleteAllContainer) return;
|
||||
|
||||
const supermemoryButton = document.createElement("button")
|
||||
supermemoryButton.id = "supermemory-save-button"
|
||||
supermemoryButton.className = "btn relative btn-primary-outline mr-2"
|
||||
const supermemoryButton = document.createElement("button");
|
||||
supermemoryButton.id = "supermemory-save-button";
|
||||
supermemoryButton.className = "btn relative btn-primary-outline mr-2";
|
||||
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png")
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png");
|
||||
|
||||
supermemoryButton.innerHTML = `
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<img src="${iconUrl}" alt="supermemory" style="width: 16px; height: 16px; flex-shrink: 0; border-radius: 2px;" />
|
||||
Save to supermemory
|
||||
</div>
|
||||
`
|
||||
`;
|
||||
|
||||
supermemoryButton.style.cssText = `
|
||||
background: #1C2026 !important;
|
||||
|
|
@ -178,101 +178,108 @@ export default defineContentScript({
|
|||
font-size: 14px !important;
|
||||
margin-right: 8px !important;
|
||||
cursor: pointer !important;
|
||||
`
|
||||
`;
|
||||
|
||||
supermemoryButton.addEventListener("mouseenter", () => {
|
||||
supermemoryButton.style.backgroundColor = "#2B2E33"
|
||||
})
|
||||
supermemoryButton.style.backgroundColor = "#2B2E33";
|
||||
});
|
||||
|
||||
supermemoryButton.addEventListener("mouseleave", () => {
|
||||
supermemoryButton.style.backgroundColor = "#1C2026"
|
||||
})
|
||||
supermemoryButton.style.backgroundColor = "#1C2026";
|
||||
});
|
||||
|
||||
supermemoryButton.addEventListener("click", async () => {
|
||||
await saveMemoriesToSupermemory()
|
||||
})
|
||||
await saveMemoriesToSupermemory();
|
||||
});
|
||||
|
||||
deleteAllContainer.insertBefore(
|
||||
supermemoryButton,
|
||||
deleteAllContainer.firstChild,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function saveMemoriesToSupermemory() {
|
||||
try {
|
||||
DOMUtils.showToast("loading")
|
||||
DOMUtils.showToast("loading");
|
||||
|
||||
const memoriesTable = document.querySelector(
|
||||
'[role="dialog"] table tbody',
|
||||
)
|
||||
);
|
||||
if (!memoriesTable) {
|
||||
DOMUtils.showToast("error")
|
||||
return
|
||||
DOMUtils.showToast("error");
|
||||
return;
|
||||
}
|
||||
|
||||
const memoryRows = memoriesTable.querySelectorAll("tr")
|
||||
const memories: string[] = []
|
||||
const memoryRows = memoriesTable.querySelectorAll("tr");
|
||||
const memories: string[] = [];
|
||||
|
||||
memoryRows.forEach((row) => {
|
||||
const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap")
|
||||
const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap");
|
||||
if (memoryCell?.textContent) {
|
||||
memories.push(memoryCell.textContent.trim())
|
||||
memories.push(memoryCell.textContent.trim());
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
console.log("Memories:", memories)
|
||||
console.log("Memories:", memories);
|
||||
|
||||
if (memories.length === 0) {
|
||||
DOMUtils.showToast("error")
|
||||
return
|
||||
DOMUtils.showToast("error");
|
||||
return;
|
||||
}
|
||||
|
||||
const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}`
|
||||
const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}`;
|
||||
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: "saveMemory",
|
||||
action: MESSAGE_TYPES.SAVE_MEMORY,
|
||||
data: {
|
||||
html: combinedContent,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
function addTwitterImportButton() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// Only show the import button on the bookmarks page
|
||||
if (window.location.pathname !== "/i/bookmarks") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const button = createTwitterImportButton(() => {
|
||||
showTwitterImportUI()
|
||||
})
|
||||
showTwitterImportUI();
|
||||
});
|
||||
|
||||
document.body.appendChild(button)
|
||||
document.body.appendChild(button);
|
||||
}
|
||||
|
||||
function showTwitterImportUI() {
|
||||
if (twitterImportUI) {
|
||||
twitterImportUI.remove()
|
||||
twitterImportUI.remove();
|
||||
}
|
||||
|
||||
isTwitterImportOpen = true
|
||||
isTwitterImportOpen = true;
|
||||
|
||||
// Check if user is authenticated
|
||||
browser.storage.local.get([STORAGE_KEYS.BEARER_TOKEN], (result) => {
|
||||
const isAuthenticated = !!result[STORAGE_KEYS.BEARER_TOKEN]
|
||||
const isAuthenticated = !!result[STORAGE_KEYS.BEARER_TOKEN];
|
||||
|
||||
twitterImportUI = createTwitterImportUI(
|
||||
hideTwitterImportUI,
|
||||
|
|
@ -280,35 +287,39 @@ export default defineContentScript({
|
|||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error starting import:", error)
|
||||
console.error("Error starting import:", error);
|
||||
}
|
||||
},
|
||||
isAuthenticated,
|
||||
)
|
||||
);
|
||||
|
||||
document.body.appendChild(twitterImportUI)
|
||||
})
|
||||
document.body.appendChild(twitterImportUI);
|
||||
});
|
||||
}
|
||||
|
||||
function hideTwitterImportUI() {
|
||||
if (twitterImportUI) {
|
||||
twitterImportUI.remove()
|
||||
twitterImportUI = null
|
||||
twitterImportUI.remove();
|
||||
twitterImportUI = null;
|
||||
}
|
||||
isTwitterImportOpen = false
|
||||
isTwitterImportOpen = false;
|
||||
}
|
||||
|
||||
function updateTwitterImportUI(message: {
|
||||
type: string
|
||||
importedMessage?: string
|
||||
totalImported?: number
|
||||
type: string;
|
||||
importedMessage?: string;
|
||||
totalImported?: number;
|
||||
}) {
|
||||
if (!isTwitterImportOpen || !twitterImportUI) return
|
||||
if (!isTwitterImportOpen || !twitterImportUI) return;
|
||||
|
||||
const statusDiv = twitterImportUI.querySelector(`#${ELEMENT_IDS.TWITTER_IMPORT_STATUS}`)
|
||||
const button = twitterImportUI.querySelector(`#${ELEMENT_IDS.TWITTER_IMPORT_BTN}`)
|
||||
const statusDiv = twitterImportUI.querySelector(
|
||||
`#${ELEMENT_IDS.TWITTER_IMPORT_STATUS}`,
|
||||
);
|
||||
const button = twitterImportUI.querySelector(
|
||||
`#${ELEMENT_IDS.TWITTER_IMPORT_BTN}`,
|
||||
);
|
||||
|
||||
if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
|
||||
if (statusDiv) {
|
||||
|
|
@ -317,11 +328,11 @@ export default defineContentScript({
|
|||
<div style="width: 12px; height: 12px; border: 2px solid #f59e0b; border-top: 2px solid transparent; border-radius: 50%; animation: spin 1s linear infinite;"></div>
|
||||
<span>${message.importedMessage}</span>
|
||||
</div>
|
||||
`
|
||||
`;
|
||||
}
|
||||
if (button) {
|
||||
;(button as HTMLButtonElement).disabled = true
|
||||
;(button as HTMLButtonElement).textContent = "Importing..."
|
||||
(button as HTMLButtonElement).disabled = true;
|
||||
(button as HTMLButtonElement).textContent = "Importing...";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -332,300 +343,347 @@ export default defineContentScript({
|
|||
<span style="color: #059669;">✓</span>
|
||||
<span>Successfully imported ${message.totalImported} tweets!</span>
|
||||
</div>
|
||||
`
|
||||
`;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
hideTwitterImportUI()
|
||||
}, 3000)
|
||||
hideTwitterImportUI();
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function addSaveChatGPTElementBeforeComposerBtn() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const composerButtons = document.querySelectorAll("button.composer-btn")
|
||||
const composerButtons = document.querySelectorAll("button.composer-btn");
|
||||
|
||||
composerButtons.forEach((button) => {
|
||||
if (button.hasAttribute("data-supermemory-icon-added-before")) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = button.parentElement
|
||||
if (!parent) return
|
||||
const parent = button.parentElement;
|
||||
if (!parent) return;
|
||||
|
||||
const parentSiblings = parent.parentElement?.children
|
||||
if (!parentSiblings) return
|
||||
const parentSiblings = parent.parentElement?.children;
|
||||
if (!parentSiblings) return;
|
||||
|
||||
let hasSpeechButtonSibling = false
|
||||
let hasSpeechButtonSibling = false;
|
||||
for (const sibling of parentSiblings) {
|
||||
if (
|
||||
sibling.getAttribute("data-testid") ===
|
||||
"composer-speech-button-container"
|
||||
) {
|
||||
hasSpeechButtonSibling = true
|
||||
break
|
||||
hasSpeechButtonSibling = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasSpeechButtonSibling) return
|
||||
if (!hasSpeechButtonSibling) return;
|
||||
|
||||
const grandParent = parent.parentElement
|
||||
if (!grandParent) return
|
||||
const grandParent = parent.parentElement;
|
||||
if (!grandParent) return;
|
||||
|
||||
const existingIcon = grandParent.querySelector(
|
||||
`#${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer`,
|
||||
)
|
||||
);
|
||||
if (existingIcon) {
|
||||
button.setAttribute("data-supermemory-icon-added-before", "true")
|
||||
return
|
||||
button.setAttribute("data-supermemory-icon-added-before", "true");
|
||||
return;
|
||||
}
|
||||
|
||||
const saveChatGPTElement = createChatGPTInputBarElement(async () => {
|
||||
await getRelatedMemories()
|
||||
})
|
||||
await getRelatedMemories();
|
||||
});
|
||||
|
||||
saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
|
||||
saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
|
||||
button.setAttribute("data-supermemory-icon-added-before", "true")
|
||||
button.setAttribute("data-supermemory-icon-added-before", "true");
|
||||
|
||||
grandParent.insertBefore(saveChatGPTElement, parent)
|
||||
})
|
||||
grandParent.insertBefore(saveChatGPTElement, parent);
|
||||
});
|
||||
}
|
||||
|
||||
function addSupermemoryIconToClaudeInput() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
await getRelatedMemoriesForClaude();
|
||||
});
|
||||
|
||||
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() {
|
||||
try {
|
||||
let userQuery = ""
|
||||
let userQuery = "";
|
||||
|
||||
const supermemoryContainer = document.querySelector('[data-supermemory-icon-added="true"]')
|
||||
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 || ""
|
||||
const pTag =
|
||||
supermemoryContainer.parentElement.previousElementSibling.querySelector(
|
||||
"p",
|
||||
);
|
||||
userQuery = pTag?.innerText || pTag?.textContent || "";
|
||||
}
|
||||
|
||||
if (!userQuery.trim()) {
|
||||
const textareaElement = document.querySelector('div[contenteditable="true"]') as HTMLElement
|
||||
userQuery = textareaElement?.innerText || textareaElement?.textContent || ""
|
||||
const textareaElement = document.querySelector(
|
||||
'div[contenteditable="true"]',
|
||||
) as HTMLElement;
|
||||
userQuery =
|
||||
textareaElement?.innerText || textareaElement?.textContent || "";
|
||||
}
|
||||
|
||||
if (!userQuery.trim()) {
|
||||
const inputElements = document.querySelectorAll('div[contenteditable="true"], textarea, input[type="text"]')
|
||||
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
|
||||
const text =
|
||||
(element as HTMLElement).innerText ||
|
||||
(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")
|
||||
DOMUtils.showToast("error")
|
||||
return
|
||||
console.log("No query text found");
|
||||
DOMUtils.showToast("error");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
|
||||
data: userQuery,
|
||||
})
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
const textareaElement = document.querySelector(
|
||||
'div[contenteditable="true"]',
|
||||
) as HTMLElement;
|
||||
|
||||
if (textareaElement) {
|
||||
const currentContent = textareaElement.innerHTML
|
||||
textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`
|
||||
|
||||
textareaElement.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
const currentContent = textareaElement.innerHTML;
|
||||
textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`;
|
||||
|
||||
textareaElement.dispatchEvent(
|
||||
new Event("input", { bubbles: true }),
|
||||
);
|
||||
} else {
|
||||
console.log("Could not find Claude input area")
|
||||
console.log("Could not find Claude input area");
|
||||
}
|
||||
} else {
|
||||
console.log("Failed to get memories:", response.error || "Unknown error")
|
||||
console.log(
|
||||
"Failed to get memories:",
|
||||
response.error || "Unknown error",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting related memories for Claude:", error)
|
||||
console.error("Error getting related memories for Claude:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function addSupermemoryIconToT3Input() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.T3)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
await getRelatedMemoriesForT3();
|
||||
});
|
||||
|
||||
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() {
|
||||
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 || ""
|
||||
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 || "";
|
||||
}
|
||||
|
||||
if (!userQuery.trim()) {
|
||||
const textareaElement = document.querySelector('div[contenteditable="true"]') as HTMLElement
|
||||
userQuery = textareaElement?.innerText || textareaElement?.textContent || ""
|
||||
const textareaElement = document.querySelector(
|
||||
'div[contenteditable="true"]',
|
||||
) as HTMLElement;
|
||||
userQuery =
|
||||
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")
|
||||
return
|
||||
console.log("No query text found");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
|
||||
data: userQuery,
|
||||
})
|
||||
});
|
||||
|
||||
console.log("T3 memories response:", response)
|
||||
console.log("T3 memories response:", response);
|
||||
|
||||
if (response.success && response.data) {
|
||||
let textareaElement = null
|
||||
const supermemoryContainer = document.querySelector('[data-supermemory-icon-added="true"]')
|
||||
if (supermemoryContainer?.parentElement?.parentElement?.previousElementSibling) {
|
||||
textareaElement = supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector('textarea')
|
||||
let textareaElement = null;
|
||||
const supermemoryContainer = document.querySelector(
|
||||
'[data-supermemory-icon-added="true"]',
|
||||
);
|
||||
if (
|
||||
supermemoryContainer?.parentElement?.parentElement
|
||||
?.previousElementSibling
|
||||
) {
|
||||
textareaElement =
|
||||
supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector(
|
||||
"textarea",
|
||||
);
|
||||
}
|
||||
|
||||
if (!textareaElement) {
|
||||
textareaElement = document.querySelector('div[contenteditable="true"]') as HTMLElement
|
||||
textareaElement = document.querySelector(
|
||||
'div[contenteditable="true"]',
|
||||
) as HTMLElement;
|
||||
}
|
||||
|
||||
|
||||
if (textareaElement) {
|
||||
if (textareaElement.tagName === 'TEXTAREA') {
|
||||
const currentContent = (textareaElement as HTMLTextAreaElement).value
|
||||
;(textareaElement as HTMLTextAreaElement).value = `${currentContent}\n\nSupermemories: ${response.data}`
|
||||
if (textareaElement.tagName === "TEXTAREA") {
|
||||
const currentContent = (textareaElement as HTMLTextAreaElement)
|
||||
.value;
|
||||
(textareaElement as HTMLTextAreaElement).value =
|
||||
`${currentContent}\n\nSupermemories: ${response.data}`;
|
||||
} else {
|
||||
const currentContent = textareaElement.innerHTML
|
||||
textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`
|
||||
const currentContent = textareaElement.innerHTML;
|
||||
textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`;
|
||||
}
|
||||
|
||||
textareaElement.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
|
||||
textareaElement.dispatchEvent(
|
||||
new Event("input", { bubbles: true }),
|
||||
);
|
||||
} else {
|
||||
console.log("Could not find T3 input area")
|
||||
console.log("Could not find T3 input area");
|
||||
}
|
||||
} else {
|
||||
console.log("Failed to get memories:", response.error || "Unknown error")
|
||||
console.log(
|
||||
"Failed to get memories:",
|
||||
response.error || "Unknown error",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting related memories for T3:", error)
|
||||
console.error("Error getting related memories for T3:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add Tweet Capture Functionality
|
||||
function _addSaveTweetElement() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const targetDivs = document.querySelectorAll(
|
||||
"div.css-175oi2r.r-18u37iz.r-1h0z5md.r-1wron08",
|
||||
)
|
||||
);
|
||||
|
||||
targetDivs.forEach((targetDiv) => {
|
||||
if (targetDiv.hasAttribute("data-supermemory-icon-added")) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const previousElement = targetDiv.previousElementSibling
|
||||
const previousElement = targetDiv.previousElementSibling;
|
||||
if (previousElement?.id?.startsWith(ELEMENT_IDS.SAVE_TWEET_ELEMENT)) {
|
||||
targetDiv.setAttribute("data-supermemory-icon-added", "true")
|
||||
return
|
||||
targetDiv.setAttribute("data-supermemory-icon-added", "true");
|
||||
return;
|
||||
}
|
||||
|
||||
const saveTweetElement = createSaveTweetElement(async () => {
|
||||
await saveMemory()
|
||||
})
|
||||
await saveMemory();
|
||||
});
|
||||
|
||||
saveTweetElement.id = `${ELEMENT_IDS.SAVE_TWEET_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
|
||||
saveTweetElement.id = `${ELEMENT_IDS.SAVE_TWEET_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
|
||||
targetDiv.setAttribute("data-supermemory-icon-added", "true")
|
||||
targetDiv.setAttribute("data-supermemory-icon-added", "true");
|
||||
|
||||
targetDiv.parentNode?.insertBefore(saveTweetElement, targetDiv)
|
||||
})
|
||||
targetDiv.parentNode?.insertBefore(saveTweetElement, targetDiv);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", async (event) => {
|
||||
|
|
@ -634,16 +692,16 @@ export default defineContentScript({
|
|||
event.shiftKey &&
|
||||
event.key === "m"
|
||||
) {
|
||||
event.preventDefault()
|
||||
await saveMemory()
|
||||
event.preventDefault();
|
||||
await saveMemory();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.source !== window) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const bearerToken = event.data.token
|
||||
const bearerToken = event.data.token;
|
||||
|
||||
if (bearerToken) {
|
||||
if (
|
||||
|
|
@ -655,14 +713,17 @@ export default defineContentScript({
|
|||
) {
|
||||
console.log(
|
||||
"Bearer token is only allowed to be used on localhost or supermemory.ai",
|
||||
)
|
||||
return
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.local.set({
|
||||
[STORAGE_KEYS.BEARER_TOKEN]: bearerToken,
|
||||
}, () => {})
|
||||
chrome.storage.local.set(
|
||||
{
|
||||
[STORAGE_KEYS.BEARER_TOKEN]: bearerToken,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,130 +1,137 @@
|
|||
import { useQueryClient } from "@tanstack/react-query"
|
||||
import { useEffect, useState } from "react"
|
||||
import "./App.css"
|
||||
import { STORAGE_KEYS } from "../../utils/constants"
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect, useState } from "react";
|
||||
import "./App.css";
|
||||
import { 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 [activeTab, setActiveTab] = useState<"save" | "imports">("save")
|
||||
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">("save");
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
const result = await chrome.storage.local.get([
|
||||
STORAGE_KEYS.BEARER_TOKEN,
|
||||
])
|
||||
const isSignedIn = !!result[STORAGE_KEYS.BEARER_TOKEN]
|
||||
setUserSignedIn(isSignedIn)
|
||||
]);
|
||||
const isSignedIn = !!result[STORAGE_KEYS.BEARER_TOKEN];
|
||||
setUserSignedIn(isSignedIn);
|
||||
} 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) {
|
||||
await chrome.tabs.sendMessage(tabs[0].id, {
|
||||
action: "saveMemory",
|
||||
})
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save current page:", error)
|
||||
console.error("Failed to save current page:", error);
|
||||
} finally {
|
||||
setSaving(false)
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
await chrome.storage.local.remove([STORAGE_KEYS.BEARER_TOKEN])
|
||||
setUserSignedIn(false)
|
||||
queryClient.clear()
|
||||
await chrome.storage.local.remove([STORAGE_KEYS.BEARER_TOKEN]);
|
||||
setUserSignedIn(false);
|
||||
queryClient.clear();
|
||||
} catch (error) {
|
||||
console.error("Error signing out:", error)
|
||||
console.error("Error signing out:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="w-80 p-0 font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif] bg-white rounded-lg relative overflow-hidden">
|
||||
<div className="flex items-center justify-between gap-3 p-2.5 border-b border-gray-200 relative">
|
||||
<img alt="supermemory" className="w-8 h-8 flex-shrink-0" src="/icon-48.png" />
|
||||
<h1 className="m-0 text-lg font-semibold text-black flex-1">supermemory</h1>
|
||||
<img
|
||||
alt="supermemory"
|
||||
className="w-8 h-8 flex-shrink-0"
|
||||
src="/icon-48.png"
|
||||
/>
|
||||
<h1 className="m-0 text-lg font-semibold text-black flex-1">
|
||||
supermemory
|
||||
</h1>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -199,7 +206,9 @@ function App() {
|
|||
<h3 className="m-0 mb-1 text-sm font-semibold text-black overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{currentTitle || "Current Page"}
|
||||
</h3>
|
||||
<p className="m-0 text-xs text-gray-500 overflow-hidden text-ellipsis whitespace-nowrap">{currentUrl}</p>
|
||||
<p className="m-0 text-xs text-gray-500 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{currentUrl}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -211,7 +220,9 @@ function App() {
|
|||
type="button"
|
||||
>
|
||||
<div className="flex justify-between items-center p-3 bg-gray-50 rounded-lg border border-gray-200 transition-colors duration-200 hover:bg-gray-200 hover:border-gray-300">
|
||||
<span className="text-sm font-medium text-gray-600">Save to project:</span>
|
||||
<span className="text-sm font-medium text-gray-600">
|
||||
Save to project:
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-black overflow-hidden text-ellipsis whitespace-nowrap max-w-[120px]">
|
||||
{defaultProject
|
||||
|
|
@ -260,7 +271,7 @@ function App() {
|
|||
onClick={() => {
|
||||
chrome.tabs.create({
|
||||
url: "https://chatgpt.com/#settings/Personalization",
|
||||
})
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -285,7 +296,7 @@ function App() {
|
|||
onClick={() => {
|
||||
chrome.tabs.create({
|
||||
url: "https://x.com/i/bookmarks",
|
||||
})
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -322,7 +333,9 @@ function App() {
|
|||
</button>
|
||||
</div>
|
||||
{loadingProjects ? (
|
||||
<div className="py-8 px-4 text-center text-gray-500 text-sm">Loading projects...</div>
|
||||
<div className="py-8 px-4 text-center text-gray-500 text-sm">
|
||||
Loading projects...
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
{projects.map((project) => (
|
||||
|
|
@ -343,7 +356,9 @@ function App() {
|
|||
</span>
|
||||
</div>
|
||||
{defaultProject?.id === project.id && (
|
||||
<span className="text-blue-600 font-bold text-base">✓</span>
|
||||
<span className="text-blue-600 font-bold text-base">
|
||||
✓
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
|
|
@ -360,9 +375,15 @@ function App() {
|
|||
</h2>
|
||||
|
||||
<ul className="list-none p-0 m-0 text-left">
|
||||
<li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold">Save any page to your supermemory</li>
|
||||
<li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold">Import all your Twitter / X Bookmarks</li>
|
||||
<li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold">Import your ChatGPT Memories</li>
|
||||
<li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold">
|
||||
Save any page to your supermemory
|
||||
</li>
|
||||
<li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold">
|
||||
Import all your Twitter / X Bookmarks
|
||||
</li>
|
||||
<li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold">
|
||||
Import your ChatGPT Memories
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
@ -372,7 +393,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"
|
||||
>
|
||||
|
|
@ -387,18 +408,18 @@ function App() {
|
|||
url: import.meta.env.PROD
|
||||
? "https://app.supermemory.ai/login"
|
||||
: "http://localhost:3000/login",
|
||||
})
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
login in
|
||||
Sign in or create account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -10,23 +10,24 @@ function Welcome() {
|
|||
src="https://assets.supermemory.ai/brand/wordmark/dark-transparent.svg"
|
||||
/>
|
||||
<p className="text-gray-600 text-lg font-normal max-w-2xl mx-auto">
|
||||
Your AI-powered second brain for saving and organizing everything
|
||||
that matters
|
||||
Your AI second brain for saving and organizing everything that
|
||||
matters. Supermemory learns and remembers everything you save, your
|
||||
preferences, and understands you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Features Section */}
|
||||
<div className="mb-12">
|
||||
<h2 className="text-2xl font-semibold text-black mb-8"
|
||||
>What can you do with supermemory
|
||||
?</h2>
|
||||
<h2 className="text-2xl font-semibold text-black mb-8">
|
||||
What can you do with supermemory ?
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 text-center transition-all duration-200 shadow-sm hover:-translate-y-0.5 hover:shadow-md hover:border-gray-300">
|
||||
<div className="text-3xl mb-4 block">💾</div>
|
||||
<h3 className="text-lg font-semibold text-black mb-3"
|
||||
>Save Any Pag
|
||||
e</h3>
|
||||
<h3 className="text-lg font-semibold text-black mb-3">
|
||||
Save Any Page
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 leading-snug">
|
||||
Instantly save web pages, articles, and content to your personal
|
||||
knowledge base
|
||||
|
|
@ -57,10 +58,11 @@ function Welcome() {
|
|||
<div className="bg-white border border-gray-200 rounded-xl p-6 text-center transition-all duration-200 shadow-sm hover:-translate-y-0.5 hover:shadow-md hover:border-gray-300">
|
||||
<div className="text-3xl mb-4 block">🔍</div>
|
||||
<h3 className="text-lg font-semibold text-black mb-3">
|
||||
AI-Powered Search
|
||||
Your context, everywhere.
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 leading-snug">
|
||||
Find anything you've saved using intelligent semantic search
|
||||
You can connect chatbots with MCP, chat with your personal
|
||||
assistant, and more.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -75,7 +77,7 @@ function Welcome() {
|
|||
url: import.meta.env.PROD
|
||||
? "https://app.supermemory.ai/login"
|
||||
: "http://localhost:3000/login",
|
||||
})
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -99,7 +101,7 @@ function Welcome() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Welcome
|
||||
export default Welcome;
|
||||
|
|
|
|||
BIN
apps/browser-extension/public/icon-128x128.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
apps/browser-extension/public/icon-144x144.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
apps/browser-extension/public/icon-152x152.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/browser-extension/public/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
apps/browser-extension/public/icon-256x256.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
apps/browser-extension/public/icon-384x384.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
apps/browser-extension/public/icon-48x48.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
apps/browser-extension/public/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
apps/browser-extension/public/icon-72x72.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
apps/browser-extension/public/icon-96x96.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -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/memories", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
return response
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Failed to save memory:", error)
|
||||
throw error
|
||||
console.error("Failed to save memory:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,12 +122,12 @@ export async function searchMemories(query: string): Promise<unknown> {
|
|||
try {
|
||||
const response = await makeAuthenticatedRequest<unknown>("/v4/search", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ q: query }),
|
||||
})
|
||||
return response
|
||||
body: JSON.stringify({ q: query, include: { relatedMemories: true } }),
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Failed to search memories:", error)
|
||||
throw error
|
||||
console.error("Failed to search memories:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -144,13 +144,13 @@ export async function saveTweet(
|
|||
containerTags: [containerTag],
|
||||
content,
|
||||
metadata,
|
||||
}
|
||||
await saveMemory(payload)
|
||||
};
|
||||
await saveMemory(payload);
|
||||
} catch (error) {
|
||||
if (error instanceof SupermemoryAPIError && error.statusCode === 409) {
|
||||
// Skip if already exists (409 Conflict)
|
||||
return
|
||||
return;
|
||||
}
|
||||
throw error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
@ -11,6 +11,7 @@ export default defineConfig({
|
|||
manifest: {
|
||||
name: "supermemory",
|
||||
homepage_url: "https://supermemory.ai",
|
||||
version: "6.0.000",
|
||||
permissions: [
|
||||
"contextMenus",
|
||||
"storage",
|
||||
|
|
@ -37,4 +38,4 @@ export default defineConfig({
|
|||
webExt: {
|
||||
disabled: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { cn } from "@lib/utils";
|
|||
import { Button } from "@ui/components/button";
|
||||
import { Input } from "@ui/components/input";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import { ArrowUp, Check, Copy, RotateCcw, X } from "lucide-react";
|
||||
import { ArrowUp, Check, Copy, RotateCcw, X, ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
|
@ -14,6 +14,104 @@ import { usePersistentChat, useProject } from "@/stores";
|
|||
import { useGraphHighlights } from "@/stores/highlights";
|
||||
import { Spinner } from "../../spinner";
|
||||
|
||||
interface MemoryResult {
|
||||
documentId?: string;
|
||||
title?: string;
|
||||
content?: string;
|
||||
url?: string;
|
||||
score?: number;
|
||||
}
|
||||
|
||||
interface ExpandableMemoriesProps {
|
||||
foundCount: number;
|
||||
results: MemoryResult[];
|
||||
}
|
||||
|
||||
function ExpandableMemories({ foundCount, results }: ExpandableMemoriesProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
if (foundCount === 0) {
|
||||
return (
|
||||
<div className="text-sm flex items-center gap-2 text-muted-foreground">
|
||||
<Check className="size-4" /> No memories found
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-sm">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="size-4" />
|
||||
) : (
|
||||
<ChevronRight className="size-4" />
|
||||
)}
|
||||
<Check className="size-4" />
|
||||
Found {foundCount} {foundCount === 1 ? "memory" : "memories"}
|
||||
</button>
|
||||
|
||||
{isExpanded && results.length > 0 && (
|
||||
<div className="mt-2 ml-6 space-y-2 max-h-48 overflow-y-auto">
|
||||
{results.map((result, index) => {
|
||||
const isClickable = result.url && (result.url.startsWith('http://') || result.url.startsWith('https://'));
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{result.title && (
|
||||
<div className="font-medium text-sm mb-1 text-foreground">
|
||||
{result.title}
|
||||
</div>
|
||||
)}
|
||||
{result.content && (
|
||||
<div className="text-xs text-muted-foreground line-clamp-2">
|
||||
{result.content}
|
||||
</div>
|
||||
)}
|
||||
{result.url && (
|
||||
<div className="text-xs text-blue-400 mt-1 truncate">
|
||||
{result.url}
|
||||
</div>
|
||||
)}
|
||||
{result.score && (
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
Score: {(result.score * 100).toFixed(1)}%
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (isClickable) {
|
||||
return (
|
||||
<a
|
||||
key={result.documentId || index}
|
||||
href={result.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block p-2 bg-white/5 rounded-md border border-white/10 hover:bg-white/10 hover:border-white/20 transition-colors cursor-pointer"
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={result.documentId || index}
|
||||
className="p-2 bg-white/5 rounded-md border border-white/10"
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useStickyAutoScroll(triggerKeys: ReadonlyArray<unknown>) {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -278,14 +376,14 @@ export function ChatMessages() {
|
|||
"count" in output
|
||||
? Number(output.count) || 0
|
||||
: 0;
|
||||
const results = Array.isArray(output?.results) ? output.results : [];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="text-sm flex items-center gap-2 text-muted-foreground"
|
||||
<ExpandableMemories
|
||||
key={message.id + part.type}
|
||||
>
|
||||
<Check className="size-4" /> Found {foundCount}{" "}
|
||||
memories
|
||||
</div>
|
||||
foundCount={foundCount}
|
||||
results={results}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "next dev --turbo",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
|
|
|
|||
12
bun.lock
|
|
@ -5235,6 +5235,8 @@
|
|||
|
||||
"alchemy/@cloudflare/unenv-preset/unenv": ["unenv@2.0.0-rc.19", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.7", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.6.1" } }, "sha512-t/OMHBNAkknVCI7bVB9OWjUUAwhVv9vsPIAGnNUxnu3FxPQN11rjh0sksLMzc3g7IlTgvHmOTl4JM7JHpcv5wA=="],
|
||||
|
||||
"alchemy/@cloudflare/unenv-preset/workerd": ["workerd@1.20250823.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250823.0", "@cloudflare/workerd-darwin-arm64": "1.20250823.0", "@cloudflare/workerd-linux-64": "1.20250823.0", "@cloudflare/workerd-linux-arm64": "1.20250823.0", "@cloudflare/workerd-windows-64": "1.20250823.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-95lToK9zeaC7bX5ZmlP/wz6zqoUPBk3hhec1JjEMGZrxsXY9cPRkjWNCcjDctQ17U97vjMcY/ymchgx7w8Cfmg=="],
|
||||
|
||||
"alchemy/miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
|
||||
|
||||
"alchemy/miniflare/undici": ["undici@7.15.0", "", {}, "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ=="],
|
||||
|
|
@ -5565,6 +5567,16 @@
|
|||
|
||||
"@semantic-release/github/@octokit/core/@octokit/request/fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
|
||||
|
||||
"alchemy/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250823.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-yRLJc1cQNqQYcDViOk7kpTXnR5XuBP7B/Ms5KBdlQ6eTr2Vsg9mfKqWKInjzY8/Cx+p+Sic2Tbld42gcYkiM2A=="],
|
||||
|
||||
"alchemy/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250823.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-KJnikUe6J29Ga1QMPKNCc8eHD56DdBlu5XE5LoBH/AYRrbS5UI1d5F844hUWoFKJb8KRaPIH9F849HZWfNa1vw=="],
|
||||
|
||||
"alchemy/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250823.0", "", { "os": "linux", "cpu": "x64" }, "sha512-4QFXq4eDWEAK5QjGxRe0XUTBax1Fgarc08HETL6q0y/KPZp2nOTLfjLjklTn/qEiztafNFoJEIwhkiknHeOi/g=="],
|
||||
|
||||
"alchemy/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250823.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-sODSrSVe4W/maoBu76qb0sJGBhxhSM2Q2tg/+G7q1IPgRZSzArMKIPrW6nBnmBrrG1O0X6aoAdID6w5hfuEM4g=="],
|
||||
|
||||
"alchemy/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250823.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WaNqUOXUnrcEI+i2NI4+okA9CrJMI9n2XTfVtDg/pLvcA/ZPTz23MEFMZU1splr4SslS1th1NBO38RMPnDB4rA=="],
|
||||
|
||||
"alchemy/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
|
||||
|
||||
"alchemy/miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
|
||||
|
|
|
|||