diff options
Diffstat (limited to 'apps/browser-extension/utils')
| -rw-r--r-- | apps/browser-extension/utils/api.ts | 68 | ||||
| -rw-r--r-- | apps/browser-extension/utils/constants.ts | 18 | ||||
| -rw-r--r-- | apps/browser-extension/utils/memory-popup.ts | 48 | ||||
| -rw-r--r-- | apps/browser-extension/utils/posthog.ts | 50 | ||||
| -rw-r--r-- | apps/browser-extension/utils/query-client.ts | 8 | ||||
| -rw-r--r-- | apps/browser-extension/utils/query-hooks.ts | 22 | ||||
| -rw-r--r-- | apps/browser-extension/utils/route-detection.ts | 85 | ||||
| -rw-r--r-- | apps/browser-extension/utils/twitter-auth.ts | 54 | ||||
| -rw-r--r-- | apps/browser-extension/utils/twitter-import.ts | 126 | ||||
| -rw-r--r-- | apps/browser-extension/utils/twitter-utils.ts | 298 | ||||
| -rw-r--r-- | apps/browser-extension/utils/types.ts | 134 | ||||
| -rw-r--r-- | apps/browser-extension/utils/ui-components.ts | 268 |
12 files changed, 595 insertions, 584 deletions
diff --git a/apps/browser-extension/utils/api.ts b/apps/browser-extension/utils/api.ts index 2a4c838b..273f9152 100644 --- a/apps/browser-extension/utils/api.ts +++ b/apps/browser-extension/utils/api.ts @@ -1,27 +1,27 @@ /** * API service for supermemory browser extension */ -import { API_ENDPOINTS, STORAGE_KEYS } from "./constants" +import { API_ENDPOINTS, STORAGE_KEYS } from "./constants"; import { AuthenticationError, type MemoryPayload, type Project, type ProjectsResponse, SupermemoryAPIError, -} from "./types" +} from "./types"; /** * Get bearer token from storage */ async function getBearerToken(): Promise<string> { - const result = await chrome.storage.local.get([STORAGE_KEYS.BEARER_TOKEN]) - const token = result[STORAGE_KEYS.BEARER_TOKEN] + const result = await chrome.storage.local.get([STORAGE_KEYS.BEARER_TOKEN]); + const token = result[STORAGE_KEYS.BEARER_TOKEN]; if (!token) { - throw new AuthenticationError("Bearer token not found") + throw new AuthenticationError("Bearer token not found"); } - return token + return token; } /** @@ -31,7 +31,7 @@ async function makeAuthenticatedRequest<T>( endpoint: string, options: RequestInit = {}, ): Promise<T> { - const token = await getBearerToken() + const token = await getBearerToken(); const response = await fetch(`${API_ENDPOINTS.SUPERMEMORY_API}${endpoint}`, { ...options, @@ -41,19 +41,19 @@ async function makeAuthenticatedRequest<T>( "Content-Type": "application/json", ...options.headers, }, - }) + }); if (!response.ok) { if (response.status === 401) { - throw new AuthenticationError("Invalid or expired token") + throw new AuthenticationError("Invalid or expired token"); } throw new SupermemoryAPIError( `API request failed: ${response.statusText}`, response.status, - ) + ); } - return response.json() + return response.json(); } /** @@ -62,11 +62,11 @@ async function makeAuthenticatedRequest<T>( export async function fetchProjects(): Promise<Project[]> { try { const response = - await makeAuthenticatedRequest<ProjectsResponse>("/v3/projects") - return response.projects + await makeAuthenticatedRequest<ProjectsResponse>("/v3/projects"); + return response.projects; } catch (error) { - console.error("Failed to fetch projects:", error) - throw error + console.error("Failed to fetch projects:", error); + throw error; } } @@ -77,11 +77,11 @@ export async function getDefaultProject(): Promise<Project | null> { try { const result = await chrome.storage.local.get([ STORAGE_KEYS.DEFAULT_PROJECT, - ]) - return result[STORAGE_KEYS.DEFAULT_PROJECT] || null + ]); + return result[STORAGE_KEYS.DEFAULT_PROJECT] || null; } catch (error) { - console.error("Failed to get default project:", error) - return null + console.error("Failed to get default project:", error); + return null; } } @@ -92,10 +92,10 @@ export async function setDefaultProject(project: Project): Promise<void> { try { await chrome.storage.local.set({ [STORAGE_KEYS.DEFAULT_PROJECT]: project, - }) + }); } catch (error) { - console.error("Failed to set default project:", error) - throw error + console.error("Failed to set default project:", error); + throw error; } } @@ -107,11 +107,11 @@ export async function saveMemory(payload: MemoryPayload): Promise<unknown> { const response = await makeAuthenticatedRequest<unknown>("/v3/documents", { method: "POST", body: JSON.stringify(payload), - }) - return response + }); + return response; } catch (error) { - console.error("Failed to save memory:", error) - throw error + console.error("Failed to save memory:", error); + throw error; } } @@ -123,11 +123,11 @@ export async function searchMemories(query: string): Promise<unknown> { const response = await makeAuthenticatedRequest<unknown>("/v4/search", { method: "POST", body: JSON.stringify({ q: query, include: { relatedMemories: true } }), - }) - return response + }); + return response; } catch (error) { - console.error("Failed to search memories:", error) - throw error + console.error("Failed to search memories:", error); + throw error; } } @@ -150,13 +150,13 @@ export async function saveAllTweets( }, }), }, - ) - return response + ); + return response; } catch (error) { if (error instanceof SupermemoryAPIError && error.statusCode === 409) { // Skip if already exists (409 Conflict) - return + return; } - throw error + throw error; } } diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts index ac286717..39ea54a7 100644 --- a/apps/browser-extension/utils/constants.ts +++ b/apps/browser-extension/utils/constants.ts @@ -8,7 +8,7 @@ export const API_ENDPOINTS = { SUPERMEMORY_WEB: import.meta.env.PROD ? "https://app.supermemory.ai" : "http://localhost:3000", -} as const +} as const; /** * Storage Keys @@ -22,7 +22,7 @@ export const STORAGE_KEYS = { TWITTER_AUTH_TOKEN: "twitter-auth-token", DEFAULT_PROJECT: "sm-default-project", AUTO_SEARCH_ENABLED: "sm-auto-search-enabled", -} as const +} as const; /** * DOM Element IDs @@ -35,7 +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", -} as const +} as const; /** * UI Configuration @@ -49,7 +49,7 @@ export const UI_CONFIG = { OBSERVER_THROTTLE_DELAY: 300, // milliseconds between observer callback executions ROUTE_CHECK_INTERVAL: 2000, // milliseconds between route change checks API_REQUEST_TIMEOUT: 10000, // milliseconds for API request timeout -} as const +} as const; /** * Supported Domains @@ -60,7 +60,7 @@ export const DOMAINS = { CLAUDE: ["claude.ai"], T3: ["t3.chat"], SUPERMEMORY: ["localhost", "supermemory.ai", "app.supermemory.ai"], -} as const +} as const; /** * Container Tags @@ -68,7 +68,7 @@ export const DOMAINS = { export const CONTAINER_TAGS = { TWITTER_BOOKMARKS: "sm_project_twitter_bookmarks", DEFAULT_PROJECT: "sm_project_default", -} as const +} as const; /** * Message Types for extension communication @@ -81,11 +81,11 @@ export const MESSAGE_TYPES = { IMPORT_DONE: "sm-import-done", GET_RELATED_MEMORIES: "sm-get-related-memories", CAPTURE_PROMPT: "sm-capture-prompt", -} as const +} as const; export const CONTEXT_MENU_IDS = { SAVE_TO_SUPERMEMORY: "sm-save-to-supermemory", -} as const +} as const; export const POSTHOG_EVENT_KEY = { TWITTER_IMPORT_STARTED: "twitter_import_started", @@ -98,4 +98,4 @@ export const POSTHOG_EVENT_KEY = { CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED: "claude_chat_memories_auto_searched", CHATGPT_CHAT_MEMORIES_SEARCHED: "chatgpt_chat_memories_searched", CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED: "chatgpt_chat_memories_auto_searched", -} as const +} as const; diff --git a/apps/browser-extension/utils/memory-popup.ts b/apps/browser-extension/utils/memory-popup.ts index ba2d2a1c..b10d814b 100644 --- a/apps/browser-extension/utils/memory-popup.ts +++ b/apps/browser-extension/utils/memory-popup.ts @@ -4,13 +4,13 @@ */ export interface MemoryPopupConfig { - memoriesData: string - onClose: () => void - onRemove?: () => void + memoriesData: string; + onClose: () => void; + onRemove?: () => void; } export function createMemoryPopup(config: MemoryPopupConfig): HTMLElement { - const popup = document.createElement("div") + const popup = document.createElement("div"); popup.style.cssText = ` position: fixed; bottom: 80px; @@ -28,9 +28,9 @@ export function createMemoryPopup(config: MemoryPopupConfig): HTMLElement { z-index: 999999; display: none; overflow: hidden; - ` + `; - const header = document.createElement("div") + const header = document.createElement("div"); header.style.cssText = ` display: flex; justify-content: space-between; @@ -38,56 +38,56 @@ export function createMemoryPopup(config: MemoryPopupConfig): HTMLElement { padding: 8px; border-bottom: 1px solid #333; opacity: 0.8; - ` + `; header.innerHTML = ` <span style="font-size: 11px; font-weight: 600; letter-spacing: 0.5px;">INCLUDED MEMORIES</span> <div style="display: flex; gap: 4px;"> ${config.onRemove ? '<button id="remove-memories-btn" style="background: none; border: none; color: #ff4444; cursor: pointer; font-size: 14px; padding: 2px; border-radius: 2px;" title="Remove memories">✕</button>' : ""} <button id="close-popup-btn" style="background: none; border: none; color: white; cursor: pointer; font-size: 14px; padding: 2px; border-radius: 2px;">✕</button> </div> - ` + `; - const content = document.createElement("div") + const content = document.createElement("div"); content.style.cssText = ` padding: 8px; max-height: 300px; overflow-y: auto; line-height: 1.4; - ` - content.textContent = config.memoriesData + `; + content.textContent = config.memoriesData; - const closeBtn = header.querySelector("#close-popup-btn") - closeBtn?.addEventListener("click", config.onClose) + const closeBtn = header.querySelector("#close-popup-btn"); + closeBtn?.addEventListener("click", config.onClose); - const removeBtn = header.querySelector("#remove-memories-btn") + const removeBtn = header.querySelector("#remove-memories-btn"); if (removeBtn && config.onRemove) { - removeBtn.addEventListener("click", config.onRemove) + removeBtn.addEventListener("click", config.onRemove); } - popup.appendChild(header) - popup.appendChild(content) + popup.appendChild(header); + popup.appendChild(content); - return popup + return popup; } export function showMemoryPopup(popup: HTMLElement): void { - popup.style.display = "block" + popup.style.display = "block"; setTimeout(() => { if (popup.style.display === "block") { - hideMemoryPopup(popup) + hideMemoryPopup(popup); } - }, 10000) + }, 10000); } export function hideMemoryPopup(popup: HTMLElement): void { - popup.style.display = "none" + popup.style.display = "none"; } export function toggleMemoryPopup(popup: HTMLElement): void { if (popup.style.display === "none" || popup.style.display === "") { - showMemoryPopup(popup) + showMemoryPopup(popup); } else { - hideMemoryPopup(popup) + hideMemoryPopup(popup); } } diff --git a/apps/browser-extension/utils/posthog.ts b/apps/browser-extension/utils/posthog.ts index cdcdbc4e..8cf0b557 100644 --- a/apps/browser-extension/utils/posthog.ts +++ b/apps/browser-extension/utils/posthog.ts @@ -1,21 +1,21 @@ -import { PostHog } from "posthog-js/dist/module.no-external" -import { STORAGE_KEYS } from "./constants" +import { PostHog } from "posthog-js/dist/module.no-external"; +import { STORAGE_KEYS } from "./constants"; export async function identifyUser(posthog: PostHog): Promise<void> { - const stored = await chrome.storage.local.get([STORAGE_KEYS.USER_DATA]) - const userData = stored[STORAGE_KEYS.USER_DATA] + const stored = await chrome.storage.local.get([STORAGE_KEYS.USER_DATA]); + const userData = stored[STORAGE_KEYS.USER_DATA]; if (userData?.userId) { posthog.identify(userData.userId, { email: userData.email, name: userData.name, userId: userData.userId, - }) + }); } } -let posthogInstance: PostHog | null = null -let initializationPromise: Promise<PostHog> | null = null +let posthogInstance: PostHog | null = null; +let initializationPromise: Promise<PostHog> | null = null; export const POSTHOG_CONFIG = { api_host: "https://api.supermemory.ai/orange", @@ -24,40 +24,40 @@ export const POSTHOG_CONFIG = { persistence: "localStorage", capture_pageview: false, autocapture: false, -} as const +} as const; export async function getPostHogInstance(): Promise<PostHog> { if (posthogInstance) { - return posthogInstance + return posthogInstance; } if (initializationPromise) { - return initializationPromise + return initializationPromise; } - initializationPromise = initializePostHog() - return initializationPromise + initializationPromise = initializePostHog(); + return initializationPromise; } async function initializePostHog(): Promise<PostHog> { try { - const posthog = new PostHog() + const posthog = new PostHog(); if (!import.meta.env.WXT_POSTHOG_API_KEY) { - console.error("PostHog API key not configured") - throw new Error("PostHog API key not configured") + console.error("PostHog API key not configured"); + throw new Error("PostHog API key not configured"); } - posthog.init(import.meta.env.WXT_POSTHOG_API_KEY || "", POSTHOG_CONFIG) + posthog.init(import.meta.env.WXT_POSTHOG_API_KEY || "", POSTHOG_CONFIG); - await identifyUser(posthog) + await identifyUser(posthog); - posthogInstance = posthog - return posthog + posthogInstance = posthog; + return posthog; } catch (error) { - console.error("Failed to initialize PostHog:", error) - initializationPromise = null - throw error + console.error("Failed to initialize PostHog:", error); + initializationPromise = null; + throw error; } } @@ -66,9 +66,9 @@ export async function trackEvent( properties?: Record<string, unknown>, ): Promise<void> { try { - const posthog = await getPostHogInstance() - posthog.capture(eventName, properties) + const posthog = await getPostHogInstance(); + posthog.capture(eventName, properties); } catch (error) { - console.error(`Failed to track event ${eventName}:`, error) + console.error(`Failed to track event ${eventName}:`, error); } } diff --git a/apps/browser-extension/utils/query-client.ts b/apps/browser-extension/utils/query-client.ts index c1839691..dac7b639 100644 --- a/apps/browser-extension/utils/query-client.ts +++ b/apps/browser-extension/utils/query-client.ts @@ -1,7 +1,7 @@ /** * React Query configuration for supermemory browser extension */ -import { QueryClient } from "@tanstack/react-query" +import { QueryClient } from "@tanstack/react-query"; export const queryClient = new QueryClient({ defaultOptions: { @@ -11,9 +11,9 @@ export const queryClient = new QueryClient({ retry: (failureCount, error) => { // Don't retry on authentication errors if (error?.constructor?.name === "AuthenticationError") { - return false + return false; } - return failureCount < 3 + return failureCount < 3; }, refetchOnWindowFocus: false, }, @@ -21,4 +21,4 @@ export const queryClient = new QueryClient({ retry: 1, }, }, -}) +}); diff --git a/apps/browser-extension/utils/query-hooks.ts b/apps/browser-extension/utils/query-hooks.ts index 721a68ad..f1b683e7 100644 --- a/apps/browser-extension/utils/query-hooks.ts +++ b/apps/browser-extension/utils/query-hooks.ts @@ -1,21 +1,21 @@ /** * React Query hooks for supermemory API */ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { fetchProjects, getDefaultProject, saveMemory, searchMemories, setDefaultProject, -} from "./api" -import type { MemoryPayload } from "./types" +} from "./api"; +import type { MemoryPayload } from "./types"; // Query Keys export const queryKeys = { projects: ["projects"] as const, defaultProject: ["defaultProject"] as const, -} +}; // Projects Query export function useProjects(options?: { enabled?: boolean }) { @@ -24,7 +24,7 @@ export function useProjects(options?: { enabled?: boolean }) { queryFn: fetchProjects, staleTime: 5 * 60 * 1000, // 5 minutes enabled: options?.enabled ?? true, - }) + }); } // Default Project Query @@ -34,31 +34,31 @@ export function useDefaultProject(options?: { enabled?: boolean }) { queryFn: getDefaultProject, staleTime: 2 * 60 * 1000, // 2 minutes enabled: options?.enabled ?? true, - }) + }); } // Set Default Project Mutation export function useSetDefaultProject() { - const queryClient = useQueryClient() + const queryClient = useQueryClient(); return useMutation({ mutationFn: setDefaultProject, onSuccess: (_, project) => { - queryClient.setQueryData(queryKeys.defaultProject, project) + queryClient.setQueryData(queryKeys.defaultProject, project); }, - }) + }); } // Save Memory Mutation export function useSaveMemory() { return useMutation({ mutationFn: (payload: MemoryPayload) => saveMemory(payload), - }) + }); } // Search Memories Mutation export function useSearchMemories() { return useMutation({ mutationFn: (query: string) => searchMemories(query), - }) + }); } diff --git a/apps/browser-extension/utils/route-detection.ts b/apps/browser-extension/utils/route-detection.ts index a8a4714f..26787800 100644 --- a/apps/browser-extension/utils/route-detection.ts +++ b/apps/browser-extension/utils/route-detection.ts @@ -3,20 +3,20 @@ * Shared logic for detecting route changes across different AI chat platforms */ -import { UI_CONFIG } from "./constants" +import { UI_CONFIG } from "./constants"; export interface RouteDetectionConfig { - platform: string - selectors: string[] - reinitCallback: () => void - checkInterval?: number - observerThrottleDelay?: number + platform: string; + selectors: string[]; + reinitCallback: () => void; + checkInterval?: number; + observerThrottleDelay?: number; } export interface RouteDetectionCleanup { - observer: MutationObserver | null - urlCheckInterval: NodeJS.Timeout | null - observerThrottle: NodeJS.Timeout | null + observer: MutationObserver | null; + urlCheckInterval: NodeJS.Timeout | null; + observerThrottle: NodeJS.Timeout | null; } export function createRouteDetection( @@ -24,94 +24,97 @@ export function createRouteDetection( cleanup: RouteDetectionCleanup, ): void { if (cleanup.observer) { - cleanup.observer.disconnect() + cleanup.observer.disconnect(); } if (cleanup.urlCheckInterval) { - clearInterval(cleanup.urlCheckInterval) + clearInterval(cleanup.urlCheckInterval); } if (cleanup.observerThrottle) { - clearTimeout(cleanup.observerThrottle) - cleanup.observerThrottle = null + clearTimeout(cleanup.observerThrottle); + cleanup.observerThrottle = null; } - let currentUrl = window.location.href + let currentUrl = window.location.href; const checkForRouteChange = () => { if (window.location.href !== currentUrl) { - currentUrl = window.location.href - console.log(`${config.platform} route changed, re-initializing`) - setTimeout(config.reinitCallback, 1000) + currentUrl = window.location.href; + console.log(`${config.platform} route changed, re-initializing`); + setTimeout(config.reinitCallback, 1000); } - } + }; cleanup.urlCheckInterval = setInterval( checkForRouteChange, config.checkInterval || UI_CONFIG.ROUTE_CHECK_INTERVAL, - ) + ); cleanup.observer = new MutationObserver((mutations) => { if (cleanup.observerThrottle) { - return + return; } - let shouldRecheck = false + let shouldRecheck = false; mutations.forEach((mutation) => { if (mutation.type === "childList" && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { - const element = node as Element + const element = node as Element; for (const selector of config.selectors) { if ( element.querySelector?.(selector) || element.matches?.(selector) ) { - shouldRecheck = true - break + shouldRecheck = true; + break; } } } - }) + }); } - }) + }); if (shouldRecheck) { cleanup.observerThrottle = setTimeout(() => { try { - cleanup.observerThrottle = null - config.reinitCallback() + cleanup.observerThrottle = null; + config.reinitCallback(); } catch (error) { - console.error(`Error in ${config.platform} observer callback:`, error) + console.error( + `Error in ${config.platform} observer callback:`, + error, + ); } - }, config.observerThrottleDelay || UI_CONFIG.OBSERVER_THROTTLE_DELAY) + }, config.observerThrottleDelay || UI_CONFIG.OBSERVER_THROTTLE_DELAY); } - }) + }); try { cleanup.observer.observe(document.body, { childList: true, subtree: true, - }) + }); } catch (error) { - console.error(`Failed to set up ${config.platform} route observer:`, error) + console.error(`Failed to set up ${config.platform} route observer:`, error); if (cleanup.urlCheckInterval) { - clearInterval(cleanup.urlCheckInterval) + clearInterval(cleanup.urlCheckInterval); } - cleanup.urlCheckInterval = setInterval(checkForRouteChange, 1000) + cleanup.urlCheckInterval = setInterval(checkForRouteChange, 1000); } } export function cleanupRouteDetection(cleanup: RouteDetectionCleanup): void { if (cleanup.observer) { - cleanup.observer.disconnect() - cleanup.observer = null + cleanup.observer.disconnect(); + cleanup.observer = null; } if (cleanup.urlCheckInterval) { - clearInterval(cleanup.urlCheckInterval) - cleanup.urlCheckInterval = null + clearInterval(cleanup.urlCheckInterval); + cleanup.urlCheckInterval = null; } if (cleanup.observerThrottle) { - clearTimeout(cleanup.observerThrottle) - cleanup.observerThrottle = null + clearTimeout(cleanup.observerThrottle); + cleanup.observerThrottle = null; } } diff --git a/apps/browser-extension/utils/twitter-auth.ts b/apps/browser-extension/utils/twitter-auth.ts index 3dfc50f6..17cbe464 100644 --- a/apps/browser-extension/utils/twitter-auth.ts +++ b/apps/browser-extension/utils/twitter-auth.ts @@ -2,12 +2,12 @@ * Twitter Authentication Module * Handles token capture and storage for Twitter API access */ -import { STORAGE_KEYS } from "./constants" +import { STORAGE_KEYS } from "./constants"; export interface TwitterAuthTokens { - cookie: string - csrf: string - auth: string + cookie: string; + csrf: string; + auth: string; } /** @@ -17,41 +17,41 @@ export interface TwitterAuthTokens { */ export function captureTwitterTokens( details: chrome.webRequest.WebRequestDetails & { - requestHeaders?: chrome.webRequest.HttpHeader[] + requestHeaders?: chrome.webRequest.HttpHeader[]; }, ): boolean { if (!(details.url.includes("x.com") || details.url.includes("twitter.com"))) { - return false + return false; } const authHeader = details.requestHeaders?.find( (header) => header.name.toLowerCase() === "authorization", - ) + ); const cookieHeader = details.requestHeaders?.find( (header) => header.name.toLowerCase() === "cookie", - ) + ); const csrfHeader = details.requestHeaders?.find( (header) => header.name.toLowerCase() === "x-csrf-token", - ) + ); if (authHeader?.value && cookieHeader?.value && csrfHeader?.value) { chrome.storage.session.get([STORAGE_KEYS.TOKENS_LOGGED], (result) => { if (!result[STORAGE_KEYS.TOKENS_LOGGED]) { - console.log("Twitter auth tokens captured successfully") - chrome.storage.session.set({ [STORAGE_KEYS.TOKENS_LOGGED]: true }) + console.log("Twitter auth tokens captured successfully"); + chrome.storage.session.set({ [STORAGE_KEYS.TOKENS_LOGGED]: true }); } - }) + }); chrome.storage.session.set({ [STORAGE_KEYS.TWITTER_COOKIE]: cookieHeader.value, [STORAGE_KEYS.TWITTER_CSRF]: csrfHeader.value, [STORAGE_KEYS.TWITTER_AUTH_TOKEN]: authHeader.value, - }) + }); - return true + return true; } - return false + return false; } /** @@ -63,21 +63,21 @@ export async function getTwitterTokens(): Promise<TwitterAuthTokens | null> { STORAGE_KEYS.TWITTER_COOKIE, STORAGE_KEYS.TWITTER_CSRF, STORAGE_KEYS.TWITTER_AUTH_TOKEN, - ]) + ]); if ( !result[STORAGE_KEYS.TWITTER_COOKIE] || !result[STORAGE_KEYS.TWITTER_CSRF] || !result[STORAGE_KEYS.TWITTER_AUTH_TOKEN] ) { - return null + return null; } return { cookie: result[STORAGE_KEYS.TWITTER_COOKIE], csrf: result[STORAGE_KEYS.TWITTER_CSRF], auth: result[STORAGE_KEYS.TWITTER_AUTH_TOKEN], - } + }; } /** @@ -86,16 +86,16 @@ export async function getTwitterTokens(): Promise<TwitterAuthTokens | null> { * @returns Headers object ready for fetch requests */ export function createTwitterAPIHeaders(tokens: TwitterAuthTokens): Headers { - const headers = new Headers() - headers.append("Cookie", tokens.cookie) - headers.append("X-Csrf-Token", tokens.csrf) - headers.append("Authorization", tokens.auth) - headers.append("Content-Type", "application/json") + const headers = new Headers(); + headers.append("Cookie", tokens.cookie); + headers.append("X-Csrf-Token", tokens.csrf); + headers.append("Authorization", tokens.auth); + headers.append("Content-Type", "application/json"); headers.append( "User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", - ) - headers.append("Accept", "*/*") - headers.append("Accept-Language", "en-US,en;q=0.9") - return headers + ); + headers.append("Accept", "*/*"); + headers.append("Accept-Language", "en-US,en;q=0.9"); + return headers; } diff --git a/apps/browser-extension/utils/twitter-import.ts b/apps/browser-extension/utils/twitter-import.ts index e68d6dbf..bfd3a046 100644 --- a/apps/browser-extension/utils/twitter-import.ts +++ b/apps/browser-extension/utils/twitter-import.ts @@ -3,45 +3,45 @@ * Handles the import process for Twitter bookmarks */ -import { saveAllTweets } from "./api" -import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth" +import { saveAllTweets } from "./api"; +import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth"; import { BOOKMARKS_URL, buildRequestVariables, extractNextCursor, getAllTweets, type TwitterAPIResponse, -} from "./twitter-utils" +} from "./twitter-utils"; -export type ImportProgressCallback = (message: string) => Promise<void> +export type ImportProgressCallback = (message: string) => Promise<void>; -export type ImportCompleteCallback = (totalImported: number) => Promise<void> +export type ImportCompleteCallback = (totalImported: number) => Promise<void>; export interface TwitterImportConfig { - onProgress: ImportProgressCallback - onComplete: ImportCompleteCallback - onError: (error: Error) => Promise<void> + onProgress: ImportProgressCallback; + onComplete: ImportCompleteCallback; + onError: (error: Error) => Promise<void>; } /** * Rate limiting configuration */ class RateLimiter { - private waitTime = 60000 // Start with 1 minute + private waitTime = 60000; // Start with 1 minute async handleRateLimit(onProgress: ImportProgressCallback): Promise<void> { - const waitTimeInSeconds = this.waitTime / 1000 + const waitTimeInSeconds = this.waitTime / 1000; await onProgress( `Rate limit reached. Waiting for ${waitTimeInSeconds} seconds before retrying...`, - ) + ); - await new Promise((resolve) => setTimeout(resolve, this.waitTime)) - this.waitTime *= 2 // Exponential backoff + await new Promise((resolve) => setTimeout(resolve, this.waitTime)); + this.waitTime *= 2; // Exponential backoff } reset(): void { - this.waitTime = 60000 + this.waitTime = 60000; } } @@ -49,8 +49,8 @@ class RateLimiter { * Main class for handling Twitter bookmarks import */ export class TwitterImporter { - private importInProgress = false - private rateLimiter = new RateLimiter() + private importInProgress = false; + private rateLimiter = new RateLimiter(); constructor(private config: TwitterImportConfig) {} @@ -60,19 +60,19 @@ export class TwitterImporter { */ async startImport(): Promise<void> { if (this.importInProgress) { - throw new Error("Import already in progress") + throw new Error("Import already in progress"); } - this.importInProgress = true - const uniqueGroupId = crypto.randomUUID() + this.importInProgress = true; + const uniqueGroupId = crypto.randomUUID(); try { - await this.batchImportAll("", 0, uniqueGroupId) - this.rateLimiter.reset() + await this.batchImportAll("", 0, uniqueGroupId); + this.rateLimiter.reset(); } catch (error) { - await this.config.onError(error as Error) + await this.config.onError(error as Error); } finally { - this.importInProgress = false + this.importInProgress = false; } } @@ -81,52 +81,56 @@ 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 + let importedCount = totalImported; // Get authentication tokens - const tokens = await getTwitterTokens() + const tokens = await getTwitterTokens(); if (!tokens) { await this.config.onProgress( "Please visit Twitter/X first to capture authentication tokens", - ) - return + ); + return; } // Create headers for API request - const headers = createTwitterAPIHeaders(tokens) + const headers = createTwitterAPIHeaders(tokens); // Build API request with pagination - const variables = buildRequestVariables(cursor) + const variables = buildRequestVariables(cursor); const urlWithCursor = cursor ? `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}` - : BOOKMARKS_URL + : BOOKMARKS_URL; const response = await fetch(urlWithCursor, { method: "GET", headers, redirect: "follow", - }) + }); if (!response.ok) { - const errorText = await response.text() - console.error(`Twitter API Error ${response.status}:`, errorText) + const errorText = await response.text(); + console.error(`Twitter API Error ${response.status}:`, errorText); if (response.status === 429) { - await this.rateLimiter.handleRateLimit(this.config.onProgress) - return this.batchImportAll(cursor, totalImported, uniqueGroupId) + await this.rateLimiter.handleRateLimit(this.config.onProgress); + return this.batchImportAll(cursor, totalImported, uniqueGroupId); } throw new Error( `Failed to fetch data: ${response.status} - ${errorText}`, - ) + ); } - const data: TwitterAPIResponse = await response.json() - const tweets = getAllTweets(data) + const data: TwitterAPIResponse = await response.json(); + const tweets = getAllTweets(data); - const documents: MemoryPayload[] = [] + const documents: MemoryPayload[] = []; // Convert tweets to MemoryPayload for (const tweet of tweets) { @@ -139,49 +143,51 @@ export class TwitterImporter { likes: tweet.favorite_count, retweets: tweet.retweet_count || 0, sm_internal_group_id: uniqueGroupId, - } + }; documents.push({ containerTags: ["sm_project_twitter_bookmarks"], 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...`) + }); + importedCount++; + await this.config.onProgress( + `Imported ${importedCount} tweets, so far...`, + ); } catch (error) { - console.error("Error importing tweet:", error) + console.error("Error importing tweet:", error); } } try { if (documents.length > 0) { - await saveAllTweets(documents) + await saveAllTweets(documents); } - console.log("Tweets saved") - console.log("Documents:", documents) + console.log("Tweets saved"); + console.log("Documents:", documents); } catch (error) { - console.error("Error saving tweets batch:", error) - await this.config.onError(error as Error) - return + console.error("Error saving tweets batch:", error); + await this.config.onError(error as Error); + return; } // Handle pagination const instructions = - data.data?.bookmark_timeline_v2?.timeline?.instructions - const nextCursor = extractNextCursor(instructions || []) + data.data?.bookmark_timeline_v2?.timeline?.instructions; + const nextCursor = extractNextCursor(instructions || []); - console.log("Next cursor:", nextCursor) - console.log("Tweets length:", tweets.length) + console.log("Next cursor:", nextCursor); + console.log("Tweets length:", tweets.length); if (nextCursor && tweets.length > 0) { - await new Promise((resolve) => setTimeout(resolve, 1000)) // Rate limiting - await this.batchImportAll(nextCursor, importedCount, uniqueGroupId) + await new Promise((resolve) => setTimeout(resolve, 1000)); // Rate limiting + await this.batchImportAll(nextCursor, importedCount, uniqueGroupId); } else { - await this.config.onComplete(importedCount) + await this.config.onComplete(importedCount); } } catch (error) { - console.error("Batch import error:", error) - await this.config.onError(error as Error) + console.error("Batch import error:", error); + await this.config.onError(error as Error); } } } diff --git a/apps/browser-extension/utils/twitter-utils.ts b/apps/browser-extension/utils/twitter-utils.ts index 7a7b86db..89d774bd 100644 --- a/apps/browser-extension/utils/twitter-utils.ts +++ b/apps/browser-extension/utils/twitter-utils.ts @@ -1,120 +1,120 @@ // Twitter API data structures and transformation utilities interface TwitterAPITweet { - __typename?: string + __typename?: string; legacy: { - lang?: string - favorite_count: number - created_at: string - display_text_range?: [number, number] + lang?: string; + favorite_count: number; + created_at: string; + display_text_range?: [number, number]; entities?: { - hashtags?: Array<{ indices: [number, number]; text: string }> + hashtags?: Array<{ indices: [number, number]; text: string }>; urls?: Array<{ - display_url: string - expanded_url: string - indices: [number, number] - url: string - }> + display_url: string; + expanded_url: string; + indices: [number, number]; + url: string; + }>; user_mentions?: Array<{ - id_str: string - indices: [number, number] - name: string - screen_name: string - }> - symbols?: Array<{ indices: [number, number]; text: string }> - media?: MediaEntity[] - } - id_str: string - full_text: string - reply_count?: number - retweet_count?: number - quote_count?: number - } + id_str: string; + indices: [number, number]; + name: string; + screen_name: string; + }>; + symbols?: Array<{ indices: [number, number]; text: string }>; + media?: MediaEntity[]; + }; + id_str: string; + full_text: string; + reply_count?: number; + retweet_count?: number; + quote_count?: number; + }; core?: { user_results?: { result?: { legacy?: { - id_str: string - name: string - profile_image_url_https: string - screen_name: string - verified: boolean - } - is_blue_verified?: boolean - } - } - } + id_str: string; + name: string; + profile_image_url_https: string; + screen_name: string; + verified: boolean; + }; + is_blue_verified?: boolean; + }; + }; + }; } interface MediaEntity { - type: string - media_url_https: string + type: string; + media_url_https: string; sizes?: { large?: { - w: number - h: number - } - } + w: number; + h: number; + }; + }; video_info?: { variants?: Array<{ - url: string - }> - duration_millis?: number - } + url: string; + }>; + duration_millis?: number; + }; } export interface Tweet { - __typename?: string - lang?: string - favorite_count: number - created_at: string - display_text_range?: [number, number] + __typename?: string; + lang?: string; + favorite_count: number; + created_at: string; + display_text_range?: [number, number]; entities: { hashtags: Array<{ - indices: [number, number] - text: string - }> + indices: [number, number]; + text: string; + }>; urls?: Array<{ - display_url: string - expanded_url: string - indices: [number, number] - url: string - }> + display_url: string; + expanded_url: string; + indices: [number, number]; + url: string; + }>; user_mentions: Array<{ - id_str: string - indices: [number, number] - name: string - screen_name: string - }> + id_str: string; + indices: [number, number]; + name: string; + screen_name: string; + }>; symbols: Array<{ - indices: [number, number] - text: string - }> - } - id_str: string - text: string + indices: [number, number]; + text: string; + }>; + }; + id_str: string; + text: string; user: { - id_str: string - name: string - profile_image_url_https: string - screen_name: string - verified: boolean - is_blue_verified?: boolean - } - conversation_count: number + id_str: string; + name: string; + profile_image_url_https: string; + screen_name: string; + verified: boolean; + is_blue_verified?: boolean; + }; + conversation_count: number; photos?: Array<{ - url: string - width: number - height: number - }> + url: string; + width: number; + height: number; + }>; videos?: Array<{ - url: string - thumbnail_url: string - duration: number - }> - retweet_count?: number - quote_count?: number - reply_count?: number + url: string; + thumbnail_url: string; + duration: number; + }>; + retweet_count?: number; + quote_count?: number; + reply_count?: number; } export interface TwitterAPIResponse { @@ -122,16 +122,16 @@ export interface TwitterAPIResponse { bookmark_timeline_v2: { timeline: { instructions: Array<{ - type: string + type: string; entries?: Array<{ - entryId: string - sortIndex: string - content: Record<string, unknown> - }> - }> - } - } - } + entryId: string; + sortIndex: string; + content: Record<string, unknown>; + }>; + }>; + }; + }; + }; } // Twitter API features configuration @@ -165,9 +165,9 @@ export const TWITTER_API_FEATURES = { articles_preview_enabled: true, rweb_video_timestamps_enabled: true, verified_phone_label_enabled: true, -} +}; -export const BOOKMARKS_URL = `https://x.com/i/api/graphql/xLjCVTqYWz8CGSprLU349w/Bookmarks?features=${encodeURIComponent(JSON.stringify(TWITTER_API_FEATURES))}` +export const BOOKMARKS_URL = `https://x.com/i/api/graphql/xLjCVTqYWz8CGSprLU349w/Bookmarks?features=${encodeURIComponent(JSON.stringify(TWITTER_API_FEATURES))}`; /** * Transform raw Twitter API response data into standardized Tweet format @@ -177,29 +177,29 @@ export function transformTweetData( ): Tweet | null { try { const content = input.content as { - itemContent?: { tweet_results?: { result?: unknown } } - } - const tweetData = content?.itemContent?.tweet_results?.result + itemContent?: { tweet_results?: { result?: unknown } }; + }; + const tweetData = content?.itemContent?.tweet_results?.result; if (!tweetData) { - return null + return null; } - const tweet = tweetData as TwitterAPITweet + const tweet = tweetData as TwitterAPITweet; if (!tweet.legacy) { - return null + return null; } // Handle media entities - const media = (tweet.legacy.entities?.media as MediaEntity[]) || [] + const media = (tweet.legacy.entities?.media as MediaEntity[]) || []; const photos = media .filter((m) => m.type === "photo") .map((m) => ({ url: m.media_url_https, width: m.sizes?.large?.w || 0, height: m.sizes?.large?.h || 0, - })) + })); const videos = media .filter((m) => m.type === "video") @@ -207,7 +207,7 @@ export function transformTweetData( url: m.video_info?.variants?.[0]?.url || "", thumbnail_url: m.media_url_https, duration: m.video_info?.duration_millis || 0, - })) + })); const transformed: Tweet = { __typename: tweet.__typename, @@ -239,20 +239,20 @@ export function transformTweetData( retweet_count: tweet.legacy.retweet_count || 0, quote_count: tweet.legacy.quote_count || 0, reply_count: tweet.legacy.reply_count || 0, - } + }; if (photos.length > 0) { - transformed.photos = photos + transformed.photos = photos; } if (videos.length > 0) { - transformed.videos = videos + transformed.videos = videos; } - return transformed + return transformed; } catch (error) { - console.error("Error transforming tweet data:", error) - return null + console.error("Error transforming tweet data:", error); + return null; } } @@ -260,29 +260,29 @@ export function transformTweetData( * Extract all tweets from Twitter API response */ export function getAllTweets(data: TwitterAPIResponse): Tweet[] { - const tweets: Tweet[] = [] + const tweets: Tweet[] = []; try { const instructions = - data.data?.bookmark_timeline_v2?.timeline?.instructions || [] + data.data?.bookmark_timeline_v2?.timeline?.instructions || []; for (const instruction of instructions) { if (instruction.type === "TimelineAddEntries" && instruction.entries) { for (const entry of instruction.entries) { if (entry.entryId.startsWith("tweet-")) { - const tweet = transformTweetData(entry) + const tweet = transformTweetData(entry); if (tweet) { - tweets.push(tweet) + tweets.push(tweet); } } } } } } catch (error) { - console.error("Error extracting tweets:", error) + console.error("Error extracting tweets:", error); } - return tweets + return tweets; } /** @@ -295,69 +295,69 @@ export function extractNextCursor( for (const instruction of instructions) { if (instruction.type === "TimelineAddEntries" && instruction.entries) { const entries = instruction.entries as Array<{ - entryId: string - content?: { value?: string } - }> + entryId: string; + content?: { value?: string }; + }>; for (const entry of entries) { if (entry.entryId.startsWith("cursor-bottom-")) { - return entry.content?.value || null + return entry.content?.value || null; } } } } } catch (error) { - console.error("Error extracting cursor:", error) + console.error("Error extracting cursor:", error); } - return null + return null; } /** * Convert Tweet object to markdown format for storage */ export function tweetToMarkdown(tweet: Tweet): string { - const username = tweet.user?.screen_name || "unknown" - const displayName = tweet.user?.name || "Unknown User" - const date = new Date(tweet.created_at).toLocaleDateString() - const time = new Date(tweet.created_at).toLocaleTimeString() + const username = tweet.user?.screen_name || "unknown"; + const displayName = tweet.user?.name || "Unknown User"; + const date = new Date(tweet.created_at).toLocaleDateString(); + const time = new Date(tweet.created_at).toLocaleTimeString(); - let markdown = `# Tweet by @${username} (${displayName})\n\n` - markdown += `**Date:** ${date} ${time}\n` - markdown += `**Likes:** ${tweet.favorite_count} | **Retweets:** ${tweet.retweet_count || 0} | **Replies:** ${tweet.reply_count || 0}\n\n` + let markdown = `# Tweet by @${username} (${displayName})\n\n`; + markdown += `**Date:** ${date} ${time}\n`; + markdown += `**Likes:** ${tweet.favorite_count} | **Retweets:** ${tweet.retweet_count || 0} | **Replies:** ${tweet.reply_count || 0}\n\n`; // Add tweet text - markdown += `${tweet.text}\n\n` + markdown += `${tweet.text}\n\n`; // Add media if present if (tweet.photos && tweet.photos.length > 0) { - markdown += "**Images:**\n" + markdown += "**Images:**\n"; tweet.photos.forEach((photo, index) => { - markdown += `\n` - }) - markdown += "\n" + markdown += `\n`; + }); + markdown += "\n"; } if (tweet.videos && tweet.videos.length > 0) { - markdown += "**Videos:**\n" + markdown += "**Videos:**\n"; tweet.videos.forEach((video, index) => { - markdown += `[Video ${index + 1}](${video.url})\n` - }) - markdown += "\n" + markdown += `[Video ${index + 1}](${video.url})\n`; + }); + markdown += "\n"; } // Add hashtags and mentions if (tweet.entities.hashtags.length > 0) { - markdown += `**Hashtags:** ${tweet.entities.hashtags.map((h) => `#${h.text}`).join(", ")}\n` + markdown += `**Hashtags:** ${tweet.entities.hashtags.map((h) => `#${h.text}`).join(", ")}\n`; } if (tweet.entities.user_mentions.length > 0) { - markdown += `**Mentions:** ${tweet.entities.user_mentions.map((m) => `@${m.screen_name}`).join(", ")}\n` + markdown += `**Mentions:** ${tweet.entities.user_mentions.map((m) => `@${m.screen_name}`).join(", ")}\n`; } // Add raw data for reference - markdown += `\n---\n<details>\n<summary>Raw Tweet Data</summary>\n\n\`\`\`json\n${JSON.stringify(tweet, null, 2)}\n\`\`\`\n</details>` + markdown += `\n---\n<details>\n<summary>Raw Tweet Data</summary>\n\n\`\`\`json\n${JSON.stringify(tweet, null, 2)}\n\`\`\`\n</details>`; - return markdown + return markdown; } /** @@ -367,11 +367,11 @@ export function buildRequestVariables(cursor?: string, count = 100) { const variables = { count, includePromotedContent: false, - } + }; if (cursor) { - ;(variables as Record<string, unknown>).cursor = cursor + (variables as Record<string, unknown>).cursor = cursor; } - return variables + return variables; } diff --git a/apps/browser-extension/utils/types.ts b/apps/browser-extension/utils/types.ts index 06a4ae72..d5049e19 100644 --- a/apps/browser-extension/utils/types.ts +++ b/apps/browser-extension/utils/types.ts @@ -5,102 +5,102 @@ /** * Toast states for UI feedback */ -export type ToastState = "loading" | "success" | "error" +export type ToastState = "loading" | "success" | "error"; /** * Message types for extension communication */ export interface ExtensionMessage { - action?: string - type?: string - data?: unknown - state?: ToastState - importedMessage?: string - totalImported?: number - actionSource?: string + action?: string; + type?: string; + data?: unknown; + state?: ToastState; + importedMessage?: string; + totalImported?: number; + actionSource?: string; } /** * Memory data structure for saving content */ export interface MemoryData { - html?: string - content?: string - highlightedText?: string - url?: string + html?: string; + content?: string; + highlightedText?: string; + url?: string; } /** * Supermemory API payload for storing memories */ export interface MemoryPayload { - containerTags?: string[] - content: string + containerTags?: string[]; + content: string; metadata: { - sm_source: string - [key: string]: unknown - } - customId?: string + sm_source: string; + [key: string]: unknown; + }; + customId?: string; } /** * Twitter-specific memory metadata */ export interface TwitterMemoryMetadata { - sm_source: "twitter_bookmarks" - tweet_id: string - author: string - created_at: string - likes: number - retweets: number + sm_source: "twitter_bookmarks"; + tweet_id: string; + author: string; + created_at: string; + likes: number; + retweets: number; } /** * Storage data structure for Chrome storage */ export interface StorageData { - bearerToken?: string + bearerToken?: string; twitterAuth?: { - cookie: string - csrf: string - auth: string - } - tokens_logged?: boolean - cookie?: string - csrf?: string - auth?: string - defaultProject?: Project + cookie: string; + csrf: string; + auth: string; + }; + tokens_logged?: boolean; + cookie?: string; + csrf?: string; + auth?: string; + defaultProject?: Project; projectsCache?: { - projects: Project[] - timestamp: number - } + projects: Project[]; + timestamp: number; + }; } /** * Context menu click info */ export interface ContextMenuClickInfo { - menuItemId: string | number - editable?: boolean - frameId?: number - frameUrl?: string - linkUrl?: string - mediaType?: string - pageUrl?: string - parentMenuItemId?: string | number - selectionText?: string - srcUrl?: string - targetElementId?: number - wasChecked?: boolean + menuItemId: string | number; + editable?: boolean; + frameId?: number; + frameUrl?: string; + linkUrl?: string; + mediaType?: string; + pageUrl?: string; + parentMenuItemId?: string | number; + selectionText?: string; + srcUrl?: string; + targetElementId?: number; + wasChecked?: boolean; } /** * API Response types */ export interface APIResponse<T = unknown> { - success: boolean - data?: T - error?: string + success: boolean; + data?: T; + error?: string; } /** @@ -112,41 +112,41 @@ export class ExtensionError extends Error { public code?: string, public statusCode?: number, ) { - super(message) - this.name = "ExtensionError" + super(message); + this.name = "ExtensionError"; } } export class TwitterAPIError extends ExtensionError { constructor(message: string, statusCode?: number) { - super(message, "TWITTER_API_ERROR", statusCode) - this.name = "TwitterAPIError" + super(message, "TWITTER_API_ERROR", statusCode); + this.name = "TwitterAPIError"; } } export class SupermemoryAPIError extends ExtensionError { constructor(message: string, statusCode?: number) { - super(message, "SUPERMEMORY_API_ERROR", statusCode) - this.name = "SupermemoryAPIError" + super(message, "SUPERMEMORY_API_ERROR", statusCode); + this.name = "SupermemoryAPIError"; } } export class AuthenticationError extends ExtensionError { constructor(message = "Authentication required") { - super(message, "AUTH_ERROR") - this.name = "AuthenticationError" + super(message, "AUTH_ERROR"); + this.name = "AuthenticationError"; } } export interface Project { - id: string - name: string - containerTag: string - createdAt: string - updatedAt: string - documentCount: number + id: string; + name: string; + containerTag: string; + createdAt: string; + updatedAt: string; + documentCount: number; } export interface ProjectsResponse { - projects: Project[] + projects: Project[]; } diff --git a/apps/browser-extension/utils/ui-components.ts b/apps/browser-extension/utils/ui-components.ts index 8a56ea5a..ed14faff 100644 --- a/apps/browser-extension/utils/ui-components.ts +++ b/apps/browser-extension/utils/ui-components.ts @@ -3,8 +3,8 @@ * Reusable UI components for the browser extension */ -import { ELEMENT_IDS, UI_CONFIG } from "./constants" -import type { ToastState } from "./types" +import { ELEMENT_IDS, UI_CONFIG } from "./constants"; +import type { ToastState } from "./types"; /** * Creates a toast notification element @@ -12,8 +12,8 @@ import type { ToastState } from "./types" * @returns HTMLElement - The toast element */ export function createToast(state: ToastState): HTMLElement { - const toast = document.createElement("div") - toast.id = ELEMENT_IDS.SUPERMEMORY_TOAST + const toast = document.createElement("div"); + toast.id = ELEMENT_IDS.SUPERMEMORY_TOAST; toast.style.cssText = ` position: fixed; @@ -33,12 +33,12 @@ export function createToast(state: ToastState): HTMLElement { max-width: 300px; animation: slideIn 0.3s ease-out; box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18), 0 1.5px 6px 0 rgba(0,0,0,0.12); - ` + `; // Add keyframe animations and fonts if not already present if (!document.getElementById("supermemory-toast-styles")) { - const style = document.createElement("style") - style.id = "supermemory-toast-styles" + const style = document.createElement("style"); + style.id = "supermemory-toast-styles"; style.textContent = ` @font-face { font-family: 'Space Grotesk'; @@ -87,15 +87,15 @@ export function createToast(state: ToastState): HTMLElement { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } - ` - document.head.appendChild(style) + `; + document.head.appendChild(style); } - const icon = document.createElement("div") - icon.style.cssText = "width: 20px; height: 20px; flex-shrink: 0;" + const icon = document.createElement("div"); + icon.style.cssText = "width: 20px; height: 20px; flex-shrink: 0;"; - const text = document.createElement("span") - text.style.fontWeight = "500" + const text = document.createElement("span"); + text.style.fontWeight = "500"; // Configure toast based on state switch (state) { @@ -111,16 +111,16 @@ export function createToast(state: ToastState): HTMLElement { <path d="M20.49 15.49L18.36 17.62" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.9"/> <path d="M5.64 6.36L3.51 8.49" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.6"/> </svg> - ` - icon.style.animation = "spin 1s linear infinite" - text.textContent = "Adding to Memory..." - break + `; + icon.style.animation = "spin 1s linear infinite"; + text.textContent = "Adding to Memory..."; + break; case "success": { - const iconUrl = browser.runtime.getURL("/icon-16.png") - icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />` - text.textContent = "Added to Memory" - break + const iconUrl = browser.runtime.getURL("/icon-16.png"); + icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`; + text.textContent = "Added to Memory"; + break; } case "error": @@ -130,15 +130,15 @@ export function createToast(state: ToastState): HTMLElement { <path d="M15 9L9 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M9 9L15 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> - ` - text.textContent = "Failed to save memory / Make sure you are logged in" - break + `; + text.textContent = "Failed to save memory / Make sure you are logged in"; + break; } - toast.appendChild(icon) - toast.appendChild(text) + toast.appendChild(icon); + toast.appendChild(text); - return toast + return toast; } /** @@ -147,8 +147,8 @@ export function createToast(state: ToastState): HTMLElement { * @returns HTMLElement - The button element */ export function createTwitterImportButton(onClick: () => void): HTMLElement { - const button = document.createElement("div") - button.id = ELEMENT_IDS.TWITTER_IMPORT_BUTTON + const button = document.createElement("div"); + button.id = ELEMENT_IDS.TWITTER_IMPORT_BUTTON; button.style.cssText = ` position: fixed; top: 10px; @@ -164,27 +164,27 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement { align-items: center; gap: 8px; transition: all 0.2s ease; - ` + `; - const iconUrl = browser.runtime.getURL("/icon-16.png") + const iconUrl = browser.runtime.getURL("/icon-16.png"); button.innerHTML = ` <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" /> <span style="font-weight: 500; font-size: 12px;">Import Bookmarks</span> - ` + `; button.addEventListener("mouseenter", () => { - button.style.opacity = "0.8" - button.style.boxShadow = "0 4px 12px rgba(29, 155, 240, 0.4)" - }) + button.style.opacity = "0.8"; + button.style.boxShadow = "0 4px 12px rgba(29, 155, 240, 0.4)"; + }); button.addEventListener("mouseleave", () => { - button.style.opacity = "1" - button.style.boxShadow = "0 2px 8px rgba(29, 155, 240, 0.3)" - }) + button.style.opacity = "1"; + button.style.boxShadow = "0 2px 8px rgba(29, 155, 240, 0.3)"; + }); - button.addEventListener("click", onClick) + button.addEventListener("click", onClick); - return button + return button; } /** @@ -193,7 +193,7 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement { * @returns HTMLElement - The save button element */ export function createSaveTweetElement(onClick: () => void): HTMLElement { - const iconButton = document.createElement("div") + const iconButton = document.createElement("div"); iconButton.style.cssText = ` display: inline-flex; align-items: flex-end; @@ -206,29 +206,29 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement { margin-right: 10px; margin-bottom: 2px; z-index: 1000; - ` + `; - const iconFileName = "/icon-16.png" - const iconUrl = browser.runtime.getURL(iconFileName) + const iconFileName = "/icon-16.png"; + const iconUrl = browser.runtime.getURL(iconFileName); iconButton.innerHTML = ` <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" /> - ` + `; iconButton.addEventListener("mouseenter", () => { - iconButton.style.opacity = "1" - }) + iconButton.style.opacity = "1"; + }); iconButton.addEventListener("mouseleave", () => { - iconButton.style.opacity = "0.7" - }) + iconButton.style.opacity = "0.7"; + }); iconButton.addEventListener("click", (event) => { - event.stopPropagation() - event.preventDefault() - onClick() - }) + event.stopPropagation(); + event.preventDefault(); + onClick(); + }); - return iconButton + return iconButton; } /** @@ -237,7 +237,7 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement { * @returns HTMLElement - The save button element */ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement { - const iconButton = document.createElement("div") + const iconButton = document.createElement("div"); iconButton.style.cssText = ` display: inline-flex; align-items: center; @@ -247,30 +247,30 @@ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement { cursor: pointer; transition: opacity 0.2s ease; border-radius: 50%; - ` + `; // Use appropriate icon based on theme - const iconFileName = "/icon-16.png" - const iconUrl = browser.runtime.getURL(iconFileName) + const iconFileName = "/icon-16.png"; + const iconUrl = browser.runtime.getURL(iconFileName); iconButton.innerHTML = ` <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 50%;" /> - ` + `; iconButton.addEventListener("mouseenter", () => { - iconButton.style.opacity = "0.8" - }) + iconButton.style.opacity = "0.8"; + }); iconButton.addEventListener("mouseleave", () => { - iconButton.style.opacity = "1" - }) + iconButton.style.opacity = "1"; + }); iconButton.addEventListener("click", (event) => { - event.stopPropagation() - event.preventDefault() - onClick() - }) + event.stopPropagation(); + event.preventDefault(); + onClick(); + }); - return iconButton + return iconButton; } /** @@ -279,7 +279,7 @@ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement { * @returns HTMLElement - The save button element */ export function createClaudeInputBarElement(onClick: () => void): HTMLElement { - const iconButton = document.createElement("div") + const iconButton = document.createElement("div"); iconButton.style.cssText = ` display: inline-flex; align-items: center; @@ -290,31 +290,31 @@ export function createClaudeInputBarElement(onClick: () => void): HTMLElement { transition: all 0.2s ease; border-radius: 6px; background: transparent; - ` + `; - const iconFileName = "/icon-16.png" - const iconUrl = browser.runtime.getURL(iconFileName) + const iconFileName = "/icon-16.png"; + const iconUrl = browser.runtime.getURL(iconFileName); iconButton.innerHTML = ` <img src="${iconUrl}" width="20" height="20" alt="Get Related Memories from supermemory" style="border-radius: 4px;" /> - ` + `; iconButton.addEventListener("mouseenter", () => { - iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)" - iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)" - }) + iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)"; + iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)"; + }); iconButton.addEventListener("mouseleave", () => { - iconButton.style.backgroundColor = "transparent" - iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)" - }) + iconButton.style.backgroundColor = "transparent"; + iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)"; + }); iconButton.addEventListener("click", (event) => { - event.stopPropagation() - event.preventDefault() - onClick() - }) + event.stopPropagation(); + event.preventDefault(); + onClick(); + }); - return iconButton + return iconButton; } /** @@ -323,7 +323,7 @@ export function createClaudeInputBarElement(onClick: () => void): HTMLElement { * @returns HTMLElement - The save button element */ export function createT3InputBarElement(onClick: () => void): HTMLElement { - const iconButton = document.createElement("div") + const iconButton = document.createElement("div"); iconButton.style.cssText = ` display: inline-flex; align-items: center; @@ -334,31 +334,31 @@ export function createT3InputBarElement(onClick: () => void): HTMLElement { transition: all 0.2s ease; border-radius: 6px; background: transparent; - ` + `; - const iconFileName = "/icon-16.png" - const iconUrl = browser.runtime.getURL(iconFileName) + const iconFileName = "/icon-16.png"; + const iconUrl = browser.runtime.getURL(iconFileName); iconButton.innerHTML = ` <img src="${iconUrl}" width="20" height="20" alt="Get Related Memories from supermemory" style="border-radius: 4px;" /> - ` + `; iconButton.addEventListener("mouseenter", () => { - iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)" - iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)" - }) + iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)"; + iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)"; + }); iconButton.addEventListener("mouseleave", () => { - iconButton.style.backgroundColor = "transparent" - iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)" - }) + iconButton.style.backgroundColor = "transparent"; + iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)"; + }); iconButton.addEventListener("click", (event) => { - event.stopPropagation() - event.preventDefault() - onClick() - }) + event.stopPropagation(); + event.preventDefault(); + onClick(); + }); - return iconButton + return iconButton; } /** @@ -371,7 +371,7 @@ export const DOMUtils = { * @returns boolean */ isOnDomain(domains: readonly string[]): boolean { - return domains.includes(window.location.hostname) + return domains.includes(window.location.hostname); }, /** @@ -379,9 +379,9 @@ export const DOMUtils = { * @returns boolean - true if dark mode, false if light mode */ isDarkMode(): boolean { - const htmlElement = document.documentElement - const style = htmlElement.getAttribute("style") - return style?.includes("color-scheme: dark") || false + const htmlElement = document.documentElement; + const style = htmlElement.getAttribute("style"); + return style?.includes("color-scheme: dark") || false; }, /** @@ -390,7 +390,7 @@ export const DOMUtils = { * @returns boolean */ elementExists(id: string): boolean { - return !!document.getElementById(id) + return !!document.getElementById(id); }, /** @@ -398,8 +398,8 @@ export const DOMUtils = { * @param id - Element ID to remove */ removeElement(id: string): void { - const element = document.getElementById(id) - element?.remove() + const element = document.getElementById(id); + element?.remove(); }, /** @@ -412,18 +412,20 @@ export const DOMUtils = { state: ToastState, duration: number = UI_CONFIG.TOAST_DURATION, ): HTMLElement { - const existingToast = document.getElementById(ELEMENT_IDS.SUPERMEMORY_TOAST) + const existingToast = document.getElementById( + ELEMENT_IDS.SUPERMEMORY_TOAST, + ); if ((state === "success" || state === "error") && existingToast) { - const icon = existingToast.querySelector("div") - const text = existingToast.querySelector("span") + const icon = existingToast.querySelector("div"); + const text = existingToast.querySelector("span"); if (icon && text) { if (state === "success") { - const iconUrl = browser.runtime.getURL("/icon-16.png") - icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />` - icon.style.animation = "" - text.textContent = "Added to Memory" + const iconUrl = browser.runtime.getURL("/icon-16.png"); + icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`; + icon.style.animation = ""; + text.textContent = "Added to Memory"; } else if (state === "error") { icon.innerHTML = ` <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> @@ -431,52 +433,52 @@ export const DOMUtils = { <path d="M15 9L9 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M9 9L15 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> - ` - icon.style.animation = "" + `; + icon.style.animation = ""; text.textContent = - "Failed to save memory / Make sure you are logged in" + "Failed to save memory / Make sure you are logged in"; } // Auto-dismiss setTimeout(() => { if (document.body.contains(existingToast)) { - existingToast.style.animation = "fadeOut 0.3s ease-out" + existingToast.style.animation = "fadeOut 0.3s ease-out"; setTimeout(() => { if (document.body.contains(existingToast)) { - existingToast.remove() + existingToast.remove(); } - }, 300) + }, 300); } - }, duration) + }, duration); - return existingToast + return existingToast; } } const existingToasts = document.querySelectorAll( `#${ELEMENT_IDS.SUPERMEMORY_TOAST}`, - ) + ); existingToasts.forEach((toast) => { - toast.remove() - }) + toast.remove(); + }); - const toast = createToast(state) - document.body.appendChild(toast) + const toast = createToast(state); + document.body.appendChild(toast); // Auto-dismiss for success and error states if (state === "success" || state === "error") { setTimeout(() => { if (document.body.contains(toast)) { - toast.style.animation = "fadeOut 0.3s ease-out" + toast.style.animation = "fadeOut 0.3s ease-out"; setTimeout(() => { if (document.body.contains(toast)) { - toast.remove() + toast.remove(); } - }, 300) + }, 300); } - }, duration) + }, duration); } - return toast + return toast; }, -} +}; |