From e41c431b3aeeba1d9eeac92705d0704539474238 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Sat, 17 Jan 2026 00:24:18 +0000 Subject: [PATCH 1/4] docs: override install md file (#675) --- apps/docs/install.md | 296 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 apps/docs/install.md diff --git a/apps/docs/install.md b/apps/docs/install.md new file mode 100644 index 00000000..1196de78 --- /dev/null +++ b/apps/docs/install.md @@ -0,0 +1,296 @@ +You are integrating Supermemory into my application. Supermemory provides user memory, semantic search, and automatic knowledge extraction for AI applications. + + +You can always reference the documentation by using the **SearchSupermemoryDocs MCP** or running a web search tool for content on **supermemory.ai/docs**. + + +## STEP 1: ASK ME THESE QUESTIONS + +1. What are you building? + - Personal chatbot/assistant + - Team knowledge base + - Customer support bot + - Document Q&A + - Other + +2. How do you want to integrate? + - Vercel AI SDK (@supermemory/tools) + - OpenAI plugins + - Direct SDK (supermemory npm/pip) + - Direct API calls + +3. Data model? + - Individual users only → containerTag: userId + - Organizations only → containerTag: orgId + - Both users AND orgs → ask for strategy + +4. Do you want USER PROFILES? + User profiles are automatically-maintained facts about users (what they like, what they're working on, preferences). + - Yes (RECOMMENDED) → Use client.profile() to get context + - No → Just use search + +5. How should I retrieve context? + - OPTION A: One call with search included → profile({ containerTag, q: userMessage }) + - OPTION B: Separate calls → profile() for facts, search() for memories + +## STEP 2: INSTALL + +```bash +# Get API key: https://console.supermemory.ai +npm install supermemory # or: pip install supermemory +# For Vercel AI SDK: npm install @supermemory/tools +export SUPERMEMORY_API_KEY="sm_..." +``` + +## STEP 3: CONFIGURE SETTINGS (DO THIS FIRST) + +```typescript +// PATCH https://api.supermemory.ai/v3/settings +fetch('https://api.supermemory.ai/v3/settings', { + method: 'PATCH', + headers: { 'x-supermemory-api-key': process.env.SUPERMEMORY_API_KEY }, + body: JSON.stringify({ + shouldLLMFilter: true, + filterPrompt: `This is a [your app description]. containerTag is [userId/orgId]. We store [what data].` + }) +}) +``` + +## STEP 4: CONTAINER TAG STRATEGY + +Based on their data model answer: + +**USER-ONLY APP:** + +```typescript +containerTag: userId +``` + +**ORG-ONLY APP:** + +```typescript +containerTag: orgId // Org members share memories +``` + +**BOTH (ask which):** +- Option A: `containerTag: \`\${userId}-\${orgId}\`` +- Option B: `containerTag: orgId, metadata: { userId }` +- Option C: `containerTag: userId, metadata: { orgId }` + +## STEP 5: INTEGRATION CODE + +Based on their integration choice: + +### VERCEL AI SDK + +```typescript +import { streamText } from 'ai' +import { anthropic } from '@ai-sdk/anthropic' +import { supermemoryTools } from '@supermemory/tools/ai-sdk' + +// Option 1: Agent tools (recommended for agentic flows) +const result = await streamText({ + model: anthropic('claude-3-5-sonnet-20241022'), + prompt: userMessage, + tools: supermemoryTools(process.env.SUPERMEMORY_API_KEY, { + containerTags: [userId] + }) +}) +// Agent gets searchMemories, addMemory, fetchMemory tools + +// Option 2: Profile middleware (automatic context injection) +import { withSupermemory } from '@supermemory/tools/ai-sdk' +const modelWithMemory = withSupermemory(anthropic('claude-3-5-sonnet-20241022'), userId) + +const result = await generateText({ + model: modelWithMemory, + messages: [{ role: 'user', content: userMessage }] +}) +// Profile is automatically injected into context +``` + +### DIRECT SDK (WITH PROFILES) + +```typescript +import Supermemory from 'supermemory' + +const client = new Supermemory() + +// Before each LLM call: +const { profile, searchResults } = await client.profile({ + containerTag: userId, + q: userMessage // Include this if they chose OPTION A (one call) + // Omit if they chose OPTION B (separate calls) +}) + +// Build context +const context = ` +Static facts: ${profile.static.join('\n')} +Recent context: ${profile.dynamic.join('\n')} +${searchResults ? `Memories: ${searchResults.results.map(r => r.content).join('\n')}` : ''} +` + +// Send to LLM +const messages = [ + { role: 'system', content: `User context:\n${context}` }, + { role: 'user', content: userMessage } +] + +// After LLM responds: +await client.memories.add({ + content: `user: ${userMessage}\nassistant: ${response}`, + containerTag: userId +}) +``` + +### DIRECT SDK (NO PROFILES) + +```typescript +import Supermemory from 'supermemory' + +const client = new Supermemory() + +// Search for relevant memories +const results = await client.search({ + q: userMessage, + containerTag: userId, + searchMode: 'hybrid', // Searches memories + document chunks + limit: 5 +}) + +// Build context +const context = results.results.map(r => r.content).join('\n') + +// Send to LLM with context +const messages = [ + { role: 'system', content: `Relevant context:\n${context}` }, + { role: 'user', content: userMessage } +] + +// Store the conversation +await client.memories.add({ + content: `user: ${userMessage}\nassistant: ${response}`, + containerTag: userId +}) +``` + +### PYTHON VERSION + +```python +from supermemory import Supermemory + +client = Supermemory() + +# With profiles (if they want it) +profile_data = client.profile( + container_tag=user_id, + q=user_message # Include if OPTION A, omit if OPTION B +) + +context = f""" +Static: {chr(10).join(profile_data.profile.static)} +Dynamic: {chr(10).join(profile_data.profile.dynamic)} +""" + +# Store conversation +client.add(content=f"user: {user_message}\\nassistant: {response}", container_tag=user_id) +``` + +### DIRECT API + +```bash +# Add memory +curl -X POST https://api.supermemory.ai/v3/documents \ + -H "x-supermemory-api-key: $SUPERMEMORY_API_KEY" \ + -d '{"content": "conversation", "containerTag": "userId"}' + +# Get profile +curl -X POST https://api.supermemory.ai/v4/profile \ + -H "x-supermemory-api-key: $SUPERMEMORY_API_KEY" \ + -d '{"containerTag": "userId", "q": "search query"}' + +# Search +curl -X POST https://api.supermemory.ai/v4/search \ + -H "x-supermemory-api-key: $SUPERMEMORY_API_KEY" \ + -d '{"q": "query", "containerTag": "userId", "searchMode": "hybrid"}' +``` + +## STEP 6: FILE UPLOADS (if they need it) + +```typescript +// Files are automatically extracted (PDFs, images with OCR, videos with transcription) +const formData = new FormData() +formData.append('file', fileBlob) +formData.append('containerTag', userId) + +await fetch('https://api.supermemory.ai/v3/documents/file', { + method: 'POST', + headers: { 'x-supermemory-api-key': process.env.SUPERMEMORY_API_KEY }, + body: formData +}) + +// Processing is async - check status before assuming searchable +// GET /v3/documents/{documentId} +``` + +## STEP 7: SEARCH MODES + +```typescript +// HYBRID (recommended) - searches memories + document chunks +searchMode: 'hybrid' + +// MEMORIES ONLY - just extracted memories, no original text +searchMode: 'memories' +``` + +## STEP 8: METADATA FILTERS (if they need secondary filtering) + +```typescript +await client.search({ + q: query, + containerTag: userId, + filters: { + AND: [ + { key: 'type', value: 'conversation', type: 'string_equal' }, + { key: 'timestamp', value: '2024', type: 'string_contains' } + ] + } +}) +``` + +## KEY POINTS: + +1. Configure settings FIRST with filterPrompt +2. User profiles = automatic facts about users (profile.static + profile.dynamic) +3. profile({ containerTag, q }) combines profile + search in ONE call +4. Search modes: 'hybrid' (recommended) or 'memories' +5. File extraction is automatic - no config needed +6. Store conversations after each interaction +7. containerTag should match what you put in filterPrompt + +## TESTING: + +```bash +# 1. Configure settings +curl -X PATCH https://api.supermemory.ai/v3/settings \ + -H "x-supermemory-api-key: $SUPERMEMORY_API_KEY" \ + -d '{"shouldLLMFilter": true, "filterPrompt": "..."}' + +# 2. Add test memory +curl -X POST https://api.supermemory.ai/v3/documents \ + -H "x-supermemory-api-key: $SUPERMEMORY_API_KEY" \ + -d '{"content": "Test", "containerTag": "test_user"}' + +# 3. Get profile +curl -X POST https://api.supermemory.ai/v4/profile \ + -H "x-supermemory-api-key: $SUPERMEMORY_API_KEY" \ + -d '{"containerTag": "test_user"}' +``` + +## NOW: + +1. Ask me the 5 questions above +2. Generate complete working code based on my answers +3. Include installation, settings config, and full integration + +**DOCS:** https://supermemory.ai/docs From b1b37ddc4efea081d4a8d22b3351d443e397aeb8 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Sat, 17 Jan 2026 00:45:52 +0000 Subject: [PATCH 2/4] fix: mintlify build (#677) --- apps/docs/install.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/docs/install.md b/apps/docs/install.md index 1196de78..d0d4b1b8 100644 --- a/apps/docs/install.md +++ b/apps/docs/install.md @@ -1,8 +1,6 @@ You are integrating Supermemory into my application. Supermemory provides user memory, semantic search, and automatic knowledge extraction for AI applications. - You can always reference the documentation by using the **SearchSupermemoryDocs MCP** or running a web search tool for content on **supermemory.ai/docs**. - ## STEP 1: ASK ME THESE QUESTIONS From acdb4226350b93047dea590cadcb2e72a37471d5 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Sat, 17 Jan 2026 00:57:48 +0000 Subject: [PATCH 3/4] add (mcp): projects aware tool on every init (#676) --- apps/mcp/package.json | 4 +- apps/mcp/src/index.ts | 5 ++- apps/mcp/src/server.ts | 88 ++++++++++++++++++++++++++++++++++++++++-- bun.lock | 46 ++++------------------ 4 files changed, 96 insertions(+), 47 deletions(-) diff --git a/apps/mcp/package.json b/apps/mcp/package.json index a0042da6..5ba472ad 100644 --- a/apps/mcp/package.json +++ b/apps/mcp/package.json @@ -9,8 +9,8 @@ }, "dependencies": { "@cloudflare/workers-oauth-provider": "^0.2.2", - "@modelcontextprotocol/sdk": "^1.12.1", - "agents": "^0.2.32", + "@modelcontextprotocol/sdk": "^1.25.2", + "agents": "^0.3.5", "hono": "^4.11.1", "posthog-node": "^5.18.0", "supermemory": "^4.0.0", diff --git a/apps/mcp/src/index.ts b/apps/mcp/src/index.ts index a1fe7bac..0586492f 100644 --- a/apps/mcp/src/index.ts +++ b/apps/mcp/src/index.ts @@ -1,8 +1,9 @@ -import { Hono } from "hono" import { cors } from "hono/cors" +import { Hono } from "hono" import { SupermemoryMCP } from "./server" import { isApiKey, validateApiKey, validateOAuthToken } from "./auth" import { initPosthog } from "./posthog" +import type { ContentfulStatusCode } from "hono/utils/http-status" type Bindings = { MCP_SERVER: DurableObjectNamespace @@ -78,7 +79,7 @@ app.get("/.well-known/oauth-authorization-server", async (c) => { if (!response.ok) { return c.json( { error: "Failed to fetch authorization server metadata" }, - response.status, + { status: response.status as ContentfulStatusCode }, ) } diff --git a/apps/mcp/src/server.ts b/apps/mcp/src/server.ts index bbcd493e..d83d2326 100644 --- a/apps/mcp/src/server.ts +++ b/apps/mcp/src/server.ts @@ -20,6 +20,7 @@ type Props = { export class SupermemoryMCP extends McpAgent { private clientInfo: { name: string; version?: string } | null = null + private cachedContainerTags: string[] = [] server = new McpServer({ name: "supermemory", @@ -37,7 +38,6 @@ export class SupermemoryMCP extends McpAgent { initPosthog(this.env.POSTHOG_API_KEY) - // Hook MCP McpAgent to capture client info this.server.server.oninitialized = async () => { const clientVersion = this.server.server.getClientVersion() @@ -49,6 +49,11 @@ export class SupermemoryMCP extends McpAgent { await this.ctx.storage.put("clientInfo", this.clientInfo) } } + + await this.refreshContainerTags() // Fetch available projects for schema descriptions + + const containerTagDescription = this.getContainerTagDescription() + const memorySchema = z.object({ content: z .string() @@ -58,7 +63,7 @@ export class SupermemoryMCP extends McpAgent { containerTag: z .string() .max(128, "Container tag exceeds maximum length") - .describe("Optional container tag") + .describe(containerTagDescription) .optional(), }) @@ -71,7 +76,7 @@ export class SupermemoryMCP extends McpAgent { containerTag: z .string() .max(128, "Container tag exceeds maximum length") - .describe("Optional container tag") + .describe(containerTagDescription) .optional(), }) @@ -79,7 +84,7 @@ export class SupermemoryMCP extends McpAgent { containerTag: z .string() .max(128, "Container tag exceeds maximum length") - .describe("Optional container tag to scope the profile") + .describe(containerTagDescription) .optional(), includeRecent: z .boolean() @@ -176,6 +181,63 @@ export class SupermemoryMCP extends McpAgent { }, ) + // Register listProjects tool + this.server.registerTool( + "listProjects", + { + description: + "List all available projects for organizing memories. Use this to discover valid project names for memory/recall operations.", + inputSchema: z.object({ + refresh: z + .boolean() + .optional() + .default(true) + .describe("Refresh the list from the server (default: true)"), + }), + }, + // @ts-expect-error - zod type inference issue with MCP SDK + async (args: { refresh?: boolean }) => { + try { + if (args.refresh !== false) { + await this.refreshContainerTags() + } + const projects = this.cachedContainerTags + + if (projects.length === 0) { + return { + content: [ + { + type: "text" as const, + text: "No projects found. Memories will use the default project.", + }, + ], + } + } + + return { + content: [ + { + type: "text" as const, + text: `Available projects:\n${projects.map((p) => `- ${p}`).join("\n")}`, + }, + ], + } + } catch (error) { + const message = + error instanceof Error ? error.message : "An unexpected error occurred" + return { + content: [ + { + type: "text" as const, + text: `Error listing projects: ${message}`, + }, + ], + isError: true, + } + } + }, + ) + // Register whoAmI tool this.server.registerTool( "whoAmI", @@ -222,6 +284,7 @@ export class SupermemoryMCP extends McpAgent { "User profile and preferences for system context injection. Returns a formatted system message with user's stable preferences and recent activity.", //argsSchema: contextPromptSchema.shape, TODO: commenting out for now as it will add more friction to the user }, + // @ts-expect-error - zod type inference issue with MCP SDK async (args: ContextPromptArgs) => { try { const { containerTag, includeRecent = true } = args @@ -543,4 +606,21 @@ export class SupermemoryMCP extends McpAgent { private getMcpSessionId(): string { return this.ctx.id.name || "unknown" } + + private async refreshContainerTags(): Promise { + try { + const client = this.getClient() + this.cachedContainerTags = await client.getProjects() + } catch (error) { + console.error("Failed to fetch container tags:", error) + } + } + + private getContainerTagDescription(): string { + const baseDescription = "Optional project to scope memories" + if (this.cachedContainerTags.length === 0) { + return baseDescription + } + return `${baseDescription}. Available projects: ${this.cachedContainerTags.join(", ")}` + } } diff --git a/bun.lock b/bun.lock index ce1cc468..b63f3ca9 100644 --- a/bun.lock +++ b/bun.lock @@ -87,8 +87,8 @@ "version": "4.0.0", "dependencies": { "@cloudflare/workers-oauth-provider": "^0.2.2", - "@modelcontextprotocol/sdk": "^1.12.1", - "agents": "^0.2.32", + "@modelcontextprotocol/sdk": "^1.25.2", + "agents": "^0.3.5", "hono": "^4.11.1", "posthog-node": "^5.18.0", "supermemory": "^4.0.0", @@ -658,6 +658,10 @@ "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], + "@cloudflare/ai-chat": ["@cloudflare/ai-chat@0.0.4", "", { "peerDependencies": { "agents": "^0.3.4", "ai": "^6.0.0", "react": "^19.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-NGQRt34X/UI+mx9fss7LmTTNBDVlFrtu+7JFoaykLUI738w9gjDplsMbOenCRbSr7UW4ngHGnv3YWUmV99eEnQ=="], + + "@cloudflare/codemode": ["@cloudflare/codemode@0.0.4", "", { "dependencies": { "zod-to-ts": "^2.0.0" }, "peerDependencies": { "agents": "^0.3.4", "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-hsHu9q03yBSi8UaQKbVjpEMLJjr2BJ9BpPrH/up9I6JBrvZiMSymVOGuPSPJGso2HGlOfwLEZq9gyFyRmBsSCA=="], + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.1", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg=="], "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.9.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20251202.0" }, "optionalPeers": ["workerd"] }, "sha512-99nEvuOTCGGGRNaIat8UVVXJ27aZK+U09SYDp0kVjQLwC9wyxcrQ28IqLwrQq2DjWLmBI1+UalGJzdPqYgPlRw=="], @@ -2102,7 +2106,7 @@ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "agents": ["agents@0.2.35", "", { "dependencies": { "@cfworker/json-schema": "^4.1.1", "@modelcontextprotocol/sdk": "1.23.0", "cron-schedule": "^6.0.0", "json-schema": "^0.4.0", "json-schema-to-typescript": "^15.0.4", "mimetext": "^3.0.27", "nanoid": "^5.1.6", "partyserver": "^0.1.0", "partysocket": "1.1.10", "yargs": "^18.0.0", "zod-to-ts": "^2.0.0" }, "peerDependencies": { "@ai-sdk/openai": ">=2.0.0", "@ai-sdk/react": ">=1.0.0", "ai": ">=5.0.0", "react": "^19.0.0", "viem": ">=2.0.0", "x402": "^0.7.1", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@ai-sdk/openai", "@ai-sdk/react", "ai", "viem", "x402"], "bin": { "agents": "dist/cli/index.js" } }, "sha512-e6Ift6X0sFceYgFBIZGK4jQ+vyFq9caSF80ilsAVHzfFHvqb21013UrgGzZac4X+fzAP103YBSgS7C57tjAj8w=="], + "agents": ["agents@0.3.5", "", { "dependencies": { "@cfworker/json-schema": "^4.1.1", "@modelcontextprotocol/sdk": "1.25.2", "cron-schedule": "^6.0.0", "json-schema": "^0.4.0", "json-schema-to-typescript": "^15.0.4", "mimetext": "^3.0.27", "nanoid": "^5.1.6", "partyserver": "^0.1.0", "partysocket": "1.1.10", "yargs": "^18.0.0" }, "peerDependencies": { "@ai-sdk/openai": "^3.0.0", "@ai-sdk/react": "^3.0.0", "@cloudflare/ai-chat": "^0.0.4", "@cloudflare/codemode": "^0.0.4", "ai": "^6.0.0", "react": "^19.0.0", "viem": ">=2.0.0", "x402": "^0.7.1", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@ai-sdk/openai", "@ai-sdk/react", "viem", "x402"], "bin": { "agents": "dist/cli/index.js" } }, "sha512-+qEW8jWP8JHEDE8izdMBYFXvh+bWIhnlSujf+zI9m8XWkWWpRUhiiZz7eB40lMeEBZ35p4MEDWmHUL55PnaUDA=="], "aggregate-error": ["aggregate-error@4.0.1", "", { "dependencies": { "clean-stack": "^4.0.0", "indent-string": "^5.0.0" } }, "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w=="], @@ -5294,8 +5298,6 @@ "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "agents/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.23.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-MCGd4K9aZKvuSqdoBkdMvZNcYXCkZRYVs/Gh92mdV5IHbctX9H9uIvd4X93+9g8tBbXv08sxc/QHXTzf8y65bA=="], - "aggregate-error/clean-stack": ["clean-stack@4.2.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg=="], "aggregate-error/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], @@ -6322,12 +6324,6 @@ "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "agents/@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "agents/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - - "agents/@modelcontextprotocol/sdk/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], - "aggregate-error/clean-stack/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "ai-gateway-provider/ai/@ai-sdk/react": ["@ai-sdk/react@1.2.12", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/ui-utils": "1.2.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g=="], @@ -6784,30 +6780,6 @@ "@repo/web/ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "agents/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "agents/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "agents/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - - "agents/@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], - - "agents/@modelcontextprotocol/sdk/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "agents/@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "agents/@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - - "agents/@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "agents/@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "agents/@modelcontextprotocol/sdk/express/send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "agents/@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "agents/@modelcontextprotocol/sdk/express/type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "memory-graph-playground/next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -6863,9 +6835,5 @@ "@puppeteer/browsers/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "@puppeteer/browsers/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "agents/@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "agents/@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], } } From 3fa72c4ec782cfc84e0df49aa1924b84e4f63889 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Sun, 18 Jan 2026 04:06:36 +0000 Subject: [PATCH 4/4] feat: fix interaction and improve Design for extension (#679) ### TL;DR Redesigned the browser extension UI with a dark theme and improved the Twitter bookmarks import experience with a new onboarding flow. ### What changed? - Added a new `RightArrow` icon component for UI navigation - Completely redesigned the popup UI with a dark theme and improved layout - Enhanced Twitter bookmarks import functionality: - Added an onboarding toast that appears the first time a user visits the bookmarks page - Implemented a persistent import intent system that automatically opens the import modal when navigating to the bookmarks page - Created a progress toast to show import status - Improved folder import UI - Updated the extension icon and added a new logo SVG - Improved the project selection modal with better styling --- apps/browser-extension/components/icons.tsx | 18 + .../entrypoints/content/index.ts | 3 + .../entrypoints/content/twitter.ts | 680 ++++++++++++++---- .../entrypoints/popup/App.tsx | 377 +++++++--- .../entrypoints/popup/style.css | 2 +- apps/browser-extension/public/icon-48.png | Bin 110647 -> 12541 bytes .../public/logo-fullmark.svg | 15 + apps/browser-extension/utils/api.ts | 2 +- apps/browser-extension/utils/constants.ts | 13 + apps/browser-extension/utils/ui-components.ts | 14 +- apps/browser-extension/wxt.config.ts | 2 +- 11 files changed, 856 insertions(+), 270 deletions(-) create mode 100644 apps/browser-extension/components/icons.tsx create mode 100644 apps/browser-extension/public/logo-fullmark.svg diff --git a/apps/browser-extension/components/icons.tsx b/apps/browser-extension/components/icons.tsx new file mode 100644 index 00000000..2cddd587 --- /dev/null +++ b/apps/browser-extension/components/icons.tsx @@ -0,0 +1,18 @@ +export function RightArrow({ className }: { className?: string }) { + return ( + + Right arrow + + + ) +} diff --git a/apps/browser-extension/entrypoints/content/index.ts b/apps/browser-extension/entrypoints/content/index.ts index d67b37b0..34a77b6c 100644 --- a/apps/browser-extension/entrypoints/content/index.ts +++ b/apps/browser-extension/entrypoints/content/index.ts @@ -15,6 +15,7 @@ import { initializeT3 } from "./t3" import { handleTwitterNavigation, initializeTwitter, + openImportModal, updateTwitterImportUI, } from "./twitter" @@ -29,6 +30,8 @@ export default defineContentScript({ await saveMemory() } else if (message.action === MESSAGE_TYPES.OPEN_SEARCH_PANEL) { handleOpenSearchPanel(message.data as string) + } else if (message.action === MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL) { + await openImportModal() } else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { updateTwitterImportUI(message) } else if (message.type === MESSAGE_TYPES.IMPORT_DONE) { diff --git a/apps/browser-extension/entrypoints/content/twitter.ts b/apps/browser-extension/entrypoints/content/twitter.ts index d5328245..4ed67315 100644 --- a/apps/browser-extension/entrypoints/content/twitter.ts +++ b/apps/browser-extension/entrypoints/content/twitter.ts @@ -3,10 +3,11 @@ import { ELEMENT_IDS, MESSAGE_TYPES, POSTHOG_EVENT_KEY, + STORAGE_KEYS, + UI_CONFIG, } from "../../utils/constants" import { trackEvent } from "../../utils/posthog" import { - createTwitterImportButton, createProjectSelectionModal, createSaveTweetElement, DOMUtils, @@ -27,46 +28,105 @@ async function loadSpaceGroteskFonts(): Promise { await document.fonts.ready } -export function initializeTwitter() { +/** + * Check if import intent is valid (exists and not expired) + */ +async function checkAndConsumeImportIntent(): Promise { + try { + const result = await browser.storage.local.get( + STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL, + ) + const intentUntil = result[ + STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL + ] as number | undefined + + if (intentUntil && Date.now() < intentUntil) { + await browser.storage.local.remove( + STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL, + ) + return true + } + return false + } catch (error) { + console.error("Error checking import intent:", error) + return false + } +} + +/** + * Check if onboarding toast has been shown before + */ +async function hasOnboardingBeenShown(): Promise { + try { + const result = await browser.storage.local.get( + STORAGE_KEYS.TWITTER_BOOKMARKS_ONBOARDING_SEEN, + ) + return !!result[STORAGE_KEYS.TWITTER_BOOKMARKS_ONBOARDING_SEEN] + } catch (error) { + console.error("Error checking onboarding status:", error) + return true // Default to true to avoid showing toast on error + } +} + +/** + * Mark onboarding toast as shown + */ +async function markOnboardingAsShown(): Promise { + try { + await browser.storage.local.set({ + [STORAGE_KEYS.TWITTER_BOOKMARKS_ONBOARDING_SEEN]: true, + }) + } catch (error) { + console.error("Error marking onboarding as shown:", error) + } +} + +export async function initializeTwitter() { if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { return } - // Initial setup if (window.location.pathname === "/i/bookmarks") { - setTimeout(() => { - addTwitterImportButton() - addTwitterImportButtonForFolders() + setTimeout(async () => { + if (window.location.pathname === "/i/bookmarks") { + await handleBookmarksPageLoad() + } }, 2000) } else { - // Remove button if not on bookmarks page - if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { - DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON) - } + // Clean up any injected UI if navigating away + removeAllTwitterUI() } } -function addTwitterImportButton() { +/** + * Handle what to show when user lands on bookmarks page + */ +async function handleBookmarksPageLoad() { if (window.location.pathname !== "/i/bookmarks") { return } - if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { + addTwitterImportButtonForFolders() // Add buttons to bookmark folders + + const hasIntent = await checkAndConsumeImportIntent() + + if (hasIntent) { + await openImportModal() return } - const button = createTwitterImportButton(async () => { - try { - await handleAllBookmarksImportClick() - } catch (error) { - console.error("Error starting import:", error) - } - }) + const onboardingShown = await hasOnboardingBeenShown() - document.body.appendChild(button) + if (!onboardingShown) { + await showOnboardingToast() + await markOnboardingAsShown() + } } -async function handleAllBookmarksImportClick() { +/** + * Opens the import modal and handles the import flow + */ +export async function openImportModal() { try { const response = await browser.runtime.sendMessage({ action: MESSAGE_TYPES.FETCH_PROJECTS, @@ -85,14 +145,13 @@ async function handleAllBookmarksImportClick() { await showAllBookmarksProjectModal(projects) } } catch (error) { - console.error("Error handling all bookmarks import:", error) + console.error("Error opening import modal:", error) await browser.runtime.sendMessage({ type: MESSAGE_TYPES.BATCH_IMPORT_ALL, }) } } - async function showAllBookmarksProjectModal( projects: Array<{ id: string; name: string; containerTag: string }>, ) { @@ -124,6 +183,420 @@ async function showAllBookmarksProjectModal( document.body.appendChild(modal) } +/** + * Shows the one-time onboarding toast with progress bar + */ +async function showOnboardingToast() { + await loadSpaceGroteskFonts() + + // Remove any existing toast + const existingToast = document.getElementById( + ELEMENT_IDS.TWITTER_ONBOARDING_TOAST, + ) + if (existingToast) { + existingToast.remove() + } + + const duration = UI_CONFIG.ONBOARDING_TOAST_DURATION + + // Create toast container + const toast = document.createElement("div") + toast.id = ELEMENT_IDS.TWITTER_ONBOARDING_TOAST + toast.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + z-index: 2147483647; + background: #ffffff; + border-radius: 12px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; + font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + color: #374151; + min-width: 320px; + max-width: 380px; + box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18), 0 1.5px 6px 0 rgba(0,0,0,0.12); + animation: smSlideInUp 0.3s ease-out; + overflow: hidden; + ` + + // Add keyframe animations if not already present + if (!document.getElementById("supermemory-onboarding-toast-styles")) { + const style = document.createElement("style") + style.id = "supermemory-onboarding-toast-styles" + style.textContent = ` + @keyframes smSlideInUp { + from { transform: translateY(100%); opacity: 0; } + to { transform: translateY(0); opacity: 1; } + } + @keyframes smFadeOut { + from { transform: translateY(0); opacity: 1; } + to { transform: translateY(100%); opacity: 0; } + } + @keyframes smProgressGrow { + from { transform: scaleX(0); } + to { transform: scaleX(1); } + } + @keyframes smPulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } + } + ` + document.head.appendChild(style) + } + + // Header with icon, text and close button + const header = document.createElement("div") + header.style.cssText = + "display: flex; align-items: flex-start; gap: 12px; position: relative;" + + const iconUrl = browser.runtime.getURL("/icon-16.png") + const icon = document.createElement("img") + icon.src = iconUrl + icon.alt = "Supermemory" + icon.style.cssText = "width: 24px; height: 24px; border-radius: 4px; flex-shrink: 0; margin-top: 2px;" + + const textContainer = document.createElement("div") + textContainer.style.cssText = "display: flex; flex-direction: column; gap: 4px; flex: 1;" + + const title = document.createElement("span") + title.style.cssText = "font-weight: 600; font-size: 14px; color: #111827;" + title.textContent = "Import X/Twitter Bookmarks" + + const description = document.createElement("span") + description.style.cssText = "font-size: 13px; color: #6b7280; line-height: 1.4;" + description.textContent = + "You can import all your Twitter bookmarks to Supermemory with one click." + + textContainer.appendChild(title) + textContainer.appendChild(description) + + // Close button + const closeButton = document.createElement("button") + closeButton.setAttribute("aria-label", "Close onboarding toast") + closeButton.style.cssText = ` + position: absolute; + top: 0; + right: 0; + background: transparent; + border: none; + cursor: pointer; + padding: 4px; + color: #9ca3af; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: background-color 0.2s; + ` + closeButton.innerHTML = ` + + ` + closeButton.addEventListener("mouseenter", () => { + closeButton.style.backgroundColor = "#f3f4f6" + }) + closeButton.addEventListener("mouseleave", () => { + closeButton.style.backgroundColor = "transparent" + }) + closeButton.addEventListener("click", () => { + dismissToast(toast) + }) + + header.appendChild(icon) + header.appendChild(textContainer) + header.appendChild(closeButton) + + // Action buttons + const buttonsContainer = document.createElement("div") + buttonsContainer.style.cssText = "display: flex; gap: 8px; margin-top: 4px;" + + const importButton = document.createElement("button") + importButton.style.cssText = ` + padding: 8px 16px; + border: none; + border-radius: 8px; + background: linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%); + color: white; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: opacity 0.2s; + font-family: inherit; + ` + importButton.textContent = "Import now" + importButton.addEventListener("mouseenter", () => { + importButton.style.opacity = "0.9" + }) + importButton.addEventListener("mouseleave", () => { + importButton.style.opacity = "1" + }) + importButton.addEventListener("click", async () => { + dismissToast(toast) + await openImportModal() + }) + + const learnMoreButton = document.createElement("button") + learnMoreButton.style.cssText = ` + padding: 8px 16px; + border: 1px solid #e5e7eb; + border-radius: 8px; + background: transparent; + color: #374151; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; + font-family: inherit; + ` + learnMoreButton.textContent = "Learn more" + learnMoreButton.addEventListener("mouseenter", () => { + learnMoreButton.style.backgroundColor = "#f9fafb" + }) + learnMoreButton.addEventListener("mouseleave", () => { + learnMoreButton.style.backgroundColor = "transparent" + }) + learnMoreButton.addEventListener("click", () => { + window.open( + "https://docs.supermemory.ai/connectors/twitter", + "_blank", + ) + }) + + buttonsContainer.appendChild(importButton) + buttonsContainer.appendChild(learnMoreButton) + + // Progress bar container + const progressBarContainer = document.createElement("div") + progressBarContainer.setAttribute("role", "progressbar") + progressBarContainer.setAttribute("aria-valuemin", "0") + progressBarContainer.setAttribute("aria-valuemax", "100") + progressBarContainer.setAttribute("aria-valuenow", "0") + progressBarContainer.setAttribute("aria-label", "Onboarding toast auto-dismiss progress") + progressBarContainer.style.cssText = ` + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background: #e5e7eb; + ` + + const progressBar = document.createElement("div") + progressBar.style.cssText = ` + height: 100%; + background: linear-gradient(90deg, #0ff0d2, #5bd3fb, #1e0ff0); + transform-origin: left; + animation: smProgressGrow ${duration}ms linear forwards; + ` + + // Update progress bar ARIA value as animation progresses + const startTime = Date.now() + const updateProgress = () => { + const elapsed = Date.now() - startTime + const progress = Math.min(100, Math.round((elapsed / duration) * 100)) + progressBarContainer.setAttribute("aria-valuenow", String(progress)) + if (progress < 100) { + requestAnimationFrame(updateProgress) + } + } + requestAnimationFrame(updateProgress) + + progressBarContainer.appendChild(progressBar) + + // Assemble toast + toast.appendChild(header) + toast.appendChild(buttonsContainer) + toast.appendChild(progressBarContainer) + + document.body.appendChild(toast) + + // Auto-dismiss after duration + setTimeout(() => { + if (document.body.contains(toast)) { + dismissToast(toast) + } + }, duration) +} + +/** + * Dismiss the toast with animation + */ +function dismissToast(toast: HTMLElement) { + toast.style.animation = "smFadeOut 0.3s ease-out forwards" + setTimeout(() => { + if (document.body.contains(toast)) { + toast.remove() + } + }, 300) +} + +/** + * Remove all Twitter-specific injected UI + */ +function removeAllTwitterUI() { + // Remove import button (legacy) + if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { + DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON) + } + // Remove onboarding toast + if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_ONBOARDING_TOAST)) { + DOMUtils.removeElement(ELEMENT_IDS.TWITTER_ONBOARDING_TOAST) + } + // Remove import progress toast + if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST)) { + DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST) + } + // Remove any folder buttons + document.querySelectorAll("[data-supermemory-button]").forEach((button) => { + button.remove() + }) +} + +/** + * Shows or updates the import progress toast in the bottom-right + */ +function showOrUpdateImportProgressToast(message: string, isComplete = false) { + let toast = document.getElementById(ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST) + + if (!toast) { + // Ensure animation styles are available + if (!document.getElementById("supermemory-onboarding-toast-styles")) { + const style = document.createElement("style") + style.id = "supermemory-onboarding-toast-styles" + style.textContent = ` + @keyframes smSlideInUp { + from { transform: translateY(100%); opacity: 0; } + to { transform: translateY(0); opacity: 1; } + } + @keyframes smFadeOut { + from { transform: translateY(0); opacity: 1; } + to { transform: translateY(100%); opacity: 0; } + } + @keyframes smPulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } + } + ` + document.head.appendChild(style) + } + + // Create new toast + toast = document.createElement("div") + toast.id = ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST + toast.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + z-index: 2147483647; + background: #ffffff; + border-radius: 12px; + padding: 14px 16px; + display: flex; + align-items: center; + gap: 12px; + font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + color: #374151; + min-width: 280px; + max-width: 360px; + box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18), 0 1.5px 6px 0 rgba(0,0,0,0.12); + animation: smSlideInUp 0.3s ease-out; + ` + + const iconUrl = browser.runtime.getURL("/icon-16.png") + const icon = document.createElement("img") + icon.src = iconUrl + icon.alt = "Supermemory" + icon.id = "sm-import-progress-icon" + icon.style.cssText = + "width: 20px; height: 20px; border-radius: 4px; flex-shrink: 0; animation: smPulse 1.5s ease-in-out infinite;" + + const textSpan = document.createElement("span") + textSpan.id = "sm-import-progress-text" + textSpan.style.cssText = "font-weight: 500; flex: 1;" + textSpan.textContent = message + + toast.appendChild(icon) + toast.appendChild(textSpan) + document.body.appendChild(toast) + } else { + // Update existing toast + const textSpan = toast.querySelector( + "#sm-import-progress-text", + ) as HTMLSpanElement + if (textSpan) { + textSpan.textContent = message + } + } + + // Style for completion + if (isComplete) { + const icon = toast.querySelector( + "#sm-import-progress-icon", + ) as HTMLImageElement + if (icon) { + icon.style.animation = "none" + icon.style.opacity = "1" + } + + const textSpan = toast.querySelector( + "#sm-import-progress-text", + ) as HTMLSpanElement + if (textSpan) { + textSpan.style.color = "#059669" + } + + // Auto-dismiss after 4 seconds on completion + setTimeout(() => { + const existingToast = document.getElementById( + ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST, + ) + if (existingToast) { + dismissToast(existingToast) + } + }, 4000) + } +} + +export function updateTwitterImportUI(message: { + type: string + importedMessage?: string + totalImported?: number +}) { + if (message.type === MESSAGE_TYPES.IMPORT_UPDATE && message.importedMessage) { + showOrUpdateImportProgressToast(message.importedMessage, false) + } + + if (message.type === MESSAGE_TYPES.IMPORT_DONE) { + showOrUpdateImportProgressToast( + `✓ Imported ${message.totalImported} tweets!`, + true, + ) + } +} + +export async function handleTwitterNavigation() { + if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { + return + } + + if (window.location.pathname === "/i/bookmarks") { + addTwitterImportButtonForFolders() + await handleBookmarksPageLoad() + } else { + removeAllTwitterUI() + } +} + +/** + * Adds import buttons to bookmark folders + */ function addTwitterImportButtonForFolders() { if (window.location.pathname !== "/i/bookmarks") { return @@ -138,6 +611,9 @@ function addTwitterImportButtonForFolders() { }) } +/** + * Adds an import button to a bookmark folder element + */ function addButtonToElement(element: HTMLElement) { if (element.querySelector("[data-supermemory-button]")) { return @@ -148,9 +624,8 @@ function addButtonToElement(element: HTMLElement) { const button = createSaveTweetElement(async () => { const url = element.getAttribute("href") const bookmarkCollectionId = url?.split("/").pop() - console.log("Bookmark collection ID:", bookmarkCollectionId) if (bookmarkCollectionId) { - await showProjectSelectionModal(bookmarkCollectionId) + await showFolderProjectSelectionModal(bookmarkCollectionId) } }) @@ -164,132 +639,55 @@ function addButtonToElement(element: HTMLElement) { element.style.padding = "10px" } -export function updateTwitterImportUI(message: { - type: string - importedMessage?: string - totalImported?: number -}) { - const importButton = document.getElementById( - ELEMENT_IDS.TWITTER_IMPORT_BUTTON, - ) - if (!importButton) return - - const existingImg = importButton.querySelector("img") - if (existingImg) { - existingImg.remove() - const iconUrl = browser.runtime.getURL("/icon-16.png") - importButton.style.backgroundImage = `url("${iconUrl}")` - importButton.style.backgroundRepeat = "no-repeat" - importButton.style.backgroundSize = "20px 20px" - importButton.style.backgroundPosition = "8px center" - importButton.style.padding = "10px 16px 10px 32px" - } - - let textSpan = importButton.querySelector( - "#sm-import-text", - ) as HTMLSpanElement - if (!textSpan) { - textSpan = document.createElement("span") - textSpan.id = "sm-import-text" - textSpan.style.cssText = "font-weight: 500; font-size: 14px;" - importButton.appendChild(textSpan) - } - - if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { - textSpan.textContent = message.importedMessage || "" - importButton.style.cursor = "default" - } - - if (message.type === MESSAGE_TYPES.IMPORT_DONE) { - textSpan.textContent = `✓ Imported ${message.totalImported} tweets!` - textSpan.style.color = "#059669" - - setTimeout(() => { - textSpan.textContent = "Import Bookmarks" - textSpan.style.color = "" - importButton.style.cursor = "pointer" - }, 3000) - } -} - -export function handleTwitterNavigation() { - if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - return - } - - if (window.location.pathname === "/i/bookmarks") { - addTwitterImportButton() - addTwitterImportButtonForFolders() - } else { - if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { - DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON) - } - document.querySelectorAll("[data-supermemory-button]").forEach((button) => { - button.remove() - }) - } -} - /** * Shows the project selection modal for folder imports - * @param bookmarkCollectionId - The ID of the bookmark collection to import */ -async function showProjectSelectionModal(bookmarkCollectionId: string) { - try { - const modal = createProjectSelectionModal( - [], - async (selectedProject) => { - modal.remove() +async function showFolderProjectSelectionModal(bookmarkCollectionId: string) { + await loadSpaceGroteskFonts() - try { - await browser.runtime.sendMessage({ - type: MESSAGE_TYPES.BATCH_IMPORT_ALL, - isFolderImport: true, - bookmarkCollectionId: bookmarkCollectionId, - selectedProject: selectedProject, - }) - } catch (error) { - console.error("Error importing bookmarks:", error) - } - }, - () => { - modal.remove() - }, - ) + const modal = createProjectSelectionModal( + [], + async (selectedProject) => { + modal.remove() - document.body.appendChild(modal) - - try { - const response = await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.FETCH_PROJECTS, - }) - - if (response.success && response.data) { - const projects = response.data - - if (projects.length === 0) { - console.warn("No projects available for import") - updateModalWithProjects(modal, []) - } else { - updateModalWithProjects(modal, projects) - } - } else { - console.error("Failed to fetch projects:", response.error) - updateModalWithProjects(modal, []) + try { + await browser.runtime.sendMessage({ + type: MESSAGE_TYPES.BATCH_IMPORT_ALL, + isFolderImport: true, + bookmarkCollectionId: bookmarkCollectionId, + selectedProject: selectedProject, + }) + } catch (error) { + console.error("Error importing bookmarks:", error) } - } catch (error) { - console.error("Error fetching projects:", error) + }, + () => { + modal.remove() + }, + ) + + document.body.appendChild(modal) + + try { + const response = await browser.runtime.sendMessage({ + action: MESSAGE_TYPES.FETCH_PROJECTS, + }) + + if (response.success && response.data) { + const projects = response.data + updateModalWithProjects(modal, projects) + } else { + console.error("Failed to fetch projects:", response.error) updateModalWithProjects(modal, []) } } catch (error) { - console.error("Error showing project selection modal:", error) + console.error("Error fetching projects:", error) + updateModalWithProjects(modal, []) } } /** * Updates the modal with fetched projects - * @param modal - The modal element - * @param projects - Array of projects to populate the dropdown */ function updateModalWithProjects( modal: HTMLElement, @@ -316,10 +714,10 @@ function updateModalWithProjects( importButton.disabled = true importButton.style.cssText = ` padding: 10px 16px; - border: none; - border-radius: 8px; - background: #d1d5db; - color: #9ca3af; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.3); font-size: 14px; font-weight: 500; cursor: not-allowed; diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx index c8472da4..f61a2415 100644 --- a/apps/browser-extension/entrypoints/popup/App.tsx +++ b/apps/browser-extension/entrypoints/popup/App.tsx @@ -2,7 +2,7 @@ import { useQueryClient } from "@tanstack/react-query" import { useEffect, useState } from "react" import "./App.css" import { validateAuthToken } from "../../utils/api" -import { MESSAGE_TYPES } from "../../utils/constants" +import { MESSAGE_TYPES, STORAGE_KEYS, UI_CONFIG } from "../../utils/constants" import { useDefaultProject, useProjects, @@ -17,6 +17,7 @@ import { userData as userDataStorage, } from "../../utils/storage" import type { Project } from "../../utils/types" +import { RightArrow } from "@/components/icons" const Tooltip = ({ children, @@ -181,6 +182,11 @@ function App() { } }, [defaultProject, projects, setDefaultProjectMutation]) + // biome-ignore lint/correctness/useExhaustiveDependencies: close space selector when tab changes + useEffect(() => { + setShowProjectSelector(false) + }, [activeTab]) + const handleSaveCurrentPage = async () => { setSaving(true) @@ -262,25 +268,51 @@ function App() { if (loading) { return ( -
-
- supermemory +
+ -
-
+
+
- Loading... + Loading...
@@ -303,26 +335,56 @@ function App() { return (
-
-
supermemory
+ -
+
{userSignedIn ? (
{/* Tab Navigation */} @@ -408,43 +470,119 @@ function App() {
- {/* Project Selection */} -
- + )} +
+ + {showProjectSelector ? ( +
+ {loadingProjects ? ( +
+ Loading spaces... +
+ ) : ( + projects.map((project) => ( + + )) + )} +
+ ) : ( +
- + + )}
{/* Save Button at Bottom */} @@ -496,7 +634,7 @@ function App() {
+
+
@@ -588,7 +772,9 @@ function App() {
) : userData?.email ? ( <> - Email + + Email + -

+

Chat Integration

@@ -654,53 +840,6 @@ function App() {
)} - - {showProjectSelector && ( -
-
- Select the Project - -
- {loadingProjects ? ( -
- Loading projects... -
- ) : ( -
- {projects.map((project) => ( - - ))} -
- )} -
- )}
) : (
@@ -718,18 +857,18 @@ function App() {
) : (
-

+

Login to unlock all chrome extension features

    -
  • +
  • Save any page to your supermemory
  • -
  • +
  • Import all your Twitter / X Bookmarks
  • -
  • +
  • Import your ChatGPT Memories
@@ -737,7 +876,7 @@ function App() { )}
-

+

Having trouble logging in?{" "}