aboutsummaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorMaheshtheDev <[email protected]>2025-10-19 00:38:47 +0000
committerMaheshtheDev <[email protected]>2025-10-19 00:38:47 +0000
commit809eaa0d4961b2300b6394c45ba8197d9c0d109c (patch)
treee766c56ce82665d7f829f8f8fa1bae38f2852ce9 /apps
parentMerge pull request #472 from Mikethebot44/feature/show-memory-content-markdown (diff)
downloadsupermemory-809eaa0d4961b2300b6394c45ba8197d9c0d109c.tar.xz
supermemory-809eaa0d4961b2300b6394c45ba8197d9c0d109c.zip
feat(browser-extension): folder level x bookmarks import with project selection (#495)
Feature : Import folder level x bookmarks [Screen Recording 2025-10-17 at 1.37.52 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/user-attachments/thumbnails/15cd60ff-856e-4f29-8897-74ae3c869c87.mov" />](https://app.graphite.dev/user-attachments/video/15cd60ff-856e-4f29-8897-74ae3c869c87.mov)
Diffstat (limited to 'apps')
-rw-r--r--apps/browser-extension/entrypoints/background.ts25
-rw-r--r--apps/browser-extension/entrypoints/content/twitter.ts176
-rw-r--r--apps/browser-extension/utils/constants.ts2
-rw-r--r--apps/browser-extension/utils/twitter-import.ts50
-rw-r--r--apps/browser-extension/utils/twitter-utils.ts69
-rw-r--r--apps/browser-extension/utils/types.ts7
-rw-r--r--apps/browser-extension/utils/ui-components.ts260
-rw-r--r--apps/browser-extension/wxt.config.ts2
8 files changed, 573 insertions, 18 deletions
diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts
index 9d46a5e9..7461af37 100644
--- a/apps/browser-extension/entrypoints/background.ts
+++ b/apps/browser-extension/entrypoints/background.ts
@@ -1,4 +1,9 @@
-import { getDefaultProject, saveMemory, searchMemories } from "../utils/api"
+import {
+ getDefaultProject,
+ saveMemory,
+ searchMemories,
+ fetchProjects,
+} from "../utils/api"
import {
CONTAINER_TAGS,
CONTEXT_MENU_IDS,
@@ -169,6 +174,9 @@ export default defineBackground(() => {
// Handle Twitter import request
if (message.type === MESSAGE_TYPES.BATCH_IMPORT_ALL) {
const importConfig: TwitterImportConfig = {
+ isFolderImport: message.isFolderImport,
+ bookmarkCollectionId: message.bookmarkCollectionId,
+ selectedProject: message.selectedProject,
onProgress: sendMessageToCurrentTab,
onComplete: sendImportDoneMessage,
onError: async (error: Error) => {
@@ -249,6 +257,21 @@ export default defineBackground(() => {
})()
return true
}
+
+ if (message.action === MESSAGE_TYPES.FETCH_PROJECTS) {
+ ;(async () => {
+ try {
+ const projects = await fetchProjects()
+ sendResponse({ success: true, data: projects })
+ } catch (error) {
+ sendResponse({
+ success: false,
+ error: error instanceof Error ? error.message : "Unknown error",
+ })
+ }
+ })()
+ return true
+ }
},
)
})
diff --git a/apps/browser-extension/entrypoints/content/twitter.ts b/apps/browser-extension/entrypoints/content/twitter.ts
index 15c6ec50..8e01e2db 100644
--- a/apps/browser-extension/entrypoints/content/twitter.ts
+++ b/apps/browser-extension/entrypoints/content/twitter.ts
@@ -5,7 +5,26 @@ import {
POSTHOG_EVENT_KEY,
} from "../../utils/constants"
import { trackEvent } from "../../utils/posthog"
-import { createTwitterImportButton, DOMUtils } from "../../utils/ui-components"
+import {
+ createTwitterImportButton,
+ createProjectSelectionModal,
+ DOMUtils,
+} from "../../utils/ui-components"
+
+async function loadSpaceGroteskFonts(): Promise<void> {
+ if (document.getElementById("supermemory-modal-styles")) {
+ return Promise.resolve()
+ }
+
+ const style = document.createElement("style")
+ style.id = "supermemory-modal-styles"
+ style.textContent = `
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:[email protected]&display=swap');
+ `
+ document.head.appendChild(style)
+
+ await document.fonts.ready
+}
export function initializeTwitter() {
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
@@ -16,6 +35,7 @@ export function initializeTwitter() {
if (window.location.pathname === "/i/bookmarks") {
setTimeout(() => {
addTwitterImportButton()
+ addTwitterImportButtonForFolders()
}, 2000)
} else {
// Remove button if not on bookmarks page
@@ -26,7 +46,6 @@ export function initializeTwitter() {
}
function addTwitterImportButton() {
- // Only show the import button on the bookmarks page
if (window.location.pathname !== "/i/bookmarks") {
return
}
@@ -51,6 +70,46 @@ function addTwitterImportButton() {
document.body.appendChild(button)
}
+function addTwitterImportButtonForFolders() {
+ if (window.location.pathname !== "/i/bookmarks") {
+ return
+ }
+
+ const targetElements = document.querySelectorAll(
+ ".css-175oi2r.r-1wtj0ep.r-16x9es5.r-1mmae3n.r-o7ynqc.r-6416eg.r-1ny4l3l.r-1loqt21",
+ )
+
+ targetElements.forEach((element) => {
+ addButtonToElement(element as HTMLElement)
+ })
+}
+
+function addButtonToElement(element: HTMLElement) {
+ if (element.querySelector("[data-supermemory-button]")) {
+ return
+ }
+
+ loadSpaceGroteskFonts()
+
+ 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)
+ }
+ })
+
+ button.setAttribute("data-supermemory-button", "true")
+
+ element.appendChild(button)
+ element.style.flexDirection = "row"
+ element.style.alignItems = "center"
+ element.style.justifyContent = "center"
+ element.style.gap = "10px"
+ element.style.padding = "10px"
+}
+
export function updateTwitterImportUI(message: {
type: string
importedMessage?: string
@@ -94,9 +153,120 @@ export function handleTwitterNavigation() {
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()
+
+ 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()
+ },
+ )
+
+ 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, [])
+ }
+ } catch (error) {
+ console.error("Error fetching projects:", error)
+ updateModalWithProjects(modal, [])
+ }
+ } catch (error) {
+ console.error("Error showing project selection modal:", error)
+ }
+}
+
+/**
+ * Updates the modal with fetched projects
+ * @param modal - The modal element
+ * @param projects - Array of projects to populate the dropdown
+ */
+function updateModalWithProjects(
+ modal: HTMLElement,
+ projects: Array<{ id: string; name: string; containerTag: string }>,
+) {
+ const select = modal.querySelector("#project-select") as HTMLSelectElement
+ if (!select) return
+
+ while (select.children.length > 1) {
+ select.removeChild(select.children[1])
+ }
+
+ if (projects.length === 0) {
+ const noProjectsOption = document.createElement("option")
+ noProjectsOption.value = ""
+ noProjectsOption.textContent = "No projects available"
+ noProjectsOption.disabled = true
+ select.appendChild(noProjectsOption)
+
+ const importButton = modal.querySelector(
+ "button:last-child",
+ ) as HTMLButtonElement
+ if (importButton) {
+ importButton.disabled = true
+ importButton.style.cssText = `
+ padding: 10px 16px;
+ border: none;
+ border-radius: 8px;
+ background: #d1d5db;
+ color: #9ca3af;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: not-allowed;
+ transition: all 0.2s ease;
+ `
+ }
+ } else {
+ projects.forEach((project) => {
+ const option = document.createElement("option")
+ option.value = project.id
+ option.textContent = project.name
+ option.dataset.containerTag = project.containerTag
+ select.appendChild(option)
+ })
}
-} \ No newline at end of file
+}
diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts
index ac286717..d459a0f0 100644
--- a/apps/browser-extension/utils/constants.ts
+++ b/apps/browser-extension/utils/constants.ts
@@ -35,6 +35,7 @@ export const ELEMENT_IDS = {
CHATGPT_INPUT_BAR_ELEMENT: "sm-chatgpt-input-bar-element",
CLAUDE_INPUT_BAR_ELEMENT: "sm-claude-input-bar-element",
T3_INPUT_BAR_ELEMENT: "sm-t3-input-bar-element",
+ PROJECT_SELECTION_MODAL: "sm-project-selection-modal",
} as const
/**
@@ -81,6 +82,7 @@ export const MESSAGE_TYPES = {
IMPORT_DONE: "sm-import-done",
GET_RELATED_MEMORIES: "sm-get-related-memories",
CAPTURE_PROMPT: "sm-capture-prompt",
+ FETCH_PROJECTS: "sm-fetch-projects",
} as const
export const CONTEXT_MENU_IDS = {
diff --git a/apps/browser-extension/utils/twitter-import.ts b/apps/browser-extension/utils/twitter-import.ts
index e68d6dbf..14dd1aae 100644
--- a/apps/browser-extension/utils/twitter-import.ts
+++ b/apps/browser-extension/utils/twitter-import.ts
@@ -2,12 +2,14 @@
* Twitter Bookmarks Import Module
* Handles the import process for Twitter bookmarks
*/
-
import { saveAllTweets } from "./api"
+import type { MemoryPayload } from "./types"
import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth"
import {
BOOKMARKS_URL,
+ BOOKMARK_COLLECTION_URL,
buildRequestVariables,
+ buildBookmarkCollectionVariables,
extractNextCursor,
getAllTweets,
type TwitterAPIResponse,
@@ -18,6 +20,13 @@ export type ImportProgressCallback = (message: string) => Promise<void>
export type ImportCompleteCallback = (totalImported: number) => Promise<void>
export interface TwitterImportConfig {
+ isFolderImport?: boolean
+ bookmarkCollectionId?: string
+ selectedProject?: {
+ id: string
+ name: string
+ containerTag: string
+ }
onProgress: ImportProgressCallback
onComplete: ImportCompleteCallback
onError: (error: Error) => Promise<void>
@@ -81,7 +90,11 @@ export class TwitterImporter {
* @param cursor - Pagination cursor for Twitter API
* @param totalImported - Number of tweets imported so far
*/
- private async batchImportAll(cursor = "", totalImported = 0, uniqueGroupId = "twitter_bookmarks"): Promise<void> {
+ private async batchImportAll(
+ cursor = "",
+ totalImported = 0,
+ uniqueGroupId = "twitter_bookmarks",
+ ): Promise<void> {
try {
// Use a local variable to track imported count
let importedCount = totalImported
@@ -99,10 +112,19 @@ export class TwitterImporter {
const headers = createTwitterAPIHeaders(tokens)
// Build API request with pagination
- const variables = buildRequestVariables(cursor)
+ const variables =
+ this.config.isFolderImport && this.config.bookmarkCollectionId
+ ? buildBookmarkCollectionVariables(this.config.bookmarkCollectionId)
+ : buildRequestVariables(cursor)
const urlWithCursor = cursor
- ? `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
- : BOOKMARKS_URL
+ ? `${
+ this.config.isFolderImport && this.config.bookmarkCollectionId
+ ? `${BOOKMARK_COLLECTION_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
+ : BOOKMARKS_URL
+ }&variables=${encodeURIComponent(JSON.stringify(variables))}`
+ : this.config.isFolderImport && this.config.bookmarkCollectionId
+ ? `${BOOKMARK_COLLECTION_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
+ : `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
const response = await fetch(urlWithCursor, {
method: "GET",
@@ -140,14 +162,20 @@ export class TwitterImporter {
retweets: tweet.retweet_count || 0,
sm_internal_group_id: uniqueGroupId,
}
+ const containerTag =
+ this.config.selectedProject?.containerTag ||
+ "sm_project_twitter_bookmarks"
+
documents.push({
- containerTags: ["sm_project_twitter_bookmarks"],
+ containerTags: [containerTag],
content: `https://x.com/${tweet.user.screen_name}/status/${tweet.id_str}`,
metadata,
customId: tweet.id_str,
})
importedCount++
- await this.config.onProgress(`Imported ${importedCount} tweets, so far...`)
+ await this.config.onProgress(
+ `Imported ${importedCount} tweets, so far...`,
+ )
} catch (error) {
console.error("Error importing tweet:", error)
}
@@ -167,13 +195,15 @@ export class TwitterImporter {
// Handle pagination
const instructions =
- data.data?.bookmark_timeline_v2?.timeline?.instructions
- const nextCursor = extractNextCursor(instructions || [])
+ data.data?.bookmark_timeline_v2?.timeline?.instructions ||
+ data.data?.bookmark_collection_timeline?.timeline?.instructions ||
+ []
+ const nextCursor = extractNextCursor(instructions)
console.log("Next cursor:", nextCursor)
console.log("Tweets length:", tweets.length)
- if (nextCursor && tweets.length > 0) {
+ if (nextCursor && tweets.length > 0 && !this.config.isFolderImport) {
await new Promise((resolve) => setTimeout(resolve, 1000)) // Rate limiting
await this.batchImportAll(nextCursor, importedCount, uniqueGroupId)
} else {
diff --git a/apps/browser-extension/utils/twitter-utils.ts b/apps/browser-extension/utils/twitter-utils.ts
index 7a7b86db..0f498a23 100644
--- a/apps/browser-extension/utils/twitter-utils.ts
+++ b/apps/browser-extension/utils/twitter-utils.ts
@@ -119,7 +119,19 @@ export interface Tweet {
export interface TwitterAPIResponse {
data: {
- bookmark_timeline_v2: {
+ bookmark_timeline_v2?: {
+ timeline: {
+ instructions: Array<{
+ type: string
+ entries?: Array<{
+ entryId: string
+ sortIndex: string
+ content: Record<string, unknown>
+ }>
+ }>
+ }
+ }
+ bookmark_collection_timeline?: {
timeline: {
instructions: Array<{
type: string
@@ -167,8 +179,49 @@ export const TWITTER_API_FEATURES = {
verified_phone_label_enabled: true,
}
+// Twitter API features configuration for BookmarkFolderTimeline
+export const TWITTER_BOOKMARK_FOLDER_FEATURES = {
+ rweb_video_screen_enabled: false,
+ payments_enabled: false,
+ profile_label_improvements_pcf_label_in_post_enabled: true,
+ responsive_web_profile_redirect_enabled: false,
+ rweb_tipjar_consumption_enabled: true,
+ verified_phone_label_enabled: false,
+ creator_subscriptions_tweet_preview_api_enabled: true,
+ responsive_web_graphql_timeline_navigation_enabled: true,
+ responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
+ premium_content_api_read_enabled: false,
+ communities_web_enable_tweet_community_results_fetch: true,
+ c9s_tweet_anatomy_moderator_badge_enabled: true,
+ responsive_web_grok_analyze_button_fetch_trends_enabled: false,
+ responsive_web_grok_analyze_post_followups_enabled: true,
+ responsive_web_jetfuel_frame: true,
+ responsive_web_grok_share_attachment_enabled: true,
+ articles_preview_enabled: true,
+ responsive_web_edit_tweet_api_enabled: true,
+ graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
+ view_counts_everywhere_api_enabled: true,
+ longform_notetweets_consumption_enabled: true,
+ responsive_web_twitter_article_tweet_consumption_enabled: true,
+ tweet_awards_web_tipping_enabled: false,
+ responsive_web_grok_show_grok_translated_post: true,
+ responsive_web_grok_analysis_button_from_backend: true,
+ creator_subscriptions_quote_tweet_preview_enabled: false,
+ freedom_of_speech_not_reach_fetch_enabled: true,
+ standardized_nudges_misinfo: true,
+ tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
+ longform_notetweets_rich_text_read_enabled: true,
+ longform_notetweets_inline_media_enabled: true,
+ responsive_web_grok_image_annotation_enabled: true,
+ responsive_web_grok_imagine_annotation_enabled: true,
+ responsive_web_grok_community_note_auto_translation_is_enabled: false,
+ responsive_web_enhance_cards_enabled: false,
+}
+
export const BOOKMARKS_URL = `https://x.com/i/api/graphql/xLjCVTqYWz8CGSprLU349w/Bookmarks?features=${encodeURIComponent(JSON.stringify(TWITTER_API_FEATURES))}`
+export const BOOKMARK_COLLECTION_URL = `https://x.com/i/api/graphql/I8Y9ni1dqP-ZSpwxqJQ--Q/BookmarkFolderTimeline?features=${encodeURIComponent(JSON.stringify(TWITTER_BOOKMARK_FOLDER_FEATURES))}`
+
/**
* Transform raw Twitter API response data into standardized Tweet format
*/
@@ -264,7 +317,9 @@ export function getAllTweets(data: TwitterAPIResponse): Tweet[] {
try {
const instructions =
- data.data?.bookmark_timeline_v2?.timeline?.instructions || []
+ data.data?.bookmark_timeline_v2?.timeline?.instructions ||
+ data.data?.bookmark_collection_timeline?.timeline?.instructions ||
+ []
for (const instruction of instructions) {
if (instruction.type === "TimelineAddEntries" && instruction.entries) {
@@ -375,3 +430,13 @@ export function buildRequestVariables(cursor?: string, count = 100) {
return variables
}
+
+/**
+ * Build Twitter API request variables for bookmark collection
+ */
+export function buildBookmarkCollectionVariables(bookmarkCollectionId: string) {
+ return {
+ bookmark_collection_id: bookmarkCollectionId,
+ includePromotedContent: true,
+ }
+}
diff --git a/apps/browser-extension/utils/types.ts b/apps/browser-extension/utils/types.ts
index 06a4ae72..f8d9efd8 100644
--- a/apps/browser-extension/utils/types.ts
+++ b/apps/browser-extension/utils/types.ts
@@ -11,6 +11,8 @@ export type ToastState = "loading" | "success" | "error"
* Message types for extension communication
*/
export interface ExtensionMessage {
+ isFolderImport?: boolean
+ bookmarkCollectionId?: string
action?: string
type?: string
data?: unknown
@@ -18,6 +20,11 @@ export interface ExtensionMessage {
importedMessage?: string
totalImported?: number
actionSource?: string
+ selectedProject?: {
+ id: string
+ name: string
+ containerTag: string
+ }
}
/**
diff --git a/apps/browser-extension/utils/ui-components.ts b/apps/browser-extension/utils/ui-components.ts
index dabe4974..1f280c7e 100644
--- a/apps/browser-extension/utils/ui-components.ts
+++ b/apps/browser-extension/utils/ui-components.ts
@@ -181,6 +181,7 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement {
align-items: center;
gap: 8px;
transition: all 0.2s ease;
+ font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`
const iconUrl = browser.runtime.getURL("/icon-16.png")
@@ -214,7 +215,6 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement {
iconButton.style.cssText = `
display: inline-flex;
align-items: flex-end;
- opacity: 0.7;
justify-content: center;
width: 20px;
height: 20px;
@@ -379,6 +379,264 @@ export function createT3InputBarElement(onClick: () => void): HTMLElement {
}
/**
+ * Creates a project selection modal for Twitter folder imports
+ * @param projects - Array of available projects
+ * @param onImport - Callback when import is clicked with selected project
+ * @param onClose - Callback when modal is closed
+ * @returns HTMLElement - The modal element
+ */
+export function createProjectSelectionModal(
+ projects: Array<{ id: string; name: string; containerTag: string }>,
+ onImport: (project: {
+ id: string
+ name: string
+ containerTag: string
+ }) => void,
+ onClose: () => void,
+): HTMLElement {
+ const modal = document.createElement("div")
+ modal.id = "sm-project-selection-modal"
+ modal.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 2147483648;
+ font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ `
+
+ const dialog = document.createElement("div")
+ dialog.style.cssText = `
+ background: #05070A;
+ border-radius: 12px;
+ padding: 24px;
+ max-width: 400px;
+ width: 90%;
+ box-shadow: 0 8px 32px rgba(5, 7, 10, 0.2);
+ position: relative;
+ `
+
+ const header = document.createElement("div")
+ header.style.cssText = `
+ margin-bottom: 20px;
+ `
+
+ const iconUrl = browser.runtime.getURL("/icon-16.png")
+ header.innerHTML = `
+ <div style="display: flex; flex-direction: column; gap: 8px;">
+ <h3 style="margin: 0; font-size: 16px; font-weight: 600; color: #ffffff; display: flex; align-items: center; gap: 8px;">
+ <img src="${iconUrl}" width="20" height="20" alt="Supermemory" style="border-radius: 4px;" />
+ Import to Supermemory
+ </h3>
+ <p style="margin: 0; font-size: 14px; font-weight: 400; color: #ffffff; opacity: 0.7;">
+ The project you want to import your bookmarks to.
+ </p>
+ </div>
+ `
+
+ const form = document.createElement("div")
+ form.style.cssText = `
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ `
+
+ const selectContainer = document.createElement("div")
+ selectContainer.style.cssText = `
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ `
+
+ const label = document.createElement("label")
+ label.style.cssText = `
+ font-size: 14px;
+ font-weight: 500;
+ color: #ffffff;
+ `
+ label.textContent = "Select Project to import"
+
+ const select = document.createElement("select")
+ select.id = "project-select"
+ select.style.cssText = `
+ padding: 12px 40px 12px 16px;
+ border: none;
+ border-radius: 12px;
+ font-size: 14px;
+ background: rgba(91, 126, 245, 0.04);
+ box-shadow: -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset, 2px 2px 1px 0 rgba(0, 0, 0, 0.50) inset;
+ color: #ffffff;
+ cursor: pointer;
+ transition: border-color 0.2s ease;
+ appearance: none;
+ background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right 16px center;
+ background-size: 16px;
+ font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ `
+ select.addEventListener("focus", () => {
+ select.style.borderColor = "#1A88FF"
+ })
+ select.addEventListener("blur", () => {
+ select.style.borderColor = "#374151"
+ })
+
+ // Add default option
+ const defaultOption = document.createElement("option")
+ defaultOption.value = ""
+ defaultOption.textContent = "Choose a project..."
+ defaultOption.disabled = true
+ defaultOption.selected = true
+ select.appendChild(defaultOption)
+
+ // Add project options
+ projects.forEach((project) => {
+ const option = document.createElement("option")
+ option.value = project.id
+ option.textContent = project.name
+ option.dataset.containerTag = project.containerTag
+ select.appendChild(option)
+ })
+
+ const buttonContainer = document.createElement("div")
+ buttonContainer.style.cssText = `
+ display: flex;
+ gap: 12px;
+ justify-content: flex-end;
+ margin-top: 8px;
+ `
+
+ const cancelButton = document.createElement("button")
+ cancelButton.textContent = "Cancel"
+ cancelButton.style.cssText = `
+ padding: 10px 16px;
+ color: #ffffff;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ border-radius: 10px;
+ border: none;
+ background: #05070A;
+ `
+ cancelButton.addEventListener("mouseenter", () => {
+ cancelButton.style.backgroundColor = "#f9fafb"
+ cancelButton.style.color = "#05070A"
+ })
+ cancelButton.addEventListener("mouseleave", () => {
+ cancelButton.style.backgroundColor = "#05070A"
+ cancelButton.style.color = "#ffffff"
+ })
+
+ const importButton = document.createElement("button")
+ importButton.textContent = "Import"
+ importButton.style.cssText = `
+ padding: 10px 16px;
+ border: none;
+ border-radius: 12px;
+ background: #d1d5db;
+ color: #9ca3af;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: not-allowed;
+ transition: all 0.2s ease;
+ `
+ importButton.disabled = true
+
+ // Handle project selection
+ select.addEventListener("change", () => {
+ const selectedOption = select.options[select.selectedIndex]
+ if (selectedOption.value) {
+ importButton.disabled = false
+ importButton.style.cssText = `
+ padding: 10px 16px;
+ border: none;
+ border-radius: 12px;
+ background: linear-gradient(203deg, #0FF0D2 -49.88%, #5BD3FB -33.14%, #1E0FF0 81.81%);
+ box-shadow: 1px 1px 2px 1px #1A88FF inset, 0 2px 10px 0 rgba(5, 1, 0, 0.20);
+ color: #ffffff;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ `
+ } else {
+ importButton.disabled = true
+ importButton.style.cssText = `
+ padding: 10px 16px;
+ border: none;
+ border-radius: 8px;
+ background: #d1d5db;
+ color: #9ca3af;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: not-allowed;
+ transition: all 0.2s ease;
+ `
+ }
+ })
+
+ // Handle import button click
+ importButton.addEventListener("click", () => {
+ const selectedOption = select.options[select.selectedIndex]
+ if (selectedOption.value) {
+ const selectedProject = {
+ id: selectedOption.value,
+ name: selectedOption.textContent,
+ containerTag: selectedOption.dataset.containerTag || "",
+ }
+ onImport(selectedProject)
+ }
+ })
+
+ // Handle cancel button click
+ cancelButton.addEventListener("click", onClose)
+
+ // Handle overlay click to close
+ modal.addEventListener("click", (e) => {
+ if (e.target === modal) {
+ onClose()
+ }
+ })
+
+ // Handle escape key
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ onClose()
+ }
+ }
+ document.addEventListener("keydown", handleKeyDown)
+
+ // Clean up event listener when modal is removed
+ const observer = new MutationObserver(() => {
+ if (!document.contains(modal)) {
+ document.removeEventListener("keydown", handleKeyDown)
+ observer.disconnect()
+ }
+ })
+ observer.observe(document.body, { childList: true, subtree: true })
+
+ selectContainer.appendChild(label)
+ selectContainer.appendChild(select)
+ form.appendChild(selectContainer)
+ buttonContainer.appendChild(cancelButton)
+ buttonContainer.appendChild(importButton)
+ form.appendChild(buttonContainer)
+
+ dialog.appendChild(header)
+ dialog.appendChild(form)
+ modal.appendChild(dialog)
+
+ return modal
+}
+
+/**
* Utility functions for DOM manipulation
*/
export const DOMUtils = {
diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts
index b536b853..aa77c88a 100644
--- a/apps/browser-extension/wxt.config.ts
+++ b/apps/browser-extension/wxt.config.ts
@@ -11,7 +11,7 @@ export default defineConfig({
manifest: {
name: "supermemory",
homepage_url: "https://supermemory.ai",
- version: "6.0.003",
+ version: "6.0.1",
permissions: ["contextMenus", "storage", "activeTab", "webRequest", "tabs"],
host_permissions: [
"*://x.com/*",