mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
feat: fix interaction and improve Design for extension (#679)
### TL;DR Redesigned the browser extension UI with a dark theme and improved the Twitter bookmarks import experience with a new onboarding flow. ### What changed? - Added a new `RightArrow` icon component for UI navigation - Completely redesigned the popup UI with a dark theme and improved layout - Enhanced Twitter bookmarks import functionality: - Added an onboarding toast that appears the first time a user visits the bookmarks page - Implemented a persistent import intent system that automatically opens the import modal when navigating to the bookmarks page - Created a progress toast to show import status - Improved folder import UI - Updated the extension icon and added a new logo SVG - Improved the project selection modal with better styling
This commit is contained in:
parent
acdb422635
commit
3fa72c4ec7
11 changed files with 856 additions and 270 deletions
18
apps/browser-extension/components/icons.tsx
Normal file
18
apps/browser-extension/components/icons.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export function RightArrow({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width="10"
|
||||
height="11"
|
||||
viewBox="0 0 10 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<title>Right arrow</title>
|
||||
<path
|
||||
d="M-1.26511e-05 5.82399V4.53599H7.81199L3.90599 0.895994L4.78799 -6.19888e-06L9.79999 4.77399V5.54399L4.78799 10.332L3.90599 9.43599L7.78399 5.82399H-1.26511e-05Z"
|
||||
fill="#737373"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import { initializeT3 } from "./t3"
|
|||
import {
|
||||
handleTwitterNavigation,
|
||||
initializeTwitter,
|
||||
openImportModal,
|
||||
updateTwitterImportUI,
|
||||
} from "./twitter"
|
||||
|
||||
|
|
@ -29,6 +30,8 @@ export default defineContentScript({
|
|||
await saveMemory()
|
||||
} else if (message.action === MESSAGE_TYPES.OPEN_SEARCH_PANEL) {
|
||||
handleOpenSearchPanel(message.data as string)
|
||||
} else if (message.action === MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL) {
|
||||
await openImportModal()
|
||||
} else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
|
||||
updateTwitterImportUI(message)
|
||||
} else if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import {
|
|||
ELEMENT_IDS,
|
||||
MESSAGE_TYPES,
|
||||
POSTHOG_EVENT_KEY,
|
||||
STORAGE_KEYS,
|
||||
UI_CONFIG,
|
||||
} from "../../utils/constants"
|
||||
import { trackEvent } from "../../utils/posthog"
|
||||
import {
|
||||
createTwitterImportButton,
|
||||
createProjectSelectionModal,
|
||||
createSaveTweetElement,
|
||||
DOMUtils,
|
||||
|
|
@ -27,46 +28,105 @@ async function loadSpaceGroteskFonts(): Promise<void> {
|
|||
await document.fonts.ready
|
||||
}
|
||||
|
||||
export function initializeTwitter() {
|
||||
/**
|
||||
* Check if import intent is valid (exists and not expired)
|
||||
*/
|
||||
async function checkAndConsumeImportIntent(): Promise<boolean> {
|
||||
try {
|
||||
const result = await browser.storage.local.get(
|
||||
STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL,
|
||||
)
|
||||
const intentUntil = result[
|
||||
STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL
|
||||
] as number | undefined
|
||||
|
||||
if (intentUntil && Date.now() < intentUntil) {
|
||||
await browser.storage.local.remove(
|
||||
STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL,
|
||||
)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error("Error checking import intent:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if onboarding toast has been shown before
|
||||
*/
|
||||
async function hasOnboardingBeenShown(): Promise<boolean> {
|
||||
try {
|
||||
const result = await browser.storage.local.get(
|
||||
STORAGE_KEYS.TWITTER_BOOKMARKS_ONBOARDING_SEEN,
|
||||
)
|
||||
return !!result[STORAGE_KEYS.TWITTER_BOOKMARKS_ONBOARDING_SEEN]
|
||||
} catch (error) {
|
||||
console.error("Error checking onboarding status:", error)
|
||||
return true // Default to true to avoid showing toast on error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark onboarding toast as shown
|
||||
*/
|
||||
async function markOnboardingAsShown(): Promise<void> {
|
||||
try {
|
||||
await browser.storage.local.set({
|
||||
[STORAGE_KEYS.TWITTER_BOOKMARKS_ONBOARDING_SEEN]: true,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error marking onboarding as shown:", error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function initializeTwitter() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Initial setup
|
||||
if (window.location.pathname === "/i/bookmarks") {
|
||||
setTimeout(() => {
|
||||
addTwitterImportButton()
|
||||
addTwitterImportButtonForFolders()
|
||||
setTimeout(async () => {
|
||||
if (window.location.pathname === "/i/bookmarks") {
|
||||
await handleBookmarksPageLoad()
|
||||
}
|
||||
}, 2000)
|
||||
} else {
|
||||
// Remove button if not on bookmarks page
|
||||
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
|
||||
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
|
||||
}
|
||||
// Clean up any injected UI if navigating away
|
||||
removeAllTwitterUI()
|
||||
}
|
||||
}
|
||||
|
||||
function addTwitterImportButton() {
|
||||
/**
|
||||
* Handle what to show when user lands on bookmarks page
|
||||
*/
|
||||
async function handleBookmarksPageLoad() {
|
||||
if (window.location.pathname !== "/i/bookmarks") {
|
||||
return
|
||||
}
|
||||
|
||||
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
|
||||
addTwitterImportButtonForFolders() // Add buttons to bookmark folders
|
||||
|
||||
const hasIntent = await checkAndConsumeImportIntent()
|
||||
|
||||
if (hasIntent) {
|
||||
await openImportModal()
|
||||
return
|
||||
}
|
||||
|
||||
const button = createTwitterImportButton(async () => {
|
||||
try {
|
||||
await handleAllBookmarksImportClick()
|
||||
} catch (error) {
|
||||
console.error("Error starting import:", error)
|
||||
}
|
||||
})
|
||||
const onboardingShown = await hasOnboardingBeenShown()
|
||||
|
||||
document.body.appendChild(button)
|
||||
if (!onboardingShown) {
|
||||
await showOnboardingToast()
|
||||
await markOnboardingAsShown()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAllBookmarksImportClick() {
|
||||
/**
|
||||
* Opens the import modal and handles the import flow
|
||||
*/
|
||||
export async function openImportModal() {
|
||||
try {
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.FETCH_PROJECTS,
|
||||
|
|
@ -85,14 +145,13 @@ async function handleAllBookmarksImportClick() {
|
|||
await showAllBookmarksProjectModal(projects)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error handling all bookmarks import:", error)
|
||||
console.error("Error opening import modal:", error)
|
||||
await browser.runtime.sendMessage({
|
||||
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function showAllBookmarksProjectModal(
|
||||
projects: Array<{ id: string; name: string; containerTag: string }>,
|
||||
) {
|
||||
|
|
@ -124,6 +183,420 @@ async function showAllBookmarksProjectModal(
|
|||
document.body.appendChild(modal)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the one-time onboarding toast with progress bar
|
||||
*/
|
||||
async function showOnboardingToast() {
|
||||
await loadSpaceGroteskFonts()
|
||||
|
||||
// Remove any existing toast
|
||||
const existingToast = document.getElementById(
|
||||
ELEMENT_IDS.TWITTER_ONBOARDING_TOAST,
|
||||
)
|
||||
if (existingToast) {
|
||||
existingToast.remove()
|
||||
}
|
||||
|
||||
const duration = UI_CONFIG.ONBOARDING_TOAST_DURATION
|
||||
|
||||
// Create toast container
|
||||
const toast = document.createElement("div")
|
||||
toast.id = ELEMENT_IDS.TWITTER_ONBOARDING_TOAST
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 2147483647;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
min-width: 320px;
|
||||
max-width: 380px;
|
||||
box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18), 0 1.5px 6px 0 rgba(0,0,0,0.12);
|
||||
animation: smSlideInUp 0.3s ease-out;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
// Add keyframe animations if not already present
|
||||
if (!document.getElementById("supermemory-onboarding-toast-styles")) {
|
||||
const style = document.createElement("style")
|
||||
style.id = "supermemory-onboarding-toast-styles"
|
||||
style.textContent = `
|
||||
@keyframes smSlideInUp {
|
||||
from { transform: translateY(100%); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
@keyframes smFadeOut {
|
||||
from { transform: translateY(0); opacity: 1; }
|
||||
to { transform: translateY(100%); opacity: 0; }
|
||||
}
|
||||
@keyframes smProgressGrow {
|
||||
from { transform: scaleX(0); }
|
||||
to { transform: scaleX(1); }
|
||||
}
|
||||
@keyframes smPulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
|
||||
// Header with icon, text and close button
|
||||
const header = document.createElement("div")
|
||||
header.style.cssText =
|
||||
"display: flex; align-items: flex-start; gap: 12px; position: relative;"
|
||||
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png")
|
||||
const icon = document.createElement("img")
|
||||
icon.src = iconUrl
|
||||
icon.alt = "Supermemory"
|
||||
icon.style.cssText = "width: 24px; height: 24px; border-radius: 4px; flex-shrink: 0; margin-top: 2px;"
|
||||
|
||||
const textContainer = document.createElement("div")
|
||||
textContainer.style.cssText = "display: flex; flex-direction: column; gap: 4px; flex: 1;"
|
||||
|
||||
const title = document.createElement("span")
|
||||
title.style.cssText = "font-weight: 600; font-size: 14px; color: #111827;"
|
||||
title.textContent = "Import X/Twitter Bookmarks"
|
||||
|
||||
const description = document.createElement("span")
|
||||
description.style.cssText = "font-size: 13px; color: #6b7280; line-height: 1.4;"
|
||||
description.textContent =
|
||||
"You can import all your Twitter bookmarks to Supermemory with one click."
|
||||
|
||||
textContainer.appendChild(title)
|
||||
textContainer.appendChild(description)
|
||||
|
||||
// Close button
|
||||
const closeButton = document.createElement("button")
|
||||
closeButton.setAttribute("aria-label", "Close onboarding toast")
|
||||
closeButton.style.cssText = `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
color: #9ca3af;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
`
|
||||
closeButton.innerHTML = `
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
`
|
||||
closeButton.addEventListener("mouseenter", () => {
|
||||
closeButton.style.backgroundColor = "#f3f4f6"
|
||||
})
|
||||
closeButton.addEventListener("mouseleave", () => {
|
||||
closeButton.style.backgroundColor = "transparent"
|
||||
})
|
||||
closeButton.addEventListener("click", () => {
|
||||
dismissToast(toast)
|
||||
})
|
||||
|
||||
header.appendChild(icon)
|
||||
header.appendChild(textContainer)
|
||||
header.appendChild(closeButton)
|
||||
|
||||
// Action buttons
|
||||
const buttonsContainer = document.createElement("div")
|
||||
buttonsContainer.style.cssText = "display: flex; gap: 8px; margin-top: 4px;"
|
||||
|
||||
const importButton = document.createElement("button")
|
||||
importButton.style.cssText = `
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%);
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
font-family: inherit;
|
||||
`
|
||||
importButton.textContent = "Import now"
|
||||
importButton.addEventListener("mouseenter", () => {
|
||||
importButton.style.opacity = "0.9"
|
||||
})
|
||||
importButton.addEventListener("mouseleave", () => {
|
||||
importButton.style.opacity = "1"
|
||||
})
|
||||
importButton.addEventListener("click", async () => {
|
||||
dismissToast(toast)
|
||||
await openImportModal()
|
||||
})
|
||||
|
||||
const learnMoreButton = document.createElement("button")
|
||||
learnMoreButton.style.cssText = `
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #374151;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
font-family: inherit;
|
||||
`
|
||||
learnMoreButton.textContent = "Learn more"
|
||||
learnMoreButton.addEventListener("mouseenter", () => {
|
||||
learnMoreButton.style.backgroundColor = "#f9fafb"
|
||||
})
|
||||
learnMoreButton.addEventListener("mouseleave", () => {
|
||||
learnMoreButton.style.backgroundColor = "transparent"
|
||||
})
|
||||
learnMoreButton.addEventListener("click", () => {
|
||||
window.open(
|
||||
"https://docs.supermemory.ai/connectors/twitter",
|
||||
"_blank",
|
||||
)
|
||||
})
|
||||
|
||||
buttonsContainer.appendChild(importButton)
|
||||
buttonsContainer.appendChild(learnMoreButton)
|
||||
|
||||
// Progress bar container
|
||||
const progressBarContainer = document.createElement("div")
|
||||
progressBarContainer.setAttribute("role", "progressbar")
|
||||
progressBarContainer.setAttribute("aria-valuemin", "0")
|
||||
progressBarContainer.setAttribute("aria-valuemax", "100")
|
||||
progressBarContainer.setAttribute("aria-valuenow", "0")
|
||||
progressBarContainer.setAttribute("aria-label", "Onboarding toast auto-dismiss progress")
|
||||
progressBarContainer.style.cssText = `
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: #e5e7eb;
|
||||
`
|
||||
|
||||
const progressBar = document.createElement("div")
|
||||
progressBar.style.cssText = `
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #0ff0d2, #5bd3fb, #1e0ff0);
|
||||
transform-origin: left;
|
||||
animation: smProgressGrow ${duration}ms linear forwards;
|
||||
`
|
||||
|
||||
// Update progress bar ARIA value as animation progresses
|
||||
const startTime = Date.now()
|
||||
const updateProgress = () => {
|
||||
const elapsed = Date.now() - startTime
|
||||
const progress = Math.min(100, Math.round((elapsed / duration) * 100))
|
||||
progressBarContainer.setAttribute("aria-valuenow", String(progress))
|
||||
if (progress < 100) {
|
||||
requestAnimationFrame(updateProgress)
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(updateProgress)
|
||||
|
||||
progressBarContainer.appendChild(progressBar)
|
||||
|
||||
// Assemble toast
|
||||
toast.appendChild(header)
|
||||
toast.appendChild(buttonsContainer)
|
||||
toast.appendChild(progressBarContainer)
|
||||
|
||||
document.body.appendChild(toast)
|
||||
|
||||
// Auto-dismiss after duration
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(toast)) {
|
||||
dismissToast(toast)
|
||||
}
|
||||
}, duration)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the toast with animation
|
||||
*/
|
||||
function dismissToast(toast: HTMLElement) {
|
||||
toast.style.animation = "smFadeOut 0.3s ease-out forwards"
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(toast)) {
|
||||
toast.remove()
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all Twitter-specific injected UI
|
||||
*/
|
||||
function removeAllTwitterUI() {
|
||||
// Remove import button (legacy)
|
||||
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
|
||||
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
|
||||
}
|
||||
// Remove onboarding toast
|
||||
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_ONBOARDING_TOAST)) {
|
||||
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_ONBOARDING_TOAST)
|
||||
}
|
||||
// Remove import progress toast
|
||||
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST)) {
|
||||
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST)
|
||||
}
|
||||
// Remove any folder buttons
|
||||
document.querySelectorAll("[data-supermemory-button]").forEach((button) => {
|
||||
button.remove()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows or updates the import progress toast in the bottom-right
|
||||
*/
|
||||
function showOrUpdateImportProgressToast(message: string, isComplete = false) {
|
||||
let toast = document.getElementById(ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST)
|
||||
|
||||
if (!toast) {
|
||||
// Ensure animation styles are available
|
||||
if (!document.getElementById("supermemory-onboarding-toast-styles")) {
|
||||
const style = document.createElement("style")
|
||||
style.id = "supermemory-onboarding-toast-styles"
|
||||
style.textContent = `
|
||||
@keyframes smSlideInUp {
|
||||
from { transform: translateY(100%); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
@keyframes smFadeOut {
|
||||
from { transform: translateY(0); opacity: 1; }
|
||||
to { transform: translateY(100%); opacity: 0; }
|
||||
}
|
||||
@keyframes smPulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
|
||||
// Create new toast
|
||||
toast = document.createElement("div")
|
||||
toast.id = ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 2147483647;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
min-width: 280px;
|
||||
max-width: 360px;
|
||||
box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18), 0 1.5px 6px 0 rgba(0,0,0,0.12);
|
||||
animation: smSlideInUp 0.3s ease-out;
|
||||
`
|
||||
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png")
|
||||
const icon = document.createElement("img")
|
||||
icon.src = iconUrl
|
||||
icon.alt = "Supermemory"
|
||||
icon.id = "sm-import-progress-icon"
|
||||
icon.style.cssText =
|
||||
"width: 20px; height: 20px; border-radius: 4px; flex-shrink: 0; animation: smPulse 1.5s ease-in-out infinite;"
|
||||
|
||||
const textSpan = document.createElement("span")
|
||||
textSpan.id = "sm-import-progress-text"
|
||||
textSpan.style.cssText = "font-weight: 500; flex: 1;"
|
||||
textSpan.textContent = message
|
||||
|
||||
toast.appendChild(icon)
|
||||
toast.appendChild(textSpan)
|
||||
document.body.appendChild(toast)
|
||||
} else {
|
||||
// Update existing toast
|
||||
const textSpan = toast.querySelector(
|
||||
"#sm-import-progress-text",
|
||||
) as HTMLSpanElement
|
||||
if (textSpan) {
|
||||
textSpan.textContent = message
|
||||
}
|
||||
}
|
||||
|
||||
// Style for completion
|
||||
if (isComplete) {
|
||||
const icon = toast.querySelector(
|
||||
"#sm-import-progress-icon",
|
||||
) as HTMLImageElement
|
||||
if (icon) {
|
||||
icon.style.animation = "none"
|
||||
icon.style.opacity = "1"
|
||||
}
|
||||
|
||||
const textSpan = toast.querySelector(
|
||||
"#sm-import-progress-text",
|
||||
) as HTMLSpanElement
|
||||
if (textSpan) {
|
||||
textSpan.style.color = "#059669"
|
||||
}
|
||||
|
||||
// Auto-dismiss after 4 seconds on completion
|
||||
setTimeout(() => {
|
||||
const existingToast = document.getElementById(
|
||||
ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST,
|
||||
)
|
||||
if (existingToast) {
|
||||
dismissToast(existingToast)
|
||||
}
|
||||
}, 4000)
|
||||
}
|
||||
}
|
||||
|
||||
export function updateTwitterImportUI(message: {
|
||||
type: string
|
||||
importedMessage?: string
|
||||
totalImported?: number
|
||||
}) {
|
||||
if (message.type === MESSAGE_TYPES.IMPORT_UPDATE && message.importedMessage) {
|
||||
showOrUpdateImportProgressToast(message.importedMessage, false)
|
||||
}
|
||||
|
||||
if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
|
||||
showOrUpdateImportProgressToast(
|
||||
`✓ Imported ${message.totalImported} tweets!`,
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleTwitterNavigation() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (window.location.pathname === "/i/bookmarks") {
|
||||
addTwitterImportButtonForFolders()
|
||||
await handleBookmarksPageLoad()
|
||||
} else {
|
||||
removeAllTwitterUI()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds import buttons to bookmark folders
|
||||
*/
|
||||
function addTwitterImportButtonForFolders() {
|
||||
if (window.location.pathname !== "/i/bookmarks") {
|
||||
return
|
||||
|
|
@ -138,6 +611,9 @@ function addTwitterImportButtonForFolders() {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an import button to a bookmark folder element
|
||||
*/
|
||||
function addButtonToElement(element: HTMLElement) {
|
||||
if (element.querySelector("[data-supermemory-button]")) {
|
||||
return
|
||||
|
|
@ -148,9 +624,8 @@ function addButtonToElement(element: HTMLElement) {
|
|||
const button = createSaveTweetElement(async () => {
|
||||
const url = element.getAttribute("href")
|
||||
const bookmarkCollectionId = url?.split("/").pop()
|
||||
console.log("Bookmark collection ID:", bookmarkCollectionId)
|
||||
if (bookmarkCollectionId) {
|
||||
await showProjectSelectionModal(bookmarkCollectionId)
|
||||
await showFolderProjectSelectionModal(bookmarkCollectionId)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -164,132 +639,55 @@ function addButtonToElement(element: HTMLElement) {
|
|||
element.style.padding = "10px"
|
||||
}
|
||||
|
||||
export function updateTwitterImportUI(message: {
|
||||
type: string
|
||||
importedMessage?: string
|
||||
totalImported?: number
|
||||
}) {
|
||||
const importButton = document.getElementById(
|
||||
ELEMENT_IDS.TWITTER_IMPORT_BUTTON,
|
||||
)
|
||||
if (!importButton) return
|
||||
|
||||
const existingImg = importButton.querySelector("img")
|
||||
if (existingImg) {
|
||||
existingImg.remove()
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png")
|
||||
importButton.style.backgroundImage = `url("${iconUrl}")`
|
||||
importButton.style.backgroundRepeat = "no-repeat"
|
||||
importButton.style.backgroundSize = "20px 20px"
|
||||
importButton.style.backgroundPosition = "8px center"
|
||||
importButton.style.padding = "10px 16px 10px 32px"
|
||||
}
|
||||
|
||||
let textSpan = importButton.querySelector(
|
||||
"#sm-import-text",
|
||||
) as HTMLSpanElement
|
||||
if (!textSpan) {
|
||||
textSpan = document.createElement("span")
|
||||
textSpan.id = "sm-import-text"
|
||||
textSpan.style.cssText = "font-weight: 500; font-size: 14px;"
|
||||
importButton.appendChild(textSpan)
|
||||
}
|
||||
|
||||
if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
|
||||
textSpan.textContent = message.importedMessage || ""
|
||||
importButton.style.cursor = "default"
|
||||
}
|
||||
|
||||
if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
|
||||
textSpan.textContent = `✓ Imported ${message.totalImported} tweets!`
|
||||
textSpan.style.color = "#059669"
|
||||
|
||||
setTimeout(() => {
|
||||
textSpan.textContent = "Import Bookmarks"
|
||||
textSpan.style.color = ""
|
||||
importButton.style.cursor = "pointer"
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
export function handleTwitterNavigation() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (window.location.pathname === "/i/bookmarks") {
|
||||
addTwitterImportButton()
|
||||
addTwitterImportButtonForFolders()
|
||||
} else {
|
||||
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
|
||||
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
|
||||
}
|
||||
document.querySelectorAll("[data-supermemory-button]").forEach((button) => {
|
||||
button.remove()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the project selection modal for folder imports
|
||||
* @param bookmarkCollectionId - The ID of the bookmark collection to import
|
||||
*/
|
||||
async function showProjectSelectionModal(bookmarkCollectionId: string) {
|
||||
try {
|
||||
const modal = createProjectSelectionModal(
|
||||
[],
|
||||
async (selectedProject) => {
|
||||
modal.remove()
|
||||
async function showFolderProjectSelectionModal(bookmarkCollectionId: string) {
|
||||
await loadSpaceGroteskFonts()
|
||||
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
|
||||
isFolderImport: true,
|
||||
bookmarkCollectionId: bookmarkCollectionId,
|
||||
selectedProject: selectedProject,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error importing bookmarks:", error)
|
||||
}
|
||||
},
|
||||
() => {
|
||||
modal.remove()
|
||||
},
|
||||
)
|
||||
const modal = createProjectSelectionModal(
|
||||
[],
|
||||
async (selectedProject) => {
|
||||
modal.remove()
|
||||
|
||||
document.body.appendChild(modal)
|
||||
|
||||
try {
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.FETCH_PROJECTS,
|
||||
})
|
||||
|
||||
if (response.success && response.data) {
|
||||
const projects = response.data
|
||||
|
||||
if (projects.length === 0) {
|
||||
console.warn("No projects available for import")
|
||||
updateModalWithProjects(modal, [])
|
||||
} else {
|
||||
updateModalWithProjects(modal, projects)
|
||||
}
|
||||
} else {
|
||||
console.error("Failed to fetch projects:", response.error)
|
||||
updateModalWithProjects(modal, [])
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
|
||||
isFolderImport: true,
|
||||
bookmarkCollectionId: bookmarkCollectionId,
|
||||
selectedProject: selectedProject,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error importing bookmarks:", error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching projects:", error)
|
||||
},
|
||||
() => {
|
||||
modal.remove()
|
||||
},
|
||||
)
|
||||
|
||||
document.body.appendChild(modal)
|
||||
|
||||
try {
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.FETCH_PROJECTS,
|
||||
})
|
||||
|
||||
if (response.success && response.data) {
|
||||
const projects = response.data
|
||||
updateModalWithProjects(modal, projects)
|
||||
} else {
|
||||
console.error("Failed to fetch projects:", response.error)
|
||||
updateModalWithProjects(modal, [])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error showing project selection modal:", error)
|
||||
console.error("Error fetching projects:", error)
|
||||
updateModalWithProjects(modal, [])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the modal with fetched projects
|
||||
* @param modal - The modal element
|
||||
* @param projects - Array of projects to populate the dropdown
|
||||
*/
|
||||
function updateModalWithProjects(
|
||||
modal: HTMLElement,
|
||||
|
|
@ -316,10 +714,10 @@ function updateModalWithProjects(
|
|||
importButton.disabled = true
|
||||
importButton.style.cssText = `
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #d1d5db;
|
||||
color: #9ca3af;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useQueryClient } from "@tanstack/react-query"
|
|||
import { useEffect, useState } from "react"
|
||||
import "./App.css"
|
||||
import { validateAuthToken } from "../../utils/api"
|
||||
import { MESSAGE_TYPES } from "../../utils/constants"
|
||||
import { MESSAGE_TYPES, STORAGE_KEYS, UI_CONFIG } from "../../utils/constants"
|
||||
import {
|
||||
useDefaultProject,
|
||||
useProjects,
|
||||
|
|
@ -17,6 +17,7 @@ import {
|
|||
userData as userDataStorage,
|
||||
} from "../../utils/storage"
|
||||
import type { Project } from "../../utils/types"
|
||||
import { RightArrow } from "@/components/icons"
|
||||
|
||||
const Tooltip = ({
|
||||
children,
|
||||
|
|
@ -181,6 +182,11 @@ function App() {
|
|||
}
|
||||
}, [defaultProject, projects, setDefaultProjectMutation])
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: close space selector when tab changes
|
||||
useEffect(() => {
|
||||
setShowProjectSelector(false)
|
||||
}, [activeTab])
|
||||
|
||||
const handleSaveCurrentPage = async () => {
|
||||
setSaving(true)
|
||||
|
||||
|
|
@ -262,25 +268,51 @@ function App() {
|
|||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="w-80 p-0 font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif] bg-white rounded-lg relative overflow-hidden">
|
||||
<div className="flex items-center justify-between gap-3 p-2.5 border-b border-gray-200 relative">
|
||||
<img
|
||||
alt="supermemory"
|
||||
className="w-8 h-8 shrink-0"
|
||||
src="./dark-transparent.svg"
|
||||
style={{ width: "80%", height: "45px" }}
|
||||
/>
|
||||
<div
|
||||
className="w-80 p-0 font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif] rounded-lg relative overflow-hidden"
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
||||
boxShadow:
|
||||
"1.5px 1.5px 20px 0 rgba(0, 0, 0, 0.65), 1px 1.5px 2px 0 rgba(128, 189, 255, 0.07) inset, -0.5px -1.5px 4px 0 rgba(0, 35, 73, 0.40) inset",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
id="popup-header"
|
||||
className="flex items-center justify-between p-2.5 relative"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-8 h-8 shrink-0 rounded-[3.75px] overflow-hidden relative"
|
||||
style={{ boxShadow: "inset 0px 1px 3.75px 0px #000" }}
|
||||
>
|
||||
<img
|
||||
alt="supermemory"
|
||||
src="./icon-48.png"
|
||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[29px] h-[29px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[11px] font-medium text-[#737373] leading-normal">
|
||||
Your
|
||||
</span>
|
||||
<img
|
||||
alt="supermemory"
|
||||
src="./logo-fullmark.svg"
|
||||
className="h-[14.5px] w-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 text-black min-h-[300px] flex items-center justify-center">
|
||||
<div className="flex items-center gap-3 text-base text-black">
|
||||
<div className="p-4 min-h-[300px] flex items-center justify-center">
|
||||
<div className="flex items-center gap-3 text-sm text-[#737373]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="text-black"
|
||||
className="text-[#737373]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z">
|
||||
|
|
@ -294,7 +326,7 @@ function App() {
|
|||
</path>
|
||||
</svg>
|
||||
|
||||
<span className="font-semibold">Loading...</span>
|
||||
<span className="font-medium">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -303,26 +335,56 @@ function App() {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="w-80 p-0 font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif] bg-white rounded-lg relative overflow-hidden"
|
||||
className="w-80 font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif] rounded-lg relative overflow-hidden p-4"
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
||||
boxShadow:
|
||||
"1.5px 1.5px 20px 0 rgba(0, 0, 0, 0.65), 1px 1.5px 2px 0 rgba(128, 189, 255, 0.07) inset, -0.5px -1.5px 4px 0 rgba(0, 35, 73, 0.40) inset",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 p-2.5 relative">
|
||||
<div className="text-white text-lg font-semibold ml-2">supermemory</div>
|
||||
<div
|
||||
id="popup-header"
|
||||
className="flex items-center justify-between p-2.5 relative"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-8 h-8 shrink-0 rounded-[3.75px] overflow-hidden relative"
|
||||
style={{ boxShadow: "inset 0px 1px 3.75px 0px #000" }}
|
||||
>
|
||||
<img
|
||||
alt="supermemory"
|
||||
src="./icon-48.png"
|
||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[29px] h-[29px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[11px] font-medium text-[#737373] leading-normal">
|
||||
{(() => {
|
||||
const name =
|
||||
userData?.name?.split(" ")[0] ||
|
||||
userData?.email?.split("@")[0]
|
||||
if (!name) return "Your"
|
||||
return name.endsWith("s") ? `${name}'` : `${name}'s`
|
||||
})()}
|
||||
</span>
|
||||
<img
|
||||
alt="supermemory"
|
||||
src="./logo-fullmark.svg"
|
||||
className="h-[14.5px] w-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{userSignedIn && (
|
||||
<button
|
||||
className="bg-none border-none text-base cursor-pointer text-gray-500 p-1 rounded transition-colors duration-200"
|
||||
className="bg-transparent border-none cursor-pointer p-1 rounded transition-colors duration-200"
|
||||
onClick={handleSignOut}
|
||||
title="Logout"
|
||||
aria-label="Logout"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
width="19"
|
||||
height="18"
|
||||
viewBox="0 0 19 18"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
|
|
@ -330,15 +392,15 @@ function App() {
|
|||
<path
|
||||
d="M17 9H7.5M15 12L18 9L15 6M10 4V3C10 2.46957 9.78929 1.96086 9.41421 1.58579C9.03914 1.21071 8.53043 1 8 1H3C2.46957 1 1.96086 1.21071 1.58579 1.58579C1.21071 1.96086 1 2.46957 1 3V15C1 15.5304 1.21071 16.0391 1.58579 16.4142C1.96086 16.7893 2.46957 17 3 17H8C8.53043 17 9.03914 16.7893 9.41421 16.4142C9.78929 16.0391 10 15.5304 10 15V14"
|
||||
stroke="#FAFAFA"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4 min-h-[250px] pt-1">
|
||||
<div className="min-h-[250px] pt-1">
|
||||
{userSignedIn ? (
|
||||
<div className="text-left">
|
||||
{/* Tab Navigation */}
|
||||
|
|
@ -408,43 +470,119 @@ function App() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Selection */}
|
||||
<div className="mb-0">
|
||||
<button
|
||||
className="w-full bg-[#5B7EF50A] border-none p-4 cursor-pointer text-left rounded-xl flex justify-between items-center transition-colors duration-200 hover:bg-[#5B7EF520]"
|
||||
onClick={handleShowProjectSelector}
|
||||
type="button"
|
||||
style={{
|
||||
boxShadow:
|
||||
"2px 2px 2px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset",
|
||||
}}
|
||||
>
|
||||
{/* Space Selection */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between pl-1">
|
||||
<span className="text-sm font-medium text-[#737373]">
|
||||
Save to project:
|
||||
Save to Space
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-[120px]">
|
||||
{showProjectSelector && (
|
||||
<button
|
||||
id="close-space-selector"
|
||||
className="bg-transparent border-none cursor-pointer p-0 text-[#737373] hover:text-white transition-colors"
|
||||
onClick={() => setShowProjectSelector(false)}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<title>Close</title>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showProjectSelector ? (
|
||||
<div className="flex flex-col gap-1 max-h-[180px] overflow-y-auto">
|
||||
{loadingProjects ? (
|
||||
<div className="h-11 flex items-center justify-center text-sm text-[#737373]">
|
||||
Loading spaces...
|
||||
</div>
|
||||
) : (
|
||||
projects.map((project) => (
|
||||
<button
|
||||
id={`space-option-${project.id}`}
|
||||
className={`w-full h-11 flex items-center justify-between px-4 rounded-lg bg-transparent border-none cursor-pointer text-left transition-colors duration-200 hover:bg-[#5B7EF510] ${
|
||||
defaultProject?.id === project.id
|
||||
? "bg-[#5B7EF50A]"
|
||||
: ""
|
||||
}`}
|
||||
style={
|
||||
defaultProject?.id === project.id
|
||||
? {
|
||||
boxShadow:
|
||||
"2px 2px 1px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
key={project.id}
|
||||
onClick={() => handleProjectSelect(project)}
|
||||
type="button"
|
||||
>
|
||||
<span className="text-sm font-normal text-[rgba(255,255,255,0.94)] overflow-hidden text-ellipsis whitespace-nowrap tracking-tight">
|
||||
{project.name}
|
||||
</span>
|
||||
{defaultProject?.id === project.id && (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-white shrink-0"
|
||||
>
|
||||
<title>Selected</title>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
id="space-selector-trigger"
|
||||
className="w-full h-11 flex items-center justify-between px-4 rounded-lg bg-[#5B7EF50A] border-none cursor-pointer text-left transition-colors duration-200 hover:bg-[#5B7EF520]"
|
||||
onClick={handleShowProjectSelector}
|
||||
type="button"
|
||||
style={{
|
||||
boxShadow:
|
||||
"2px 2px 1px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset",
|
||||
}}
|
||||
>
|
||||
<span className="text-sm font-normal text-[rgba(255,255,255,0.94)] overflow-hidden text-ellipsis whitespace-nowrap tracking-tight">
|
||||
{defaultProject
|
||||
? defaultProject.name
|
||||
: "Default Project"}
|
||||
: "Select a space"}
|
||||
</span>
|
||||
<svg
|
||||
aria-label="Select project"
|
||||
className="text-white shrink-0 transition-transform duration-200 hover:text-gray-700 transform rotate-90"
|
||||
fill="none"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
className="text-white shrink-0"
|
||||
>
|
||||
<title>Select project</title>
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
<title>Expand</title>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Save Button at Bottom */}
|
||||
|
|
@ -496,7 +634,7 @@ function App() {
|
|||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
className="w-full p-4 bg-[#5B7EF50A] text-white border-none rounded-xl text-sm cursor-pointer flex items-center justify-start transition-colors duration-200 hover:bg-[#5B7EF520]"
|
||||
className="w-full p-4 bg-[#5B7EF50A] text-white border-none rounded-xl text-sm cursor-pointer flex items-start justify-start transition-colors duration-200 hover:bg-[#5B7EF520]"
|
||||
style={{
|
||||
boxShadow:
|
||||
"2px 2px 2px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset",
|
||||
|
|
@ -524,34 +662,79 @@ function App() {
|
|||
Import ChatGPT Memories
|
||||
</p>
|
||||
<p className="m-0 text-[14px] text-[#737373] leading-tight">
|
||||
open 'manage', save your memories to supermemory
|
||||
open 'manage' > save your memories to supermemory
|
||||
</p>
|
||||
</div>
|
||||
<RightArrow className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
className="w-full p-4 bg-[#5B7EF50A] text-white border-none rounded-xl text-sm cursor-pointer flex items-center justify-center transition-colors duration-200 outline-none appearance-none hover:bg-[#5B7EF520] focus:outline-none"
|
||||
className="w-full p-4 bg-[#5B7EF50A] text-white border-none rounded-xl text-sm cursor-pointer flex items-start justify-start transition-colors duration-200 outline-none appearance-none hover:bg-[#5B7EF520] focus:outline-none"
|
||||
style={{
|
||||
boxShadow:
|
||||
"2px 2px 2px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset",
|
||||
}}
|
||||
onClick={async () => {
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
})
|
||||
|
||||
const targetUrl = "https://x.com/i/bookmarks"
|
||||
|
||||
if (activeTab?.url === targetUrl) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
})
|
||||
|
||||
await chrome.tabs.create({
|
||||
url: targetUrl,
|
||||
})
|
||||
const isOnBookmarksPage =
|
||||
activeTab?.url?.includes("x.com/i/bookmarks") ||
|
||||
activeTab?.url?.includes("twitter.com/i/bookmarks")
|
||||
|
||||
if (isOnBookmarksPage && activeTab?.id) {
|
||||
try {
|
||||
await chrome.tabs.sendMessage(activeTab.id, {
|
||||
action: MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL,
|
||||
})
|
||||
} catch (error) {
|
||||
// Content script may not be loaded yet, fall back to intent-based approach
|
||||
console.error(
|
||||
"Failed to send message to content script:",
|
||||
error,
|
||||
)
|
||||
const intentExpiry =
|
||||
Date.now() + UI_CONFIG.IMPORT_INTENT_TTL
|
||||
await chrome.storage.local.set({
|
||||
[STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL]:
|
||||
intentExpiry,
|
||||
})
|
||||
await chrome.tabs.create({
|
||||
url: targetUrl,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const intentExpiry =
|
||||
Date.now() + UI_CONFIG.IMPORT_INTENT_TTL
|
||||
await chrome.storage.local.set({
|
||||
[STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL]:
|
||||
intentExpiry,
|
||||
})
|
||||
await chrome.tabs.create({
|
||||
url: targetUrl,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error opening Twitter import:", error)
|
||||
// Fallback: try to open the bookmarks page anyway
|
||||
try {
|
||||
await chrome.tabs.create({
|
||||
url: targetUrl,
|
||||
})
|
||||
} catch (fallbackError) {
|
||||
console.error(
|
||||
"Failed to open bookmarks page:",
|
||||
fallbackError,
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -570,9 +753,10 @@ function App() {
|
|||
Import X/Twitter Bookmarks
|
||||
</p>
|
||||
<p className="m-0 text-[14px] text-[#737373] leading-tight">
|
||||
Click on supermemory on top right to import bookmarks
|
||||
Opens import dialog automatically
|
||||
</p>
|
||||
</div>
|
||||
<RightArrow className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -588,7 +772,9 @@ function App() {
|
|||
</div>
|
||||
) : userData?.email ? (
|
||||
<>
|
||||
<span className="font-medium text-base">Email</span>
|
||||
<span className="font-medium text-base text-white">
|
||||
Email
|
||||
</span>
|
||||
<span
|
||||
className="text-sm text-[#525966] p-3 rounded-xl bg-[#5B7EF50A]"
|
||||
style={{
|
||||
|
|
@ -608,7 +794,7 @@ function App() {
|
|||
|
||||
{/* Chat Integration Section */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-base font-semibold mb-3">
|
||||
<h3 className="text-base font-semibold mb-3 text-white">
|
||||
Chat Integration
|
||||
</h3>
|
||||
<div className="flex items-center justify-between p-3 rounded-xl bg-[#5B7EF50A] mb-3">
|
||||
|
|
@ -654,53 +840,6 @@ function App() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showProjectSelector && (
|
||||
<div className="absolute inset-0 bg-white rounded-lg z-1000 shadow-xl flex flex-col">
|
||||
<div className="flex justify-between items-center p-4 border-b border-gray-200 text-base font-semibold text-black shrink-0">
|
||||
<span>Select the Project</span>
|
||||
<button
|
||||
className="bg-transparent border-none text-xl cursor-pointer text-gray-500 p-0 w-6 h-6 flex items-center justify-center hover:text-black"
|
||||
onClick={() => setShowProjectSelector(false)}
|
||||
type="button"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{loadingProjects ? (
|
||||
<div className="py-8 px-4 text-center text-gray-500 text-sm">
|
||||
Loading projects...
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
{projects.map((project) => (
|
||||
<button
|
||||
className={`flex justify-between items-center py-3 px-4 cursor-pointer transition-colors duration-200 border-b border-gray-100 bg-transparent border-none w-full text-left last:border-b-0 hover:bg-gray-50 ${
|
||||
defaultProject?.id === project.id ? "bg-blue-50" : ""
|
||||
}`}
|
||||
key={project.id}
|
||||
onClick={() => handleProjectSelect(project)}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex flex-col flex-1 gap-0.5">
|
||||
<span className="text-sm font-medium text-black wrap-break-word leading-tight">
|
||||
{project.name}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{project.documentCount} docs
|
||||
</span>
|
||||
</div>
|
||||
{defaultProject?.id === project.id && (
|
||||
<span className="text-blue-600 font-bold text-base">
|
||||
✓
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-2">
|
||||
|
|
@ -718,18 +857,18 @@ function App() {
|
|||
</div>
|
||||
) : (
|
||||
<div className="mb-8">
|
||||
<h2 className="m-0 mb-4 text-sm font-normal text-black leading-tight">
|
||||
<h2 className="m-0 mb-4 text-sm font-normal leading-tight">
|
||||
Login to unlock all chrome extension features
|
||||
</h2>
|
||||
|
||||
<ul className="list-none p-0 m-0 text-left">
|
||||
<li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold">
|
||||
<li className="py-1.5 text-sm text-[#737373] relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-[#737373] before:font-bold">
|
||||
Save any page to your supermemory
|
||||
</li>
|
||||
<li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold">
|
||||
<li className="py-1.5 text-sm text-[#737373] relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-[#737373] before:font-bold">
|
||||
Import all your Twitter / X Bookmarks
|
||||
</li>
|
||||
<li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold">
|
||||
<li className="py-1.5 text-sm text-[#737373] relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-[#737373] before:font-bold">
|
||||
Import your ChatGPT Memories
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -737,7 +876,7 @@ function App() {
|
|||
)}
|
||||
|
||||
<div className="mt-8">
|
||||
<p className="m-0 mb-4 text-sm text-gray-500">
|
||||
<p className="m-0 mb-4 text-sm text-[#737373]">
|
||||
Having trouble logging in?{" "}
|
||||
<button
|
||||
className="bg-transparent border-none text-blue-500 cursor-pointer underline text-sm p-0 hover:text-blue-700"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
border: 1px solid #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
|
|
@ -18,7 +19,6 @@
|
|||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 12 KiB |
15
apps/browser-extension/public/logo-fullmark.svg
Normal file
15
apps/browser-extension/public/logo-fullmark.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7 KiB |
|
|
@ -114,7 +114,7 @@ export async function validateAuthToken(): Promise<boolean> {
|
|||
/**
|
||||
* Get user data from storage
|
||||
*/
|
||||
export async function getUserData(): Promise<{ email?: string } | null> {
|
||||
export async function getUserData(): Promise<{ email?: string; name?: string } | null> {
|
||||
try {
|
||||
return (await userData.getValue()) || null
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ export const API_ENDPOINTS = {
|
|||
*/
|
||||
export const ELEMENT_IDS = {
|
||||
TWITTER_IMPORT_BUTTON: "sm-twitter-import-button",
|
||||
TWITTER_ONBOARDING_TOAST: "sm-twitter-onboarding-toast",
|
||||
TWITTER_IMPORT_PROGRESS_TOAST: "sm-twitter-import-progress-toast",
|
||||
SUPERMEMORY_TOAST: "sm-toast",
|
||||
SUPERMEMORY_SAVE_BUTTON: "sm-save-button",
|
||||
SAVE_TWEET_ELEMENT: "sm-save-tweet-element",
|
||||
|
|
@ -26,12 +28,22 @@ export const ELEMENT_IDS = {
|
|||
SELECTION_SEARCH_PANEL: "sm-selection-search-panel",
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Storage Keys for local
|
||||
*/
|
||||
export const STORAGE_KEYS = {
|
||||
TWITTER_BOOKMARKS_ONBOARDING_SEEN: "sm_twitter_bookmarks_onboarding_seen",
|
||||
TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL: "sm_twitter_bookmarks_import_intent_until",
|
||||
} as const
|
||||
|
||||
/**
|
||||
* UI Configuration
|
||||
*/
|
||||
export const UI_CONFIG = {
|
||||
BUTTON_SHOW_DELAY: 2000, // milliseconds
|
||||
TOAST_DURATION: 3000, // milliseconds
|
||||
ONBOARDING_TOAST_DURATION: 6000, // milliseconds (6 seconds for progress bar)
|
||||
IMPORT_INTENT_TTL: 2 * 60 * 1000, // 2 minutes TTL for import intent
|
||||
RATE_LIMIT_BASE_WAIT: 60000, // 1 minute
|
||||
PAGINATION_DELAY: 1000, // 1 second between requests
|
||||
AUTO_SEARCH_DEBOUNCE_DELAY: 1500, // milliseconds to wait after user stops typing
|
||||
|
|
@ -75,6 +87,7 @@ export const MESSAGE_TYPES = {
|
|||
FETCH_PROJECTS: "sm-fetch-projects",
|
||||
SEARCH_SELECTION: "sm-search-selection",
|
||||
OPEN_SEARCH_PANEL: "sm-open-search-panel",
|
||||
TWITTER_IMPORT_OPEN_MODAL: "sm-twitter-import-open-modal",
|
||||
} as const
|
||||
|
||||
export const CONTEXT_MENU_IDS = {
|
||||
|
|
|
|||
|
|
@ -545,10 +545,10 @@ export function createProjectSelectionModal(
|
|||
importButton.textContent = "Import"
|
||||
importButton.style.cssText = `
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
background: #d1d5db;
|
||||
color: #9ca3af;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
|
|
@ -577,10 +577,10 @@ export function createProjectSelectionModal(
|
|||
importButton.disabled = true
|
||||
importButton.style.cssText = `
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #d1d5db;
|
||||
color: #9ca3af;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default defineConfig({
|
|||
manifest: {
|
||||
name: "supermemory",
|
||||
homepage_url: "https://supermemory.ai",
|
||||
version: "6.0.105",
|
||||
version: "6.1.0",
|
||||
permissions: ["contextMenus", "storage", "activeTab", "webRequest", "tabs"],
|
||||
host_permissions: [
|
||||
"*://x.com/*",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue