Web Extension with WXT

This commit is contained in:
Mahesh Sanikommmu 2025-08-20 18:55:34 -07:00
parent 6341d483a7
commit c9f0dcf969
18 changed files with 1268 additions and 410 deletions

26
apps/browser-extension/.gitignore vendored Normal file
View file

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.output
stats.html
stats-*.json
.wxt
web-ext.config.ts
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -0,0 +1 @@
## Supermemory Browser Extension

View file

@ -0,0 +1,84 @@
export default defineBackground(() => {
browser.runtime.onInstalled.addListener(() => {
browser.contextMenus.create({
id: 'save-to-supermemory',
title: 'Save to Supermemory',
contexts: ['selection', 'page', 'link'],
});
});
browser.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId === 'save-to-supermemory') {
if (tab?.id) {
try {
await browser.tabs.sendMessage(tab.id, {
action: 'saveMemory',
});
} catch (error) {
console.error('Failed to send message to content script:', error);
console.log('Content script may not be injected on this page');
}
}
}
});
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'saveMemory') {
(async () => {
try {
const result = await browser.storage.local.get(['bearerToken']);
const bearerToken = result.bearerToken;
//const backendURL = 'http://localhost:8787';
const backendURL = 'https://api.supermemory.ai';
if (!bearerToken) {
console.error('No bearer token found');
sendResponse({ success: false, error: 'No authentication token found' });
return;
}
const response = await fetch(`${backendURL}/v3/memories`, {
method: 'POST',
credentials: 'omit',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${bearerToken}`,
},
body: JSON.stringify({
containerTags: ['sm_project_default'],
content:
message.data.highlightedText +
'\n\n' +
message.data.html +
'\n\n' +
message.data.url,
metadata: { sm_source: 'consumer' },
}),
});
if (!response.ok) {
const errorData = await response.text();
console.error('API call failed:', response.status, errorData);
sendResponse({
success: false,
error: `API call failed: ${response.status}`,
});
return;
}
const data = await response.json();
console.log('Memory saved successfully:', data);
sendResponse({ success: true, data });
} catch (error) {
console.error('Error saving memory:', error);
sendResponse({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
})();
return true;
}
});
});

View file

@ -0,0 +1,212 @@
export default defineContentScript({
matches: ['<all_urls>'],
main() {
let currentToast: HTMLElement | null = null;
browser.runtime.onMessage.addListener(async (message) => {
if (message.action === 'showToast') {
showToast(message.state);
} else if (message.action === 'saveMemory') {
await saveMemory();
}
});
async function saveMemory() {
try {
showToast('loading');
const highlightedText = window.getSelection()?.toString() || '';
const url = window.location.href;
const html = document.documentElement.outerHTML;
const response = await browser.runtime.sendMessage({
action: 'saveMemory',
data: {
html,
highlightedText,
url,
},
});
console.log('Response from enxtension:', response);
if (response.success) {
showToast('success');
} else {
showToast('error');
}
} catch (error) {
console.error('Error saving memory:', error);
showToast('error');
}
}
function showToast(state: 'loading' | 'success' | 'error') {
if (currentToast) {
currentToast.remove();
}
const toast = document.createElement('div');
toast.id = 'supermemory-toast';
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 2147483647;
background: #ffffff;
border-radius: 9999px;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
color: #374151;
min-width: 200px;
max-width: 300px;
animation: slideIn 0.3s ease-out;
`;
if (!document.getElementById('supermemory-toast-styles')) {
const style = document.createElement('style');
style.id = 'supermemory-toast-styles';
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes fadeOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
const icon = document.createElement('div');
icon.style.cssText = `
width: 20px;
height: 20px;
flex-shrink: 0;
`;
const text = document.createElement('span');
text.style.cssText = `
font-weight: 500;
`;
if (state === 'loading') {
icon.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 6V2" stroke="#6366f1" stroke-width="2" stroke-linecap="round"/>
<path d="M12 22V18" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<path d="M20.49 8.51L18.36 6.38" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.7"/>
<path d="M5.64 17.64L3.51 15.51" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
<path d="M22 12H18" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.8"/>
<path d="M6 12H2" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<path d="M20.49 15.49L18.36 17.62" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.9"/>
<path d="M5.64 6.36L3.51 8.49" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
</svg>
`;
icon.style.animation = 'spin 1s linear infinite';
if (!document.getElementById('supermemory-spinner-styles')) {
const spinStyle = document.createElement('style');
spinStyle.id = 'supermemory-spinner-styles';
spinStyle.textContent = `
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`;
document.head.appendChild(spinStyle);
}
text.textContent = 'Adding to Memory...';
} else if (state === 'success') {
const iconUrl = browser.runtime.getURL('/icon-16.png');
icon.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />
`;
text.textContent = 'Added to Memory';
} else if (state === 'error') {
icon.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#ef4444"/>
<path d="M15 9L9 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 9L15 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;
text.textContent = 'Failed to save memory / Make sure you are logged in';
}
toast.appendChild(icon);
toast.appendChild(text);
document.body.appendChild(toast);
currentToast = toast;
if (state === 'success' || state === 'error') {
setTimeout(() => {
if (currentToast === toast) {
toast.style.animation = 'fadeOut 0.3s ease-out';
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
if (currentToast === toast) {
currentToast = null;
}
}, 300);
}
}, 3000);
}
}
document.addEventListener('keydown', async (event) => {
if (
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key === 'm'
) {
event.preventDefault();
await saveMemory();
}
});
window.addEventListener('message', (event) => {
if (event.source !== window) {
return;
}
const bearerToken = event.data.token;
if (bearerToken) {
if (
!(
window.location.hostname === 'localhost' ||
window.location.hostname === 'supermemory.ai' ||
window.location.hostname === 'app.supermemory.ai'
)
) {
console.log(
'Bearer token is only allowed to be used on localhost or supermemory.ai'
);
return;
}
chrome.storage.local.set({ bearerToken }, () => {});
}
});
},
});

View file

@ -0,0 +1,83 @@
.popup-container {
width: 320px;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #ffffff;
border-radius: 8px;
}
.header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-bottom: 1px solid #e5e7eb;
}
.header .logo {
width: 32px;
height: 32px;
flex-shrink: 0;
}
.header h1 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #000000;
}
.content {
padding: 16px;
}
.status {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
font-size: 14px;
color: #000000;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-indicator.signed-in {
background-color: #000000;
}
.status-indicator.signed-out {
background-color: #666666;
}
.sign-out-btn {
width: 100%;
padding: 8px 16px;
background-color: #000000;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
.sign-out-btn:hover {
background-color: #333333;
}
.instruction {
margin: 0;
font-size: 13px;
color: #666666;
line-height: 1.4;
}
.authenticated, .unauthenticated {
text-align: left;
}

View file

@ -0,0 +1,89 @@
import React, { useState, useEffect } from 'react';
import './App.css';
function App() {
const [userSignedIn, setUserSignedIn] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const checkAuthStatus = async () => {
try {
const result = await chrome.storage.local.get(['bearerToken']);
setUserSignedIn(!!result.bearerToken);
} catch (error) {
console.error('Error checking auth status:', error);
setUserSignedIn(false);
} finally {
setLoading(false);
}
};
checkAuthStatus();
}, []);
const handleSignOut = async () => {
try {
await chrome.storage.local.remove(['bearerToken']);
setUserSignedIn(false);
} catch (error) {
console.error('Error signing out:', error);
}
};
if (loading) {
return (
<div className="popup-container">
<div className="header">
<img src="/icon-48.png" alt="Supermemory" className="logo" />
<h1>Supermemory</h1>
</div>
<div className="content">
<div>Loading...</div>
</div>
</div>
);
}
return (
<div className="popup-container">
<div className="header">
<img src="/icon-48.png" alt="Supermemory" className="logo" />
<h1>Supermemory</h1>
</div>
<div className="content">
{userSignedIn ? (
<div className="authenticated">
<div className="status">
<span className="status-indicator signed-in"></span>
<span>Signed in</span>
</div>
<button className="sign-out-btn" onClick={handleSignOut}>
Sign Out
</button>
</div>
) : (
<div className="unauthenticated">
<div className="status">
<span className="status-indicator signed-out"></span>
<span>Not signed in</span>
</div>
<p className="instruction">
<a
onClick={() => {
chrome.tabs.create({
url: 'https://app.supermemory.ai/login',
});
}}
>
Login to Supermemory
</a>
.
</p>
</div>
)}
</div>
</div>
);
}
export default App;

