mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-07 02:58:11 +00:00
Merge branch 'main' into fix/clean_ups
This commit is contained in:
commit
abf9bf0882
42 changed files with 1913 additions and 785 deletions
26
README.md
26
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 :)
|
||||

|
||||
> [!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
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
You can also use it to import all your twitter bookmarks!
|
||||

|
||||
<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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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[]),
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
18
apps/extension/components.json
Normal file
18
apps/extension/components.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/* @tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities; */
|
||||
|
||||
#supermemory-extension-host {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
25
apps/extension/content/ui/shadcn/input.tsx
Normal file
25
apps/extension/content/ui/shadcn/input.tsx
Normal 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 };
|
||||
26
apps/extension/content/ui/shadcn/label.tsx
Normal file
26
apps/extension/content/ui/shadcn/label.tsx
Normal 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 };
|
||||
38
apps/extension/content/ui/shadcn/popover.tsx
Normal file
38
apps/extension/content/ui/shadcn/popover.tsx
Normal 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 };
|
||||
162
apps/extension/content/ui/shadcn/select.tsx
Normal file
162
apps/extension/content/ui/shadcn/select.tsx
Normal 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,
|
||||
};
|
||||
24
apps/extension/content/ui/shadcn/textarea.tsx
Normal file
24
apps/extension/content/ui/shadcn/textarea.tsx
Normal 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 };
|
||||
129
apps/extension/content/ui/shadcn/toast.tsx
Normal file
129
apps/extension/content/ui/shadcn/toast.tsx
Normal 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,
|
||||
};
|
||||
35
apps/extension/content/ui/shadcn/toaster.tsx
Normal file
35
apps/extension/content/ui/shadcn/toaster.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
30
apps/extension/content/ui/shadcn/tooltip.tsx
Normal file
30
apps/extension/content/ui/shadcn/tooltip.tsx
Normal 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 };
|
||||
191
apps/extension/content/ui/shadcn/use-toast.ts
Normal file
191
apps/extension/content/ui/shadcn/use-toast.ts
Normal 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 };
|
||||
6
apps/extension/content/utils.ts
Normal file
6
apps/extension/content/utils.ts
Normal 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));
|
||||
}
|
||||
|
|
@ -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>"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -1,6 +0,0 @@
|
|||
const defaults = require("@repo/tailwind-config/tailwind.config");
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
...defaults,
|
||||
};
|
||||
1
apps/extension/tailwind.config.ts
Normal file
1
apps/extension/tailwind.config.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
module.exports = require("@repo/tailwind-config/tailwind.config");
|
||||
|
|
@ -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;
|
||||
72
apps/web/app/(auth)/privacy/page.tsx
Normal file
72
apps/web/app/(auth)/privacy/page.tsx
Normal 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;
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
57
apps/web/app/(auth)/tos/page.tsx
Normal file
57
apps/web/app/(auth)/tos/page.tsx
Normal 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;
|
||||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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*",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"../../packages/ui/",
|
||||
"../../packages/shared-types/utils.ts",
|
||||
"../../packages/shared-types",
|
||||
"./components"
|
||||
],
|
||||
"exclude": ["node_modules/"]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue