diff options
| author | MaheshtheDev <[email protected]> | 2025-10-19 00:38:47 +0000 |
|---|---|---|
| committer | MaheshtheDev <[email protected]> | 2025-10-19 00:38:47 +0000 |
| commit | 809eaa0d4961b2300b6394c45ba8197d9c0d109c (patch) | |
| tree | e766c56ce82665d7f829f8f8fa1bae38f2852ce9 /apps | |
| parent | Merge pull request #472 from Mikethebot44/feature/show-memory-content-markdown (diff) | |
| download | supermemory-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.ts | 25 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/content/twitter.ts | 176 | ||||
| -rw-r--r-- | apps/browser-extension/utils/constants.ts | 2 | ||||
| -rw-r--r-- | apps/browser-extension/utils/twitter-import.ts | 50 | ||||
| -rw-r--r-- | apps/browser-extension/utils/twitter-utils.ts | 69 | ||||
| -rw-r--r-- | apps/browser-extension/utils/types.ts | 7 | ||||
| -rw-r--r-- | apps/browser-extension/utils/ui-components.ts | 260 | ||||
| -rw-r--r-- | apps/browser-extension/wxt.config.ts | 2 |
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/*", |