View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Default Popup Title</title>
<meta name="manifest.type" content="browser_action" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.js';
import './style.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View file

@ -0,0 +1,69 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View file

@ -0,0 +1,29 @@
{
"name": "Supermemory",
"description": "supermemory",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "wxt --port 3001",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"compile": "tsc --noEmit",
"postinstall": "wxt prepare"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@types/chrome": "^0.1.4",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.3",
"@wxt-dev/module-react": "^1.1.3",
"typescript": "^5.8.3",
"wxt": "^0.20.6"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View file

@ -0,0 +1,8 @@
{
"extends": "./.wxt/tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": true,
"jsx": "react-jsx",
"types": ["chrome"]
}
}

View file

@ -0,0 +1,18 @@
import { defineConfig } from 'wxt';
// See https://wxt.dev/api/config.html
export default defineConfig({
modules: ['@wxt-dev/module-react'],
manifest: {
permissions: ['contextMenus', 'storage', 'scripting', 'activeTab'],
web_accessible_resources: [
{
resources: ['icon-16.png'],
matches: ['<all_urls>']
}
],
},
webExt: {
chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'],
},
});

File diff suppressed because it is too large Load diff

BIN
bun.lockb Executable file

Binary file not shown.

View file

@ -1,5 +1,5 @@
{
"name": "supermemory",
"name": "supermemory-app",
"private": true,
"scripts": {
"build": "turbo run build",