diff --git a/README.md b/README.md index 1fad091b..045746ef 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,21 @@ Just go to [supermemory.ai](https://supermemory.ai) and sign in with your google To use the chrome extension, -1. Get the chrome ext (click on the button) -2. Click on the "Extension Auth" button so the extension knows who you are :) - ![chrome](https://i.dhr.wtf/r/Clipboard_Apr_15,_2024_at_10.47 AM.png) +> [!WARNING] +> You need to be signed in before installing the supermemory extension, or you may experience problems + +1. Download from [Chrome Web Store](https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc?authuser=0&hl=en-GB) +2. Now you can see on any page on bottom right (just click on it to save) + image + +### Import Twitter Bookmarks +1. Make sure you signed into supermemory and installed chrome extension +2. Open Twitter/X, you will see the save icon as follows + + image +4. Click on save button and give it 10 - 20 secs, where supermemory extension will sync all your twitter bookmarks to supermemory.ai +5. Volia! Now your second brain has all your twitter bookmarks. + ## 👨‍💻 The Stack @@ -36,7 +48,8 @@ Supermemory has three main modules, managed by [turborepo](https://turbo.build): The database, auth etc logic is here -![App preview](https://i.dhr.wtf/r/Clipboard_Apr_14,_2024_at_4.10 PM.png) +![image](https://github.com/MaheshtheDev/supermemory/assets/38828053/0c44708d-600a-43a8-a641-835cb1f349fc) + Built with: @@ -54,10 +67,7 @@ Built with: The chrome extension is one of the most important part of the setup, but is not required.This is to easily add pages to your memory. -![Chrome extension preview](https://i.dhr.wtf/r/Clipboard_Apr_14,_2024_at_3.54 PM.png) - -You can also use it to import all your twitter bookmarks! -![Import bookmarks](https://i.dhr.wtf/r/Clipboard_Apr_14,_2024_at_3.56 PM.png) +image Built with: diff --git a/apps/browser-rendering b/apps/browser-rendering index 4d21045a..0135d881 160000 --- a/apps/browser-rendering +++ b/apps/browser-rendering @@ -1 +1 @@ -Subproject commit 4d21045a45fdbf56b7483d3704ae0474ebf044fb +Subproject commit 0135d8813311b6825382f411cda13f5663b6c462 diff --git a/apps/cf-ai-backend/src/helper.ts b/apps/cf-ai-backend/src/helper.ts index 44dba383..98e38ce8 100644 --- a/apps/cf-ai-backend/src/helper.ts +++ b/apps/cf-ai-backend/src/helper.ts @@ -30,6 +30,8 @@ export async function initQuery( case "claude-3-opus": const anthropic = createAnthropic({ apiKey: c.env.ANTHROPIC_API_KEY, + baseURL: + "https://gateway.ai.cloudflare.com/v1/47c2b4d598af9d423c06fc9f936226d5/supermemory/anthropic", }); selectedModel = anthropic.chat("claude-3-opus-20240229"); console.log("Selected model: ", selectedModel); @@ -37,6 +39,8 @@ export async function initQuery( case "gemini-1.5-pro": const googleai = createGoogleGenerativeAI({ apiKey: c.env.GOOGLE_AI_API_KEY, + baseURL: + "https://gateway.ai.cloudflare.com/v1/47c2b4d598af9d423c06fc9f936226d5/supermemory/google-vertex-ai", }); selectedModel = googleai.chat("models/gemini-1.5-pro-latest"); console.log("Selected model: ", selectedModel); @@ -45,6 +49,9 @@ export async function initQuery( default: const openai = createOpenAI({ apiKey: c.env.OPENAI_API_KEY, + baseURL: + "https://gateway.ai.cloudflare.com/v1/47c2b4d598af9d423c06fc9f936226d5/supermemory/openai", + compatibility: "strict", }); selectedModel = openai.chat("gpt-4o"); break; diff --git a/apps/cf-ai-backend/src/index.ts b/apps/cf-ai-backend/src/index.ts index 31ee520f..04f80f0d 100644 --- a/apps/cf-ai-backend/src/index.ts +++ b/apps/cf-ai-backend/src/index.ts @@ -509,7 +509,7 @@ app.post( const userMessage: CoreMessage = { role: "user", content: prompt }; const response = await streamText({ - model, + model: model, messages: [ ...initialMessages, ...((body.chatHistory || []) as CoreMessage[]), diff --git a/apps/cf-ai-backend/src/prompts/prompt1.ts b/apps/cf-ai-backend/src/prompts/prompt1.ts index 289495b6..e52fe2cd 100644 --- a/apps/cf-ai-backend/src/prompts/prompt1.ts +++ b/apps/cf-ai-backend/src/prompts/prompt1.ts @@ -19,13 +19,10 @@ export const template = ({ contexts, question }) => { const contextParts = contexts .map( ({ context, normalisedScore }) => ` - - ${context} - - - normalisedScore: ${normalisedScore} - `, + ${context ? ` ${context} ` : ""} + + ${normalisedScore ? ` normalisedScore: ${normalisedScore} ` : ""}`, ) .join("\n"); diff --git a/apps/cf-ai-backend/src/utils/OpenAIEmbedder.ts b/apps/cf-ai-backend/src/utils/OpenAIEmbedder.ts index be5839b1..8364ed0d 100644 --- a/apps/cf-ai-backend/src/utils/OpenAIEmbedder.ts +++ b/apps/cf-ai-backend/src/utils/OpenAIEmbedder.ts @@ -22,17 +22,20 @@ export class OpenAIEmbeddings { } async embedQuery(text: string): Promise { - const response = await fetch("https://api.openai.com/v1/embeddings", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${this.apiKey}`, + const response = await fetch( + "https://gateway.ai.cloudflare.com/v1/47c2b4d598af9d423c06fc9f936226d5/supermemory/openai/embeddings", + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ + input: text, + model: this.modelName, + }), }, - body: JSON.stringify({ - input: text, - model: this.modelName, - }), - }); + ); const data = await response.json(); diff --git a/apps/extension/background.ts b/apps/extension/background.ts index 1102b764..406493f1 100644 --- a/apps/extension/background.ts +++ b/apps/extension/background.ts @@ -74,15 +74,28 @@ const batchImportAll = async (cursor = "", totalImported = 0) => { description: tweet.text.slice(0, 200), type: "tweet", }), - }).then((ers) => { + }).then(async (ers) => { console.log(ers.status); importedCount++; totalImported++; - // Send an update message to the content script - chrome.runtime.sendMessage({ - type: "import-update", - importedCount: totalImported, - }); + console.log(totalImported); + chrome.tabs.query( + { active: true, currentWindow: true }, + async function (tabs) { + if (tabs.length > 0) { + let currentTabId = tabs[0].id; + + if (!currentTabId) { + return; + } + + await chrome.tabs.sendMessage(currentTabId, { + type: "import-update", + importedCount: totalImported, + }); + } + }, + ); }); }); })(); @@ -111,19 +124,45 @@ const batchImportAll = async (cursor = "", totalImported = 0) => { batchImportAll(nextCursor, totalImported); // Recursively call with new cursor } else { console.log("All bookmarks imported"); - // Send a "done" message to the content script - chrome.runtime.sendMessage({ - type: "import-done", - importedCount: totalImported, - }); + + chrome.tabs.query( + { active: true, currentWindow: true }, + async function (tabs) { + if (tabs.length > 0) { + let currentTabId = tabs[0].id; + + if (!currentTabId) { + return; + } + + await chrome.runtime.sendMessage({ + type: "import-done", + importedCount: totalImported, + }); + } + }, + ); } } else { console.log("All bookmarks imported"); // Send a "done" message to the content script - chrome.runtime.sendMessage({ - type: "import-done", - importedCount: totalImported, - }); + chrome.tabs.query( + { active: true, currentWindow: true }, + async function (tabs) { + if (tabs.length > 0) { + let currentTabId = tabs[0].id; + + if (!currentTabId) { + return; + } + + await chrome.runtime.sendMessage({ + type: "import-done", + importedCount: totalImported, + }); + } + }, + ); } }) .catch((error) => console.error(error)); @@ -313,5 +352,145 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { })(); } else if (request.type === "batchImportAll") { batchImportAll(); + return true; + } +}); + +chrome.runtime.onInstalled.addListener(function (details) { + if (details.reason === "install") { + chrome.tabs.create({ + url: "https://supermemory.ai/signin?extension=true", + active: true, + }); + } +}); + +chrome.runtime.onInstalled.addListener(() => { + chrome.contextMenus.create({ + id: "saveSelection", + title: "Save note to Supermemory", + contexts: ["selection"], + }); + + chrome.contextMenus.create({ + id: "savePage", + title: "Save page to Supermemory", + contexts: ["page"], + }); + + // TODO + // chrome.contextMenus.create({ + // id: 'saveLink', + // title: 'Save link to Supermemory', + // contexts: ['link'], + // }); +}); + +interface FetchDataParams { + content: string; + url: string; + title: string; + description: string; + ogImage: string; + favicon: string; + isExternalContent: boolean; // Indicates if the content is from an external API +} + +const fetchData = ({ + content, + url, + title, + description, + ogImage, + favicon, + isExternalContent, +}: FetchDataParams) => { + // Construct the URL + const finalUrl = isExternalContent + ? url + : `${url}#supermemory-stuff-${Math.random()}`; + + // Construct the body + const body = JSON.stringify({ + pageContent: content, + url: finalUrl, + title, + spaces: [], + description, + ogImage, + image: favicon, + }); + + // Make the fetch call + fetch(`${BACKEND_URL}/api/store`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: body, + }) + .then((response) => { + console.log("Data saved successfully"); + }) + .catch((error) => { + console.error("Error saving data:", error); + }); + + return Promise.resolve(); +}; + +chrome.contextMenus.onClicked.addListener((info, tab) => { + if (!tab || !tab.id) return; + + const tabId = tab.id; + + const sendMessageToTab = (message: string) => { + chrome.tabs.sendMessage(tabId, { message, type: "supermemory-message" }); + }; + + if (info.menuItemId === "saveSelection" && info.selectionText) { + sendMessageToTab("Saving selection..."); + fetchData({ + content: info.selectionText || "No content", + url: info.pageUrl, + title: tab.title || "Selection Title", + description: "User-selected content from the page", + ogImage: "", + favicon: "", + isExternalContent: false, + }) + .then(() => { + sendMessageToTab("Selection saved successfully."); + }) + .catch(() => { + sendMessageToTab("Failed to save selection."); + }); + } else if (info.menuItemId === "savePage") { + sendMessageToTab("Saving page..."); + chrome.scripting.executeScript( + { + target: { tabId: tabId }, + func: () => document.body.innerText, + }, + (results) => { + if (results.length > 0 && results[0].result) { + fetchData({ + content: results[0].result as string, + url: info.pageUrl, + title: tab.title || "Page Title", + description: "Full page content", + ogImage: "", + favicon: "", + isExternalContent: false, + }) + .then(() => { + sendMessageToTab("Page saved successfully."); + }) + .catch(() => { + sendMessageToTab("Failed to save page."); + }); + } + }, + ); } }); diff --git a/apps/extension/components.json b/apps/extension/components.json new file mode 100644 index 00000000..d3099362 --- /dev/null +++ b/apps/extension/components.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "../../packages/tailwind-config/globals.css", + "baseColor": "stone", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "content/ui/components", + "utils": "content/utils", + "ui": "content/ui/shadcn" + } +} diff --git a/apps/extension/content/ContentApp.tsx b/apps/extension/content/ContentApp.tsx index 4fe2bc66..d82857e2 100644 --- a/apps/extension/content/ContentApp.tsx +++ b/apps/extension/content/ContentApp.tsx @@ -1,15 +1,100 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Readability } from "@mozilla/readability"; -import tailwindBg from "../public/tailwind_bg.png"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "./ui/shadcn/tooltip"; +import { Popover, PopoverContent, PopoverTrigger } from "./ui/shadcn/popover"; +import { Toaster } from "./ui/shadcn/toaster"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "./ui/shadcn/select"; +import { useToast } from "./ui/shadcn/use-toast"; +import { Input } from "./ui/shadcn/input"; +import { Label } from "./ui/shadcn/label"; +import { Textarea } from "./ui/shadcn/textarea"; -export default function ContentApp({ token }: { token: string | undefined }) { - const [text, setText] = useState(""); +const BACKEND_URL = "https://supermemory.ai"; + +export default function ContentApp({ + token, + shadowRoot, +}: { + token: string | undefined; + shadowRoot: ShadowRoot; +}) { const [hover, setHover] = useState(false); + const { toast } = useToast(); + const [loading, setLoading] = useState(false); + const [webNote, setWebNote] = useState(""); + const [importedCount, setImportedCount] = useState(0); const [isImporting, setIsImporting] = useState(false); + const [importDone, setImportDone] = useState(false); + + const [portalContainer, setPortalContainer] = useState( + null, + ); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [isPopover2Open, setIsPopover2Open] = useState(false); + + const [spacesOptions, setSpacesOptions] = useState< + { id: number; name: string }[] + >([]); + const [selectedSpace, setSelectedSpace] = useState(); + + const [userNotLoggedIn, setUserNotLoggedIn] = useState(false); + + const showLoginToast = async () => { + setUserNotLoggedIn(true); + + const NOSHOW_TOAST = ["accounts.google.com", "supermemory.ai"]; + + const noLoginWarning = await chrome.storage.local.get("noLoginWarning"); + if (Object.keys(noLoginWarning).length > 0) { + return; + } + + if (!NOSHOW_TOAST.includes(window.location.host)) { + const t = toast({ + title: "Please login to supermemory.ai to use this extension.", + action: ( +
+ + + +
+ ), + }); + } + }; useEffect(() => { document.addEventListener("mousemove", (e) => { @@ -24,26 +109,12 @@ export default function ContentApp({ token }: { token: string | undefined }) { }); const getUserData = () => { - const NO_JWT = [ - "supermemory.ai", - "beta.supermemory.ai", - "localhost:3000", - ]; - chrome.runtime.sendMessage({ type: "getJwt" }, (response) => { - if (!response.jwt && !NO_JWT.includes(window.location.host)) { - window.location.href = "https://supermemory.ai/signin"; - } - - console.log("jwt", response.jwt); - }); + chrome.runtime.sendMessage({ type: "getJwt" }); }; getUserData(); chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - console.log("request", request); - console.log("sender", sender); - console.log("sendResponse", sendResponse); if (request.type === "import-update") { setIsImporting(true); setImportedCount(request.importedCount); @@ -51,15 +122,51 @@ export default function ContentApp({ token }: { token: string | undefined }) { if (request.type === "import-done") { setIsImporting(false); + setImportDone(true); + } + + if (request.type === "supermemory-message") { + toast({ + title: request.message, + }); } }); + const portalDiv = document.createElement("div"); + portalDiv.id = "popover-portal"; + shadowRoot.appendChild(portalDiv); + setPortalContainer(portalDiv); + + const getSpaces = async () => { + const response = await fetch(`${BACKEND_URL}/api/spaces`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (response.status === 401) { + showLoginToast(); + return; + } + + try { + const data = await response.json(); + setSpacesOptions(data.data); + } catch (e) { + console.error( + `Error in supermemory.ai extension: ${e}. Please contact the developer https://x.com/dhravyashah`, + ); + } + }; + + getSpaces(); + return () => { document.removeEventListener("mousemove", () => {}); }; }, []); - function sendUrlToAPI(spaces: number[]) { + async function sendUrlToAPI(spaces: string[]) { setLoading(true); setTimeout(() => { @@ -72,7 +179,6 @@ export default function ContentApp({ token }: { token: string | undefined }) { const blacklist: string[] = []; // check if the URL is blacklisted if (blacklist.some((blacklisted) => url.includes(blacklisted))) { - console.log("URL is blacklisted"); return; } else { const clone = document.cloneNode(true) as Document; @@ -86,93 +192,237 @@ export default function ContentApp({ token }: { token: string | undefined }) { document.querySelector('link[rel="icon"]') as HTMLLinkElement )?.href; - console.log("article", article); - chrome.runtime.sendMessage({ - type: "urlSave", - content: article?.textContent, - url, - spaces, - title: article?.title, - description: article?.excerpt, - ogImage: ogImage, - favicon: favicon, + setLoading(true); + + setIsPopoverOpen(false); + + await fetch(`${BACKEND_URL}/api/store`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + pageContent: + (webNote ? `Note about this website: ${webNote}\n\n` : "") + + article?.textContent, + url: url + "#supermemory-user-" + Math.random(), + title: article?.title.slice(0, 500), + spaces: spaces, + description: article?.excerpt.slice(0, 250), + ogImage: ogImage?.slice(0, 1000), + image: favicon, + }), + }).then(async (rep) => { + if (rep.status === 401) { + showLoginToast(); + return; + } + + const d = await rep.json(); + + if (rep.status === 200) { + toast({ + title: "Saved to supermemory.ai", + }); + } else { + toast({ + title: `Failed to save to supermemory.ai: ${d.error ?? "Unknown error"}`, + }); + } + setLoading(false); + return rep; }); } } - return ( -
- + if (!shadowRoot || !portalContainer) { + return null; + } - {isImporting && ( -
-
-
-
- -
+ return ( +
+ + + + + + + + + + + + + + {userNotLoggedIn ? ( + <>You need to login to use this extension. + ) : ( +

Add to supermemory.ai

+ )} +
+
+
+ + {userNotLoggedIn ? ( +
+
-
-

- Your twitter bookmarks are being imported. -

-

- This is done completely locally, so please keep this tab open! -

+ ) : ( +
+ + + +