Merge branch 'main' into fix/clean_ups

This commit is contained in:
Dhravya Shah 2024-07-04 20:46:14 -05:00 • committed by GitHub
commit abf9bf0882
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 1913 additions and 785 deletions

View file

@ -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)
<img width="1058" alt="image" src="https://github.com/MaheshtheDev/supermemory/assets/38828053/f24b0988-cd2b-4216-a75b-e9ff0dbfaa6a">
### 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
<img width="480" alt="image" src="https://github.com/MaheshtheDev/supermemory/assets/38828053/2efb06a5-912a-48e7-ad1c-d527e7ffbc94">
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)
<img width="290" alt="image" src="https://github.com/MaheshtheDev/supermemory/assets/38828053/fa4993bb-c447-46a8-a301-9e0fa84d406a">
Built with:

@ -1 +1 @@
Subproject commit 4d21045a45fdbf56b7483d3704ae0474ebf044fb
Subproject commit 0135d8813311b6825382f411cda13f5663b6c462

View file

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

View file

@ -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[]),

View file

@ -19,13 +19,10 @@ export const template = ({ contexts, question }) => {
const contextParts = contexts
.map(
({ context, normalisedScore }) => `
<context>
${context}
</context>
<context_score>
normalisedScore: ${normalisedScore}
</context_score>`,
${context ? `<context> ${context} </context>` : ""}
${normalisedScore ? `<context_score> normalisedScore: ${normalisedScore} </context_score>` : ""}`,
)
.join("\n");

View file

@ -22,17 +22,20 @@ export class OpenAIEmbeddings {
}
async embedQuery(text: string): Promise<number[]> {
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();

View file

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

View file

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

View file

@ -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<string>("");
const [importedCount, setImportedCount] = useState(0);
const [isImporting, setIsImporting] = useState(false);
const [importDone, setImportDone] = useState(false);
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(
null,
);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [isPopover2Open, setIsPopover2Open] = useState(false);
const [spacesOptions, setSpacesOptions] = useState<
{ id: number; name: string }[]
>([]);
const [selectedSpace, setSelectedSpace] = useState<string>();
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: (
<div className="flex flex-col gap-2">
<button
onClick={() =>
window.open("https://supermemory.ai/signin", "_blank")
}
>
Login
</button>
<button
className="text-xs"
onClick={async () => {
await chrome.storage.local.set({
noLoginWarning: true,
});
t.dismiss();
}}
>
Ignore
</button>
</div>
),
});
}
};
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 (
<div className="flex justify-end items-end min-h-screen w-full">
<button
onClick={() => sendUrlToAPI([])}
className={`${hover && "opacity-100"} hover:bg-black p-2 rounded-l-2xl transition bg-gray-900 border border-white/20 opacity-0 size-4 h-[30vh] absolute flex bottom-20 items-center text-lg`}
>
{loading ? (
<div className="text-sm">Saving...</div>
) : (
<svg
className="size-8"
width={24}
height={24}
viewBox="0 0 42 42"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.0357 8C20.5531 8 21 9.27461 21 10.8438V16.3281H23.5536V14.2212C23.5536 13.1976 23.9468 12.216 24.6467 11.4922L25.0529 11.0721C24.9729 10.8772 24.9286 10.6627 24.9286 10.4375C24.9286 9.54004 25.6321 8.8125 26.5 8.8125C27.3679 8.8125 28.0714 9.54004 28.0714 10.4375C28.0714 11.335 27.3679 12.0625 26.5 12.0625C26.2822 12.0625 26.0748 12.0167 25.8863 11.9339L25.4801 12.354C25.0012 12.8492 24.7321 13.5209 24.7321 14.2212V16.3281H28.9714C29.2045 15.7326 29.7691 15.3125 30.4286 15.3125C31.2964 15.3125 32 16.04 32 16.9375C32 17.835 31.2964 18.5625 30.4286 18.5625C29.7691 18.5625 29.2045 18.1424 28.9714 17.5469H21V21.2031H25.0428C25.2759 20.6076 25.8405 20.1875 26.5 20.1875C27.3679 20.1875 28.0714 20.915 28.0714 21.8125C28.0714 22.71 27.3679 23.4375 26.5 23.4375C25.8405 23.4375 25.2759 23.0174 25.0428 22.4219H21V26.0781H24.4125C25.4023 26.0781 26.3516 26.4847 27.0515 27.2085L29.0292 29.2536C29.2177 29.1708 29.4251 29.125 29.6429 29.125C30.5107 29.125 31.2143 29.8525 31.2143 30.75C31.2143 31.6475 30.5107 32.375 29.6429 32.375C28.775 32.375 28.0714 31.6475 28.0714 30.75C28.0714 30.5248 28.1157 30.3103 28.1958 30.1154L26.2181 28.0703C25.7392 27.5751 25.0897 27.2969 24.4125 27.2969H21V31.1562C21 32.7254 20.5531 34 19.0357 34C17.6165 34 16.4478 32.8879 16.3004 31.4559C16.0451 31.527 15.775 31.5625 15.5 31.5625C13.7665 31.5625 12.3571 30.1051 12.3571 28.3125C12.3571 27.9367 12.421 27.5711 12.5339 27.2359C11.0509 26.657 10 25.1742 10 23.4375C10 21.8176 10.9183 20.416 12.2491 19.766C11.8219 19.2125 11.5714 18.5117 11.5714 17.75C11.5714 16.191 12.6321 14.891 14.0464 14.5711C13.9679 14.2918 13.9286 13.9922 13.9286 13.6875C13.9286 12.1691 14.9402 10.8895 16.3004 10.534C16.4478 9.11211 17.6165 8 19.0357 8Z"
fill="#888B94"
/>
</svg>
)}
</button>
if (!shadowRoot || !portalContainer) {
return null;
}
{isImporting && (
<div className="mx-auto max-w-7xl md:px-0 lg:p-6">
<div className="relative isolate overflow-hidden bg-gray-900 px-6 pt-16 shadow-2xl lg:rounded-3xl md:pt-24 md:h-full sm:h-[100vh] lg:flex lg:gap-x-20 lg:px-24 lg:pt-0">
<div className="absolute z-20 top-0 inset-x-0 flex justify-center overflow-hidden pointer-events-none">
<div className="w-[108rem] flex-none flex justify-end">
<img
src={tailwindBg}
className="w-full flex-none max-w-none hidden dark:block"
decoding="async"
/>
</div>
return (
<div className="flex justify-end items-end min-h-screen h-full w-full">
<Toaster />
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger
className={`${hover || isPopoverOpen ? "opacity-100" : "opacity-75 pointer-events-none translate-x-3/4"} focus-within:translate-x-0 focus-visible:translate-x-0 size-12 hover:bg-black p-2 rounded-l-2xl transition bg-secondary border-2 border-border opacity-0 absolute flex bottom-20 items-center text-lg`}
>
<svg
className={`w-full h-full size-8 ${loading && "animate-spin"}`}
width={24}
height={24}
viewBox="0 0 42 42"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.0357 8C20.5531 8 21 9.27461 21 10.8438V16.3281H23.5536V14.2212C23.5536 13.1976 23.9468 12.216 24.6467 11.4922L25.0529 11.0721C24.9729 10.8772 24.9286 10.6627 24.9286 10.4375C24.9286 9.54004 25.6321 8.8125 26.5 8.8125C27.3679 8.8125 28.0714 9.54004 28.0714 10.4375C28.0714 11.335 27.3679 12.0625 26.5 12.0625C26.2822 12.0625 26.0748 12.0167 25.8863 11.9339L25.4801 12.354C25.0012 12.8492 24.7321 13.5209 24.7321 14.2212V16.3281H28.9714C29.2045 15.7326 29.7691 15.3125 30.4286 15.3125C31.2964 15.3125 32 16.04 32 16.9375C32 17.835 31.2964 18.5625 30.4286 18.5625C29.7691 18.5625 29.2045 18.1424 28.9714 17.5469H21V21.2031H25.0428C25.2759 20.6076 25.8405 20.1875 26.5 20.1875C27.3679 20.1875 28.0714 20.915 28.0714 21.8125C28.0714 22.71 27.3679 23.4375 26.5 23.4375C25.8405 23.4375 25.2759 23.0174 25.0428 22.4219H21V26.0781H24.4125C25.4023 26.0781 26.3516 26.4847 27.0515 27.2085L29.0292 29.2536C29.2177 29.1708 29.4251 29.125 29.6429 29.125C30.5107 29.125 31.2143 29.8525 31.2143 30.75C31.2143 31.6475 30.5107 32.375 29.6429 32.375C28.775 32.375 28.0714 31.6475 28.0714 30.75C28.0714 30.5248 28.1157 30.3103 28.1958 30.1154L26.2181 28.0703C25.7392 27.5751 25.0897 27.2969 24.4125 27.2969H21V31.1562C21 32.7254 20.5531 34 19.0357 34C17.6165 34 16.4478 32.8879 16.3004 31.4559C16.0451 31.527 15.775 31.5625 15.5 31.5625C13.7665 31.5625 12.3571 30.1051 12.3571 28.3125C12.3571 27.9367 12.421 27.5711 12.5339 27.2359C11.0509 26.657 10 25.1742 10 23.4375C10 21.8176 10.9183 20.416 12.2491 19.766C11.8219 19.2125 11.5714 18.5117 11.5714 17.75C11.5714 16.191 12.6321 14.891 14.0464 14.5711C13.9679 14.2918 13.9286 13.9922 13.9286 13.6875C13.9286 12.1691 14.9402 10.8895 16.3004 10.534C16.4478 9.11211 17.6165 8 19.0357 8Z"
fill={loading ? "gray" : "#fff"}
/>
</svg>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="left">
{userNotLoggedIn ? (
<>You need to login to use this extension.</>
) : (
<p>Add to supermemory.ai</p>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<PopoverContent
key={userNotLoggedIn ? "login" : "spaces"}
container={portalContainer}
>
{userNotLoggedIn ? (
<div className="flex flex-col gap-2">
<button
onClick={() => {
window.open("https://supermemory.ai/signin", "_blank");
}}
className="bg-slate-700 text-white p-2 rounded-md"
>
Login to supermemory.ai
</button>
</div>
<div className="mx-auto max-w-md text-center lg:py-12 lg:mx-0 lg:flex-auto lg:text-left">
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">
Your twitter bookmarks are being imported.
</h2>
<p className="mt-6 text-lg leading-8 text-gray-300">
This is done completely locally, so please keep this tab open!
</p>
) : (
<div className="flex flex-col gap-2">
<Select onValueChange={(value) => setSelectedSpace(value)}>
<SelectTrigger className="text-white">
<SelectValue
className="placeholder:font-semibold placeholder:text-white"
placeholder="Select a space"
/>
</SelectTrigger>
<SelectContent container={portalContainer}>
<SelectGroup>
<SelectLabel>Your spaces</SelectLabel>
{spacesOptions.map((space) => (
<SelectItem key={space.id} value={`${space.id}`}>
{space.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<Label className="text-slate-400" htmlFor="input-note">
Add a note
</Label>
<Textarea
value={webNote}
onChange={(e) => setWebNote(e.target.value)}
placeholder="Add a note"
className="text-white"
id="input-note"
/>
<button
onClick={async () => {
await sendUrlToAPI(selectedSpace ? [selectedSpace] : []);
}}
className="bg-slate-700 text-white p-2 rounded-md"
>
Add to{" "}
{selectedSpace
? spacesOptions.find((s) => s.id === parseInt(selectedSpace))
?.name
: "supermemory.ai"}
</button>
</div>
Imported {importedCount} bookmarks
</div>
</div>
)}
)}
</PopoverContent>
</Popover>
{(window.location.host === "twitter.com" ||
window.location.host === "x.com") && (
<button
onClick={() => {
chrome.runtime.sendMessage({ type: "batchImportAll" });
}}
className={`${hover && "opacity-100"} group p-2 rounded-l-2xl transition bg-slate-700 border border-white/20 opacity-0 size-4 h-[30vh] absolute flex bottom-6 items-center`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="#888B94"
className="size-4"
width={24}
height={24}
<Popover open={isPopover2Open} onOpenChange={setIsPopover2Open}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger
className={`${hover || isPopover2Open ? "opacity-100" : "opacity-75 pointer-events-none translate-x-3/4"} focus-within:translate-x-0 focus-visible:translate-x-0 size-12 hover:bg-black p-2 rounded-l-2xl transition bg-secondary border-2 border-border opacity-0 absolute flex bottom-6 items-center text-lg`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="size-8"
>
<path d="M12 1.5a.75.75 0 0 1 .75.75V7.5h-1.5V2.25A.75.75 0 0 1 12 1.5ZM11.25 7.5v5.69l-1.72-1.72a.75.75 0 0 0-1.06 1.06l3 3a.75.75 0 0 0 1.06 0l3-3a.75.75 0 1 0-1.06-1.06l-1.72 1.72V7.5h3.75a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-9a3 3 0 0 1-3-3v-9a3 3 0 0 1 3-3h3.75Z" />
</svg>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="left">
{userNotLoggedIn ? (
<>You need to login to use this extension.</>
) : (
<p>Import all twitter bookmarks</p>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<PopoverContent
key={userNotLoggedIn ? "login" : "spaces"}
container={portalContainer}
>
<path
fillRule="evenodd"
d="M10 2c-1.716 0-3.408.106-5.07.31C3.806 2.45 3 3.414 3 4.517V17.25a.75.75 0 0 0 1.075.676L10 15.082l5.925 2.844A.75.75 0 0 0 17 17.25V4.517c0-1.103-.806-2.068-1.93-2.207A41.403 41.403 0 0 0 10 2Z"
clipRule="evenodd"
/>
</svg>
</button>
{userNotLoggedIn ? (
<div className="flex flex-col gap-2">
<button
onClick={() => {
window.open("https://supermemory.ai/signin", "_blank");
}}
className="bg-slate-700 text-white p-2 rounded-md"
>
Login to supermemory.ai
</button>
</div>
) : (
<div className="flex flex-col gap-2">
<button
disabled={isImporting}
onClick={async () => {
setIsImporting(true);
chrome.runtime.sendMessage({ type: "batchImportAll" });
}}
className="bg-slate-700 text-white p-2 rounded-md disabled:bg-slate-700/50 disabled:pointer-events-none"
>
Import all Twitter bookmarks
</button>
{isImporting && (
<div className="flex items-center gap-2">
<p>Imported {importedCount} bookmarks</p>
<svg
className="animate-spin w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/>
</svg>
</div>
)}
{importDone && (
<p className="text-green-500">
All your twitter bookmarks have been imported!
</p>
)}
</div>
)}
</PopoverContent>
</Popover>
)}
</div>
);

View file

@ -1,7 +1,3 @@
/* @tailwind base;
@tailwind components;
@tailwind utilities; */
#supermemory-extension-host {
position: fixed;
bottom: 0;

View file

@ -8,10 +8,11 @@ const TAILWIND_URL =
"https://cdn.jsdelivr.net/npm/tailwindcss@^2.0/dist/tailwind.min.css";
const appendTailwindStyleData = (shadowRoot: ShadowRoot) => {
// Load and inject the local Tailwind CSS file into the shadow DOM
const styleSheet = document.createElement("style");
// Change this path to your actual Tailwind CSS file's location
fetch(TAILWIND_URL)
const path = chrome.runtime.getURL("../public/output.css");
fetch(path)
.then((response) => response.text())
.then((css) => {
styleSheet.textContent = css;
@ -44,7 +45,7 @@ function initial() {
rootDiv.id = "supermemory-extension-root";
shadowRoot.appendChild(rootDiv);
appendTailwindStyleLink(shadowRoot);
appendTailwindStyleData(shadowRoot);
const root = ReactDOM.createRoot(rootDiv);
@ -52,7 +53,9 @@ function initial() {
return data.jwt;
}) as Promise<string | undefined>;
jwt.then((token) => root.render(<ContentApp token={token} />));
jwt.then((token) =>
root.render(<ContentApp shadowRoot={shadowRoot} token={token} />),
);
}
window.addEventListener("message", (event) => {
@ -70,7 +73,7 @@ window.addEventListener("message", (event) => {
)
) {
console.log(
"JWT is only allowed to be used on localhost or anycontext.dhr.wtf",
"JWT is only allowed to be used on localhost or supermemory.ai",
);
return;
}

View file

@ -0,0 +1,25 @@
import * as React from "react";
import { cn } from "../../utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };

View file

@ -0,0 +1,26 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

View file

@ -0,0 +1,38 @@
"use client";
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "../../utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
container?: Element;
}
>(
(
{ className, container, align = "center", sideOffset = 4, ...props },
ref,
) => (
<PopoverPrimitive.Portal container={container}>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 mx-4 my-2 rounded-md border border-border bg-secondary p-4 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
),
);
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };

View file

@ -0,0 +1,162 @@
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "../../utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> & {
container?: Element;
}
>(({ className, children, position = "popper", container, ...props }, ref) => (
<SelectPrimitive.Portal container={container}>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};

View file

@ -0,0 +1,24 @@
import * as React from "react";
import { cn } from "../../utils";
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Textarea.displayName = "Textarea";
export { Textarea };

View file

@ -0,0 +1,129 @@
"use client";
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "../../utils";
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View file

@ -0,0 +1,35 @@
"use client";
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "../shadcn/toast";
import { useToast } from "../shadcn/use-toast";
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View file

@ -0,0 +1,30 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "../../utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border border-white/30 text-white bg-black px-3 py-1.5 text-sm shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View file

@ -0,0 +1,191 @@
"use client";
// Inspired by react-hot-toast library
import * as React from "react";
import type { ToastActionElement, ToastProps } from "./toast";
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
};
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
),
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast };

View file

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View file

@ -1,6 +1,6 @@
{
"manifest_version": 3,
"version": "2.0",
"version": "2.5",
"name": "supermemory",
"description": "An extension for https://supermemory.ai - an AI hub for all your bookmarks.",
"background": {
@ -22,6 +22,6 @@
"matches": ["<all_urls>"]
}
],
"permissions": ["webRequest", "storage"],
"host_permissions": ["https://x.com/*", "https://twitter.com/*"]
"permissions": ["webRequest", "storage", "contextMenus"],
"host_permissions": ["<all_urls>"]
}

View file

@ -2,19 +2,33 @@
"devDependencies": {
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.5",
"@webpack-cli/generators": "^3.0.7",
"extension": "latest",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"tailwindcss": "^3.4.1",
"typescript": "5.3.3",
"extension": "latest"
"webpack-cli": "^5.1.4"
},
"scripts": {
"dev": "extension dev",
"start": "extension start",
"build": "extension build"
"build": "extension build",
"style": "tailwindcss -i ../../packages/tailwind-config/globals.css -o ./public/output.css --watch"
},
"dependencies": {},
"name": "content-shadow-dom-tailwind",
"name": "supermemory-extension",
"private": true,
"version": "0.0.0"
"version": "0.0.0",
"dependencies": {
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.400.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7"
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +0,0 @@
const defaults = require("@repo/tailwind-config/tailwind.config");
/** @type {import('tailwindcss').Config} */
module.exports = {
...defaults,
};

View file

@ -0,0 +1 @@
module.exports = require("@repo/tailwind-config/tailwind.config");

View file

@ -1,15 +0,0 @@
"use client";
import { Button } from "@repo/ui/shadcn/button";
import React from "react";
import { signIn } from "../../server/auth";
function SignIn() {
return (
<Button className="mt-4" onClick={async () => await signIn("google")}>
Login with Google
</Button>
);
}
export default SignIn;

View file

@ -0,0 +1,72 @@
import React from "react";
import Markdown from "react-markdown";
function Page() {
return (
<div className="flex flex-col items-center justify-center mt-8">
<div className="max-w-3xl prose prose-invert">
<Markdown>
{`
Privacy Policy for Supermemory.ai
# Introduction
This Privacy Policy provides detailed information on the handling, storage, and protection of your personal information by Supermemory.ai - A web app and a browser extension developed and owned by Dhravya Shah and Supermemory team in 2024. The extension is designed to enhance your browsing experience by providing contextual information based on the content of the web pages you visit. This policy outlines the types of data collected by Supermemory.ai, how it is used, and the measures we take to protect your privacy.
Information Collection
Supermemory.ai collects the following types of information:
### In the web app
- **Personal Information**:
When you interact with Supermemory.ai, we may collect personal information including but not limited to your email address, session data, name, and profile picture. This information is collected to improve your user experience and to provide personalized services.
- **Usage Data**:
Supermemory.ai stores data related to your usage of the web app, including but not limited to the pages you visit, the features you use, and the time spent on the app. This data is used to improve the functionality of the app and to provide you with a better user experience.
- **Saved content**:
When you use the save feature, Supermemory.ai stores the content you save. This information is used to provide you with quick access to your saved content. This information is also stored in a vector database so that you can easily search for your saved content.
- **Chat and Query Data**:
When you use the chat feature or submit queries through the web app, we collect the data you provide. This information is used to provide a rich user experience with history of your past conversations.
### In the extension
- **Web Browsing Data**:
The extension has the capability to see all websites that users visit. However, Supermemory.ai only stores data when the user actively clicks on the extension button while browsing. The browser history is not recorded, ensuring that your browsing activities remain private.
- **Current page data**: Upon activation (click) by the user, Supermemory.ai stores data from the current HTML page. This data is used to provide relevant contextual information based on the content of the page you are viewing.
## Data Storage and Security
All collected data is securely stored in a SQLite database hosted on [Cloudflare D1](https://developers.cloudflare.com/d1/), [Cloudflare vectorize](https://developers.cloudflare.com/vectorize/), and [Cloudflare KV](https://developers.cloudflare.com/kv).
We employ industry-standard security measures to protect your information from unauthorized access, alteration, disclosure, or destruction. Despite our efforts, no method of transmission over the Internet or method of electronic storage is 100% secure. Therefore, while we strive to use commercially acceptable means to protect your personal information, we cannot guarantee its absolute security.
When you chat with the app, your queries may be sent to OpenAI GPT-4 API, Google Gemini API or other third-party services to provide you with relevant information. These services may store your queries and responses for training purposes.
## Use of Information
Supermemory.ai uses the collected information for the following purposes:
To provide and improve the functionality of the web app and the extension.
To offer personalized user experiences.
To communicate with users regarding updates, support, and promotional offers, if consented.
To ensure the security of our services and to detect, prevent, or address technical issues.
Sharing of Information
Supermemory.ai does not sell, trade, or rent users' personal identification information to others. We may share generic aggregated demographic information not linked to any personal identification information regarding visitors and users with our business partners, trusted affiliates, and advertisers for the purposes outlined above.
# Your Privacy Rights
You have the right to access, update, or delete your personal information that we hold. If you wish to exercise these rights, please contact us at the details provided below.
## Changes to This Privacy Policy
Supermemory.ai reserves the right to update this privacy policy at any time. When we do, we will post a notification on our website and update the date at the top of this page. We encourage users to frequently check this page for any changes to stay informed about how we are protecting the personal information we collect. Your continued use of the service after the posting of changes to this policy will be deemed your acceptance of those changes.
# Contact Us
If you have any questions about this Privacy Policy, the practices of this site, or your dealings with this site, please contact us at:
Email: dhravyashah@gmail.com
This document was last updated on July 4, 2024.
`}
</Markdown>
</div>
</div>
);
}
export default Page;

View file

@ -6,15 +6,21 @@ import { Google } from "@repo/ui/components/icons";
import gradientStyle from "./_components/TextGradient/gradient.module.css";
import { cn } from "@repo/ui/lib/utils";
import { redirect } from "next/navigation";
import { toast } from "sonner";
export const runtime = "edge";
async function Signin() {
async function Signin({
searchParams,
}: {
searchParams: Record<string, string>;
}) {
const user = await auth();
if (user) {
await redirect("/home");
}
return (
<div className="flex relative font-geistSans overflow-hidden items-center justify-between min-h-screen">
<div className="relative w-full lg:w-1/2 flex items-center min-h-screen bg-page-gradient p-8 border-r-[1px] border-white/5">
@ -89,7 +95,7 @@ async function Signin() {
<div className="relative hidden w-0 lg:flex lg:w-1/2 flex-col items-center justify-center min-h-screen bg-page-gradient overflow-hidden">
<img
className="absolute inset-x-0 -top-20 opacity-15 "
src={"/images/landing-hero-left.webp"}
src={"/images/landing-hero.jpeg"}
width={1000}
height={1000}
alt="back bg"

View file

@ -0,0 +1,57 @@
import React from "react";
import Markdown from "react-markdown";
function Page() {
return (
<div className="flex flex-col items-center justify-center mt-8">
<div className="max-w-3xl prose prose-invert">
<Markdown>
{`
Terms of Service for Supermemory.ai
**Effective Date:** July 4, 2024
Welcome to Supermemory! By using our app, you agree to the following terms and conditions. Please read them carefully.
### 1. Acceptance of Terms
By accessing or using the Supermemory app, you agree to be bound by these Terms of Service and our Privacy Policy. If you do not agree with any part of these terms, you must not use the app.
### 2. Description of Service
Supermemory allows users to save information from various sources on the internet, organize it, and interact with it using AI. The service includes features such as:
- **Knowledge Canvas:** Arrange your saved information in a 2D canvas.
- **Writing Assistant:** Use our markdown editor to create content with AI assistance.
- **Data Collection:** Collect data from any website and bring it into your second brain.
- **Powerful Search:** Quickly find any saved information.
### 3. User Responsibilities
- **Account Security:** You are responsible for maintaining the confidentiality of your account and password.
- **Content:** You must ensure that any content you save or upload does not violate any laws or third-party rights.
- **Usage:** You agree not to misuse the app, including but not limited to, attempting to gain unauthorized access to the service or its related systems.
### 4. Intellectual Property
All content, trademarks, and data on the Supermemory app are the property of Supermemory or its licensors. You may not use any of this content without permission.
### 5. Privacy
Your use of the app is also governed by our Privacy Policy, which explains how we collect, use, and protect your information.
### 6. Termination
We reserve the right to suspend or terminate your access to the app at our discretion, without notice, for conduct that we believe violates these Terms of Service or is harmful to other users.
### 7. Limitation of Liability
Supermemory is provided "as is" without any warranties of any kind. We do not guarantee that the service will be uninterrupted or error-free. To the fullest extent permitted by law, Supermemory will not be liable for any damages arising from the use of the app.
### 8. Changes to Terms
We may update these Terms of Service from time to time. We will notify you of any changes by posting the new terms on our app. Your continued use of the app after such changes constitutes your acceptance of the new terms.
### 9. Contact Information
If you have any questions about these Terms of Service, please contact us at dhravyashah@gmail.com.
**By using Supermemory, you acknowledge that you have read, understood, and agree to be bound by these Terms of Service.**
`}
</Markdown>
</div>
</div>
);
}
export default Page;

View file

@ -242,7 +242,7 @@ function ChatWindow({
</AccordionTrigger>
{/* TODO: fade out content on the right side, the fade goes away when the user scrolls */}
<AccordionContent
className="flex items-center overflow-auto gap-4 relative max-w-3xl no-scrollbar"
className="flex items-center no-scrollbar overflow-auto gap-4 relative max-w-3xl no-scrollbar"
defaultChecked
>
{/* Loading state */}

View file

@ -36,6 +36,9 @@ function Page({
const [telegramUser, setTelegramUser] = useState<string | undefined>(
searchParams.telegramUser as string,
);
const [extensionInstalled, setExtensionInstalled] = useState<
string | undefined
>(searchParams.extension as string);
const { push } = useRouter();
@ -58,6 +61,10 @@ function Page({
linkTelegram();
}
if (extensionInstalled) {
toast.success("Extension installed successfully");
}
getSpaces().then((res) => {
if (res.success && res.data) {
setSpaces(res.data);
@ -72,7 +79,7 @@ function Page({
if (typeof window === "undefined") return;
window.postMessage({ token: token.data }, "*");
});
}, []);
}, [telegramUser]);
return (
<div className="max-w-3xl h-full justify-center flex mx-auto w-full flex-col px-2 md:px-0">

View file

@ -80,7 +80,7 @@ function QueryInput({
className="bg-transparent pt-2.5 text-base placeholder:text-[#9B9B9B] focus:text-gray-200 duration-200 tracking-[3%] outline-none resize-none w-full p-4"
placeholder="Ask your second brain..."
onKeyDown={(e) => {
if (e.key === "Enter") {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit(q, preparedSpaces);
setQ("");

View file

@ -109,7 +109,10 @@ function Menu() {
const handleSubmit = async (content?: string, space?: string) => {
setDialogOpen(false);
toast.info("Creating memory...");
toast.info("Creating memory...", {
icon: <PlusCircleIcon className="w-4 h-4 text-white animate-spin" />,
duration: 7500,
});
if (!content || content.length === 0) {
toast.error("Content is required");
@ -124,7 +127,9 @@ function Menu() {
setContent("");
if (cont.success) {
toast.success("Memory created");
toast.success("Memory created", {
richColors: true,
});
} else {
toast.error(`Memory creation failed: ${cont.error}`);
}

View file

@ -73,8 +73,10 @@ function Hero() {
src="/images/landing-hero.jpeg"
alt="Landing page background"
draggable="false"
className="z-40 md:mt-[-40px] hidden sm:block h-full max-w-[70vw] mx-auto md:w-full select-none rounded-2xl"
className="z-40 md:mt-[-40px] hidden sm:block h-full max-w-[70vw] mx-auto md:w-full select-none px-5 !rounded-2xl"
style={{
borderRadius: "20px",
}}
/>
<div
className="absolute -z-10 left-0 top-[10%] h-32 w-[90%] overflow-x-hidden bg-[rgb(54,157,253)] bg-opacity-100 blur-[337.4px]"

View file

@ -22,7 +22,7 @@ export async function ensureAuth(req: NextRequest) {
.innerJoin(users, eq(users.id, sessions.userId))
.where(eq(sessions.sessionToken, token!));
if (!sessionData || sessionData.length < 0) {
if (!sessionData || sessionData.length === 0) {
return undefined;
}

View file

@ -84,11 +84,7 @@ const createMemoryFromAPI = async (input: {
const error = e as Error;
console.log("Error: ", error.message);
if (
error.message.includes(
"D1_ERROR: UNIQUE constraint failed: storedContent.baseUrl",
)
) {
if (error.message.includes("D1_ERROR: UNIQUE constraint failed:")) {
return {
success: false,
data: 0,
@ -204,9 +200,15 @@ export async function POST(req: NextRequest) {
message: "Failed to save document",
error: result.error,
}),
{ status: 500 },
{ status: 501 },
);
}
return new Response("ok", { status: 200 });
return new Response(
JSON.stringify({
message: "Document saved",
data: result.data,
}),
{ status: 200 },
);
}

View file

@ -1,13 +1,24 @@
// middleware.js
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-pathname", request.nextUrl.pathname);
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
return NextResponse.next({
request: {
headers: requestHeaders,
},
export function middleware(request: NextRequest) {
if (request.method === "OPTIONS") {
return new NextResponse(null, { headers: corsHeaders });
}
const response = NextResponse.next();
Object.entries(corsHeaders).forEach(([key, value]) => {
response.headers.set(key, value);
});
return response;
}
export const config = {
matcher: "/api/:path*",
};

View file

@ -18,7 +18,7 @@
"**/*.tsx",
".next/types/**/*.ts",
"../../packages/ui/",
"../../packages/shared-types/utils.ts",
"../../packages/shared-types",
"./components"
],
"exclude": ["node_modules/"]

View file

@ -47,6 +47,7 @@
"@auth/drizzle-adapter": "^1.1.0",
"@aws-sdk/client-s3": "^3.577.0",
"@aws-sdk/s3-request-presigner": "^3.577.0",
"@babel/plugin-transform-runtime": "^7.24.7",
"@cloudflare/puppeteer": "^0.0.11",
"@headlessui/react": "^2.0.4",
"@heroicons/react": "^2.1.4",

View file

@ -35,6 +35,39 @@
--radius: 0.5rem;
}
:host {
--foreground: rgba(179, 188, 197, 1);
--foreground-menu: rgba(106, 115, 125, 1);
--background: rgba(31, 36, 40, 1);
--secondary: rgb(45, 51, 58);
--primary: rgba(54, 157, 253, 1);
--border: rgba(62, 68, 73, 1);
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary-foreground: 0 0% 98%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
}
body {
color: var(--foreground);
background: var(--background);
@ -127,7 +160,6 @@ body {
);
}
.no-scrollbar {
/* For WebKit (Safari, Chrome, etc.) */
&::-webkit-scrollbar {

View file

@ -7,9 +7,9 @@ const config = {
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
"../../apps/extension/**/*.{js,ts,jsx,tsx}",
"../../packages/ui/**/*.{js,ts,jsx,tsx}", // Add the ui package
"../../packages/ui/shadcn/**/*.{js,ts,jsx,tsx}", // Add the ui package
"./content/**/*.{js,ts,jsx,tsx}",
"./ui/**/*.{js,ts,jsx,tsx}",
],
prefix: "",
theme: {