Merge branch 'main' of https://github.com/supermemoryai/supermemory into fix/chat-history-mobile-view

This commit is contained in:
Dhravya Shah 2026-02-19 10:56:37 -08:00
commit 581b946c38
114 changed files with 767 additions and 1025 deletions

View file

@ -6,7 +6,6 @@ import {
} from "../utils/api"
import {
CONTAINER_TAGS,
CONTEXT_MENU_IDS,
MESSAGE_TYPES,
POSTHOG_EVENT_KEY,
} from "../utils/constants"
@ -26,18 +25,6 @@ export default defineBackground(() => {
let twitterImporter: TwitterImporter | null = null
browser.runtime.onInstalled.addListener(async (details) => {
browser.contextMenus.create({
id: CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY,
title: "sync to supermemory",
contexts: ["selection", "page", "link"],
})
browser.contextMenus.create({
id: CONTEXT_MENU_IDS.SEARCH_SUPERMEMORY,
title: "search supermemory",
contexts: ["selection"],
})
if (details.reason === "install") {
await trackEvent("extension_installed", {
reason: details.reason,
@ -59,38 +46,6 @@ export default defineBackground(() => {
["requestHeaders", "extraHeaders"],
)
// Handle context menu clicks.
browser.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId === CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY) {
if (tab?.id) {
try {
await browser.tabs.sendMessage(tab.id, {
action: MESSAGE_TYPES.SAVE_MEMORY,
actionSource: "context_menu",
})
} catch (error) {
console.error("Failed to send message to content script:", error)
}
}
}
if (info.menuItemId === CONTEXT_MENU_IDS.SEARCH_SUPERMEMORY) {
if (tab?.id && info.selectionText) {
try {
await browser.tabs.sendMessage(tab.id, {
action: MESSAGE_TYPES.OPEN_SEARCH_PANEL,
data: info.selectionText,
})
} catch (error) {
console.error(
"Failed to send search message to content script:",
error,
)
}
}
}
})
// Send message to current active tab.
const sendMessageToCurrentTab = async (message: string) => {
const tabs = await browser.tabs.query({
@ -319,30 +274,6 @@ export default defineBackground(() => {
return true
}
if (message.action === MESSAGE_TYPES.SEARCH_SELECTION) {
;(async () => {
try {
const query = message.data as string
const responseData = await searchMemories(query)
await trackEvent(POSTHOG_EVENT_KEY.SELECTION_SEARCH_TRIGGERED, {
query_length: query.length,
})
sendResponse({ success: true, data: responseData })
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error"
const isAuthError =
errorMessage.includes("Authentication") ||
errorMessage.includes("token")
sendResponse({
success: false,
error: errorMessage,
isAuthError,
})
}
})()
return true
}
},
)
})

View file

@ -2,10 +2,6 @@ import { DOMAINS, MESSAGE_TYPES } from "../../utils/constants"
import { DOMUtils } from "../../utils/ui-components"
import { initializeChatGPT } from "./chatgpt"
import { initializeClaude } from "./claude"
import {
handleOpenSearchPanel,
initializeSelectionSearch,
} from "./selection-search"
import {
saveMemory,
setupGlobalKeyboardShortcut,
@ -28,8 +24,6 @@ export default defineContentScript({
DOMUtils.showToast(message.state)
} else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
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) {
@ -74,9 +68,6 @@ export default defineContentScript({
initializeT3()
initializeTwitter()
// Initialize universal selection search
initializeSelectionSearch()
// Start observing for dynamic changes
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", observeForDynamicChanges)

View file

@ -1,647 +0,0 @@
import { ELEMENT_IDS, MESSAGE_TYPES, UI_CONFIG } from "../../utils/constants"
// State
let currentQuery = ""
let fabElement: HTMLElement | null = null
let panelElement: HTMLElement | null = null
const selectedResults: Set<number> = new Set()
/**
* Get the selection rectangle for positioning the FAB
*/
function getSelectionRect(): DOMRect | null {
const selection = window.getSelection()
if (!selection || selection.rangeCount === 0) return null
const range = selection.getRangeAt(0)
return range.getBoundingClientRect()
}
/**
* Check if the selection is inside our extension UI
*/
function isSelectionInsideExtensionUI(): boolean {
const selection = window.getSelection()
if (!selection || selection.rangeCount === 0) return false
const anchorNode = selection.anchorNode
if (!anchorNode) return false
const element =
anchorNode.nodeType === Node.ELEMENT_NODE
? (anchorNode as Element)
: anchorNode.parentElement
if (!element) return false
// Check if selection is inside FAB or panel
return (
!!element.closest(`#${ELEMENT_IDS.SELECTION_SEARCH_FAB}`) ||
!!element.closest(`#${ELEMENT_IDS.SELECTION_SEARCH_PANEL}`)
)
}
/**
* Create the floating action button (FAB)
*/
function createFAB(): HTMLElement {
const fab = document.createElement("div")
fab.id = ELEMENT_IDS.SELECTION_SEARCH_FAB
const iconUrl = browser.runtime.getURL("/icon-16.png")
fab.innerHTML = `
<img src="${iconUrl}" width="16" height="16" alt="Search" style="border-radius: 2px;" />
<span>Search</span>
`
fab.style.cssText = `
position: fixed;
z-index: 2147483646;
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: #05070A;
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
font-weight: 500;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.15s ease;
user-select: none;
`
fab.addEventListener("mouseenter", () => {
fab.style.background = "#0F151F"
fab.style.borderColor = "rgba(255, 255, 255, 0.2)"
})
fab.addEventListener("mouseleave", () => {
fab.style.background = "#05070A"
fab.style.borderColor = "rgba(255, 255, 255, 0.1)"
})
fab.addEventListener("click", (e) => {
e.preventDefault()
e.stopPropagation()
triggerSearch()
})
return fab
}
/**
* Show the FAB near the selection
*/
function showFAB(rect: DOMRect, text: string) {
hideFAB()
currentQuery = text
fabElement = createFAB()
// Position FAB above the selection, centered
const fabWidth = 90 // approximate width
let left = rect.left + rect.width / 2 - fabWidth / 2
let top = rect.top - 40
// Ensure FAB stays within viewport
if (left < 10) left = 10
if (left + fabWidth > window.innerWidth - 10) {
left = window.innerWidth - fabWidth - 10
}
if (top < 10) {
// Show below selection if not enough space above
top = rect.bottom + 10
}
fabElement.style.left = `${left}px`
fabElement.style.top = `${top}px`
document.body.appendChild(fabElement)
}
/**
* Hide the FAB
*/
export function hideFAB() {
if (fabElement) {
fabElement.remove()
fabElement = null
}
}
/**
* Trigger search with the current query
*/
async function triggerSearch() {
if (!currentQuery) return
hideFAB()
showPanel(currentQuery, "loading")
try {
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.SEARCH_SELECTION,
data: currentQuery,
})
if (response.success) {
showPanel(currentQuery, "results", response.data)
} else if (response.isAuthError) {
showPanel(currentQuery, "auth_error")
} else {
showPanel(currentQuery, "error", null, response.error)
}
} catch (error) {
console.error("Search failed:", error)
showPanel(
currentQuery,
"error",
null,
error instanceof Error ? error.message : "Search failed",
)
}
}
/**
* Create and show the search results panel
*/
function showPanel(
query: string,
state: "loading" | "results" | "error" | "auth_error",
data?: unknown,
errorMessage?: string,
) {
hidePanel()
selectedResults.clear()
panelElement = document.createElement("div")
panelElement.id = ELEMENT_IDS.SELECTION_SEARCH_PANEL
panelElement.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2147483647;
width: 420px;
max-width: 90vw;
max-height: 70vh;
background: #05070A;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
display: flex;
flex-direction: column;
overflow: hidden;
`
// Header
const header = document.createElement("div")
header.style.cssText = `
padding: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
`
const iconUrl = browser.runtime.getURL("/icon-16.png")
header.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px;">
<img src="${iconUrl}" width="20" height="20" alt="supermemory" style="border-radius: 4px;" />
<span style="font-size: 14px; font-weight: 600; color: #ffffff;">Search Results</span>
</div>
<button id="sm-panel-close" style="
background: transparent;
border: none;
color: #737373;
cursor: pointer;
padding: 4px;
font-size: 18px;
line-height: 1;
transition: color 0.15s;
">×</button>
`
panelElement.appendChild(header)
// Query display
const queryDisplay = document.createElement("div")
queryDisplay.style.cssText = `
padding: 12px 16px;
background: rgba(91, 126, 245, 0.04);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
flex-shrink: 0;
`
queryDisplay.innerHTML = `
<div style="font-size: 12px; color: #737373; margin-bottom: 4px;">Searching for:</div>
<div style="font-size: 13px; color: #ffffff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${escapeHtml(query)}</div>
`
panelElement.appendChild(queryDisplay)
// Content area
const content = document.createElement("div")
content.id = "sm-panel-content"
content.style.cssText = `
flex: 1;
overflow-y: auto;
padding: 16px;
min-height: 150px;
`
if (state === "loading") {
content.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 150px; gap: 12px;">
<div style="width: 24px; height: 24px; border: 2px solid rgba(255,255,255,0.1); border-top-color: #5BD3FB; border-radius: 50%; animation: sm-spin 0.8s linear infinite;"></div>
<span style="font-size: 14px; color: #737373;">Searching memories...</span>
</div>
`
} else if (state === "auth_error") {
content.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 150px; gap: 16px; text-align: center;">
<div style="font-size: 14px; color: #ef4444;">Sign in required</div>
<p style="font-size: 13px; color: #737373; margin: 0;">Please sign in to supermemory to search your memories.</p>
<button id="sm-sign-in-btn" style="
padding: 10px 20px;
background: linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%);
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
">Sign in</button>
</div>
`
} else if (state === "error") {
content.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 150px; gap: 12px; text-align: center;">
<div style="color: #ef4444; font-size: 14px;">Search failed</div>
<p style="font-size: 13px; color: #737373; margin: 0;">${escapeHtml(errorMessage || "An error occurred")}</p>
</div>
`
} else if (state === "results") {
const response = data as {
searchResults?: { results?: Array<{ memory?: string; id?: string }> }
}
const results = response?.searchResults?.results || []
if (results.length === 0) {
content.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 150px; gap: 12px; text-align: center;">
<div style="font-size: 14px; color: #737373;">No memories found</div>
<p style="font-size: 13px; color: #525966; margin: 0;">Try a different search query.</p>
</div>
`
} else {
content.innerHTML = results
.map(
(result, index) => `
<div class="sm-result-item" data-index="${index}" style="
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
background: rgba(91, 126, 245, 0.04);
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
transition: background 0.15s;
border: 1px solid transparent;
">
<input type="checkbox" data-index="${index}" style="
margin-top: 2px;
width: 16px;
height: 16px;
cursor: pointer;
accent-color: #5BD3FB;
" />
<div style="flex: 1; min-width: 0;">
<p style="font-size: 13px; color: #e5e5e5; margin: 0; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">${escapeHtml(result.memory || "")}</p>
</div>
</div>
`,
)
.join("")
}
}
panelElement.appendChild(content)
// Footer with copy button (only for results)
if (state === "results") {
const response = data as {
searchResults?: { results?: Array<{ memory?: string }> }
}
const results = response?.searchResults?.results || []
if (results.length > 0) {
const footer = document.createElement("div")
footer.style.cssText = `
padding: 12px 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
`
footer.innerHTML = `
<span id="sm-selected-count" style="font-size: 13px; color: #737373;">0 selected</span>
<button id="sm-copy-btn" style="
padding: 10px 16px;
background: linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%);
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
opacity: 0.5;
transition: opacity 0.15s;
" disabled>Copy selected</button>
`
panelElement.appendChild(footer)
}
}
// Add animations style
if (!document.getElementById("sm-panel-styles")) {
const style = document.createElement("style")
style.id = "sm-panel-styles"
style.textContent = `
@keyframes sm-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.sm-result-item:hover {
background: rgba(91, 126, 245, 0.08) !important;
}
.sm-result-item.selected {
border-color: rgba(91, 190, 251, 0.3) !important;
background: rgba(91, 126, 245, 0.08) !important;
}
`
document.head.appendChild(style)
}
document.body.appendChild(panelElement)
// Event listeners
setupPanelEventListeners(data)
}
/**
* Setup event listeners for the panel
*/
function setupPanelEventListeners(data: unknown) {
if (!panelElement) return
// Close button
const closeBtn = panelElement.querySelector("#sm-panel-close")
closeBtn?.addEventListener("click", hidePanel)
// Sign in button
const signInBtn = panelElement.querySelector("#sm-sign-in-btn")
signInBtn?.addEventListener("click", () => {
window.open(
import.meta.env.PROD
? "https://app.supermemory.ai/login"
: "http://localhost:3000/login",
"_blank",
)
})
// Result item checkboxes
const checkboxes = panelElement.querySelectorAll(
'.sm-result-item input[type="checkbox"]',
)
checkboxes.forEach((checkbox) => {
checkbox.addEventListener("change", (e) => {
const target = e.target as HTMLInputElement
const index = Number.parseInt(target.dataset.index || "0", 10)
const item = target.closest(".sm-result-item") as HTMLElement
if (target.checked) {
selectedResults.add(index)
item?.classList.add("selected")
} else {
selectedResults.delete(index)
item?.classList.remove("selected")
}
updateSelectionUI()
})
})
// Result item click (toggle checkbox)
const items = panelElement.querySelectorAll(".sm-result-item")
items.forEach((item) => {
item.addEventListener("click", (e) => {
const target = e.target as HTMLElement
if (target.tagName === "INPUT") return
const checkbox = item.querySelector(
'input[type="checkbox"]',
) as HTMLInputElement
if (checkbox) {
checkbox.checked = !checkbox.checked
checkbox.dispatchEvent(new Event("change"))
}
})
})
// Copy button
const copyBtn = panelElement.querySelector("#sm-copy-btn")
copyBtn?.addEventListener("click", () => {
copySelectedResults(data)
})
// Close on escape
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
hidePanel()
}
}
document.addEventListener("keydown", handleKeyDown)
// Close on outside click
const handleOutsideClick = (e: MouseEvent) => {
if (panelElement && !panelElement.contains(e.target as Node)) {
hidePanel()
}
}
setTimeout(() => {
document.addEventListener("click", handleOutsideClick)
}, 100)
// Cleanup listeners when panel is removed
const observer = new MutationObserver(() => {
if (!document.contains(panelElement)) {
document.removeEventListener("keydown", handleKeyDown)
document.removeEventListener("click", handleOutsideClick)
observer.disconnect()
}
})
observer.observe(document.body, { childList: true })
}
/**
* Update the selection count UI
*/
function updateSelectionUI() {
const countEl = document.getElementById("sm-selected-count")
const copyBtn = document.getElementById("sm-copy-btn") as HTMLButtonElement
if (countEl) {
countEl.textContent = `${selectedResults.size} selected`
}
if (copyBtn) {
copyBtn.disabled = selectedResults.size === 0
copyBtn.style.opacity = selectedResults.size === 0 ? "0.5" : "1"
}
}
/**
* Copy selected results to clipboard
*/
async function copySelectedResults(data: unknown) {
const response = data as {
searchResults?: { results?: Array<{ memory?: string }> }
}
const results = response?.searchResults?.results || []
const selectedMemories = Array.from(selectedResults)
.sort((a, b) => a - b)
.map((index) => results[index]?.memory)
.filter(Boolean)
if (selectedMemories.length === 0) return
// Format the copied content
const formattedContent = selectedMemories
.map((memory, i) => `${i + 1}. ${memory}`)
.join("\n\n")
try {
await navigator.clipboard.writeText(formattedContent)
// Show copied feedback
const copyBtn = document.getElementById("sm-copy-btn")
if (copyBtn) {
const originalText = copyBtn.textContent
copyBtn.textContent = "Copied!"
setTimeout(() => {
if (copyBtn) copyBtn.textContent = originalText
}, 1500)
}
// Track event
browser.runtime.sendMessage({
action: MESSAGE_TYPES.CAPTURE_PROMPT,
data: {
prompt: `Copied ${selectedMemories.length} memories`,
platform: "selection_search",
source: "copy_selected",
},
})
} catch (error) {
console.error("Failed to copy to clipboard:", error)
}
}
/**
* Hide the panel
*/
export function hidePanel() {
if (panelElement) {
panelElement.remove()
panelElement = null
}
selectedResults.clear()
}
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(text: string): string {
const div = document.createElement("div")
div.textContent = text
return div.innerHTML
}
/**
* Handle selection change
*/
function handleSelectionChange() {
const selection = window.getSelection()
const text = selection?.toString().trim() || ""
// Hide FAB if selection is empty or inside extension UI
if (
!text ||
text.length < UI_CONFIG.SELECTION_MIN_LENGTH ||
text.length > UI_CONFIG.SELECTION_MAX_LENGTH ||
isSelectionInsideExtensionUI()
) {
hideFAB()
return
}
const rect = getSelectionRect()
if (rect && rect.width > 0 && rect.height > 0) {
showFAB(rect, text)
}
}
/**
* Handle message from background to open search panel
*/
export function handleOpenSearchPanel(query: string) {
currentQuery = query
hideFAB()
triggerSearch()
}
/**
* Initialize selection search functionality
*/
export function initializeSelectionSearch() {
// Listen for mouseup to detect selection
document.addEventListener("mouseup", () => {
// Small delay to ensure selection is complete
setTimeout(handleSelectionChange, 10)
})
// Listen for keyup for keyboard selection
document.addEventListener("keyup", (e) => {
if (e.shiftKey) {
setTimeout(handleSelectionChange, 10)
}
})
// Hide FAB when clicking elsewhere
document.addEventListener("mousedown", (e) => {
const target = e.target as HTMLElement
if (
fabElement &&
!fabElement.contains(target) &&
!panelElement?.contains(target)
) {
// Don't hide immediately to allow FAB click
setTimeout(() => {
const selection = window.getSelection()
if (!selection || selection.toString().trim().length === 0) {
hideFAB()
}
}, 100)
}
})
}

View file

@ -24,8 +24,6 @@ export const ELEMENT_IDS = {
CLAUDE_INPUT_BAR_ELEMENT: "sm-claude-input-bar-element",
T3_INPUT_BAR_ELEMENT: "sm-t3-input-bar-element",
PROJECT_SELECTION_MODAL: "sm-project-selection-modal",
SELECTION_SEARCH_FAB: "sm-selection-search-fab",
SELECTION_SEARCH_PANEL: "sm-selection-search-panel",
} as const
/**
@ -51,8 +49,6 @@ 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
SELECTION_MIN_LENGTH: 3, // minimum characters to show FAB
SELECTION_MAX_LENGTH: 500, // maximum characters to show FAB
} as const
/**
@ -86,16 +82,9 @@ export const MESSAGE_TYPES = {
GET_RELATED_MEMORIES: "sm-get-related-memories",
CAPTURE_PROMPT: "sm-capture-prompt",
FETCH_PROJECTS: "sm-fetch-projects",
SEARCH_SELECTION: "sm-search-selection",
OPEN_SEARCH_PANEL: "sm-open-search-panel",
TWITTER_IMPORT_OPEN_MODAL: "sm-twitter-import-open-modal",
} as const
export const CONTEXT_MENU_IDS = {
SAVE_TO_SUPERMEMORY: "sm-save-to-supermemory",
SEARCH_SUPERMEMORY: "sm-search-supermemory",
} as const
export const POSTHOG_EVENT_KEY = {
TWITTER_IMPORT_STARTED: "twitter_import_started",
SAVE_MEMORY_ATTEMPTED: "save_memory_attempted",
@ -107,6 +96,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",
SELECTION_SEARCH_TRIGGERED: "selection_search_triggered",
SELECTION_SEARCH_COPIED: "selection_search_copied",
} as const

View file

@ -11,8 +11,8 @@ export default defineConfig({
manifest: {
name: "supermemory",
homepage_url: "https://supermemory.ai",
version: "6.1.0",
permissions: ["contextMenus", "storage", "activeTab", "webRequest", "tabs"],
version: "6.1.1",
permissions: ["storage", "activeTab", "webRequest", "tabs"],
host_permissions: [
"*://x.com/*",
"*://twitter.com/*",

View file

@ -11,51 +11,53 @@ This integration requires the **Supermemory Pro plan**. [Upgrade here](https://c
[OpenClaw](https://github.com/supermemoryai/openclaw-supermemory) is a multi-platform AI messaging gateway that connects to WhatsApp, Telegram, Discord, Slack, iMessage, and other messaging channels. The Supermemory plugin gives OpenClaw memory across every channel.
## Get Your API Key
Create a Supermemory API key from the [API Keys](https://console.supermemory.ai/keys) page, then add it to your shell profile so it persists across sessions:
<Tabs>
<Tab title="macOS / Linux (zsh)">
```bash
echo 'export SUPERMEMORY_OPENCLAW_API_KEY="sm_..."' >> ~/.zshrc
source ~/.zshrc
```
</Tab>
<Tab title="macOS / Linux (bash)">
```bash
echo 'export SUPERMEMORY_OPENCLAW_API_KEY="sm_..."' >> ~/.bashrc
source ~/.bashrc
```
</Tab>
<Tab title="Windows (PowerShell)">
```powershell
[System.Environment]::SetEnvironmentVariable("SUPERMEMORY_OPENCLAW_API_KEY", "sm_...", "User")
```
Restart your terminal after running this.
</Tab>
</Tabs>
## Install the Plugin
Get started by installing the plugin with a single command.
```bash
openclaw plugins install @supermemory/openclaw-supermemory
```
Restart OpenClaw after installing.
## Setup
Run the setup command and enter your API key when prompted.
```bash
openclaw supermemory setup
```
Enter your API key from [console.supermemory.ai](https://console.supermemory.ai). That's it.
<AccordionGroup>
<Accordion title="Advanced Setup">
Configure all options interactively with the advanced setup command:
```bash
openclaw supermemory setup-advanced
```
This lets you configure: container tag, auto-recall, auto-capture, capture mode, custom container tags, and more.
See [Configuration Options](#configuration-options) for all available settings.
</Accordion>
</AccordionGroup>
## How It Works
Once installed, the plugin runs automatically with zero interaction:
Once installed, the plugin runs automatically with zero interaction.
- **Auto-Recall** — Before every AI turn, Supermemory is queried for relevant memories and the user's profile. These are injected as context so the AI sees preferences, facts, and semantically similar past conversations.
- **Auto-Capture** — After every AI turn, the conversation exchange is sent to Supermemory for extraction and long-term storage. Supermemory handles deduplication and profile building.
- **Custom Container Tags** — When enabled via advanced setup, define custom memory containers (e.g., `work`, `personal`, `bookmarks`). The AI automatically picks the right container based on your instructions.
## Features
### AI Tools
The AI can use these tools autonomously during conversations:
The AI can use these tools autonomously during conversations.
| Tool | Description |
|------|-------------|
@ -66,7 +68,7 @@ The AI can use these tools autonomously during conversations:
### Slash Commands
Users can interact with memory directly in chat:
Users can interact with memory directly in chat.
| Command | Description |
|---------|-------------|
@ -75,43 +77,199 @@ Users can interact with memory directly in chat:
### CLI Commands
Manage your memory from the terminal.
```bash
openclaw supermemory search <query> # Search memories from the terminal
openclaw supermemory profile # View user profile
openclaw supermemory wipe # Delete all memories (requires confirmation)
openclaw supermemory setup # Configure API key
openclaw supermemory setup-advanced # Configure all options
openclaw supermemory status # View current configuration
openclaw supermemory search <query> # Search memories
openclaw supermemory profile # View user profile
openclaw supermemory wipe # Delete all memories (requires confirmation)
```
## Manual Configuration
<AccordionGroup>
<Accordion title="Manual Configuration">
### Configuration Options
Optional — only needed if you prefer to set the API key directly in the config file instead of the environment variable.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `apiKey` | `string` | — | Supermemory API key. |
| `containerTag` | `string` | `openclaw_{hostname}` | Root memory namespace. |
| `autoRecall` | `boolean` | `true` | Inject relevant memories before every AI turn. |
| `autoCapture` | `boolean` | `true` | Store conversation content after every turn. |
| `maxRecallResults` | `number` | `10` | Max memories injected into context per turn. |
| `profileFrequency` | `number` | `50` | Inject full user profile every N turns. |
| `captureMode` | `string` | `"all"` | `"all"` filters noise. `"everything"` captures all messages. |
| `debug` | `boolean` | `false` | Verbose debug logs. |
| `enableCustomContainerTags` | `boolean` | `false` | Enable custom container routing. |
| `customContainers` | `array` | `[]` | Custom containers with `tag` and `description`. |
| `customContainerInstructions` | `string` | `""` | Instructions for AI on container routing. |
Add the plugin to your `openclaw.json`:
### Full Example
```json5
{
"plugins": {
"entries": {
"openclaw-supermemory": {
"enabled": true,
"config": {
"apiKey": "sm_..."
```json
{
"plugins": {
"entries": {
"openclaw-supermemory": {
"enabled": true,
"config": {
"apiKey": "${SUPERMEMORY_OPENCLAW_API_KEY}",
"containerTag": "my_memory",
"autoRecall": true,
"autoCapture": true,
"maxRecallResults": 10,
"profileFrequency": 50,
"captureMode": "all",
"debug": false,
"enableCustomContainerTags": true,
"customContainers": [
{ "tag": "work", "description": "Work-related memories" },
{ "tag": "personal", "description": "Personal notes" }
],
"customContainerInstructions": "Store work tasks in 'work', personal stuff in 'personal'"
}
}
}
}
}
}
}
```
```
</Accordion>
</AccordionGroup>
### Advanced Options
## FAQ
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `autoRecall` | `boolean` | `true` | Inject relevant memories before every AI turn. |
| `autoCapture` | `boolean` | `true` | Store conversation content after every turn. |
| `maxRecallResults` | `number` | `10` | Max memories injected into context per turn. |
| `profileFrequency` | `number` | `50` | Inject full user profile every N turns. |
| `captureMode` | `string` | `"all"` | `"all"` filters noise. `"everything"` captures all messages. |
| `debug` | `boolean` | `false` | Verbose debug logs. |
<AccordionGroup>
<Accordion title="How do I install OpenClaw with Supermemory?">
1. Install the plugin:
```bash
openclaw plugins install @supermemory/openclaw-supermemory
```
2. Run the setup wizard:
```bash
openclaw supermemory setup
```
3. When prompted, paste your API key from [console.supermemory.ai](https://console.supermemory.ai). The key starts with `sm_`.
4. Restart OpenClaw to activate the plugin:
```bash
openclaw gateway --force
```
5. Verify the connection:
```bash
openclaw supermemory status
```
That's it — Supermemory is now connected and will automatically recall and capture memories across all your channels.
</Accordion>
<Accordion title="I have Twitter bookmarks in a separate container. How do I use them with Supermemory?">
Use the advanced setup to enable custom container tags and add a read-only Twitter bookmarks container.
```bash
openclaw supermemory setup-advanced
```
When prompted, enter:
- **API key**: paste your `sm_` key
- **Container tag**: keep the default (or set your preferred root tag)
- **Auto recall**: `true` (recommended)
- **Auto capture**: `true`
- **Enable custom container tags**: `true`
- **Custom container tags**: `twitter-bookmarks:Twitter bookmarks saved from Twitter`
<Tip>
Custom container tags use the format `tag:description`, separated by a colon. For example: `twitter-bookmarks:Twitter bookmarks saved from Twitter`.
</Tip>
When asked for **custom container instructions**, enter something like:
```
Whenever the user asks about Twitter bookmarks, search the twitter-bookmarks container.
Never save anything to the twitter-bookmarks container — it is read-only and populated directly from Twitter.
```
This ensures the AI references your Twitter bookmarks when relevant but never writes to that container.
</Accordion>
<Accordion title="How do I separate work (Slack + Gmail) and personal (Telegram + WhatsApp) memories?">
Use custom container tags to route memories by channel. Run the advanced setup:
```bash
openclaw supermemory setup-advanced
```
When prompted, enter:
- **API key**: paste your `sm_` key
- **Container tag**: keep the default
- **Auto recall**: `true`
- **Auto capture**: `true`
- **Enable custom container tags**: `true`
- **Custom container tags**:
- `work:Work-related memories from Slack and Gmail`
- `personal:Personal memories from Telegram and WhatsApp`
When asked for **custom container instructions**, enter:
```
When the active channel is slack or gmail, always use the work container for storing and recalling memories. Never mix personal memories into work context.
When the active channel is telegram or whatsapp, always use the personal container. Do not recall or store work memories in personal conversations.
```
This keeps your work and personal memories completely separated — Slack and Gmail conversations only see `work` memories, while Telegram and WhatsApp only see `personal` memories.
</Accordion>
<Accordion title="I'm not technical. How can I set up advanced mode?">
Ask the OpenClaw agent directly in any chat. No terminal needed.
```
What are all the available configurations for the Supermemory plugin advanced mode?
```
The agent will list every option. Then tell it what you want:
```
Set up my Supermemory plugin with these settings:
- Auto recall: true
- Auto capture: true
- Enable custom container tags: true
- Custom containers:
- work: Work-related memories from Slack and Gmail
- personal: Personal memories from Telegram and WhatsApp
- Custom instructions: When on Slack or Gmail, use the work container.
When on Telegram or WhatsApp, use the personal container.
```
Each custom container needs a **tag** and a **description** — e.g. `work: Work-related memories from Slack and Gmail`. The description helps the AI understand what belongs in that container.
<Tip>
You can change settings anytime by just telling the agent. For example: "Add a new container called `twitter-bookmarks` with description `Twitter bookmarks saved from Twitter` and make it read-only".
</Tip>
</Accordion>
<Accordion title="All my session memories go into one container. How can I separate them by channel?">
By default, all session memories across every channel are stored under a single root-level container tag.
To store specific memories separately, use **custom container tags** — see the FAQ above on separating work and personal memories.
Automatic per-channel separation is not supported yet. If you need this, let us know — with enough requests, we'll implement it right away.
<Card title="Request this feature" icon="mail" href="mailto:support@supermemory.com?subject=Feature%20Request%3A%20Per-Channel%20Container%20Tags&body=Hey%2C%0A%0AMy%20name%20is%20%5Byour%20name%5D.%20I%20just%20saw%20that%20you%20offer%20custom%20plugins.%0A%0AThis%20is%20my%20use%20case%3A%20%5Bdescribe%20your%20use%20case%5D%0A%0AI%20would%20love%20to%20have%20separate%20container%20tags%20for%20each%20channel%20so%20my%20session%20memories%20are%20automatically%20isolated.%0A%0AThanks!">
Email us with your use case.
</Card>
</Accordion>
</AccordionGroup>
## Next Steps

View file

@ -111,7 +111,7 @@ export class SupermemoryClient {
// Delete the most similar match
const memoryToDelete = searchResult.results[0]
await this.client.memories.delete(memoryToDelete.id)
await this.client.documents.delete(memoryToDelete.id)
const memoryText = memoryToDelete.memory || memoryToDelete.content || ""
return {

View file

@ -1,6 +1,6 @@
"use client"
import { MobileBanner } from "@/components/new/mobile-banner"
import { MobileBanner } from "@/components/mobile-banner"
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (

View file

@ -2,12 +2,12 @@
import { motion, AnimatePresence } from "motion/react"
import { RelatableQuestion } from "@/components/new/onboarding/setup/relatable-question"
import { IntegrationsStep } from "@/components/new/onboarding/setup/integrations-step"
import { RelatableQuestion } from "@/components/onboarding/setup/relatable-question"
import { IntegrationsStep } from "@/components/onboarding/setup/integrations-step"
import { SetupHeader } from "@/components/new/onboarding/setup/header"
import { ChatSidebar } from "@/components/new/onboarding/setup/chat-sidebar"
import { AnimatedGradientBackground } from "@/components/new/animated-gradient-background"
import { SetupHeader } from "@/components/onboarding/setup/header"
import { ChatSidebar } from "@/components/onboarding/setup/chat-sidebar"
import { AnimatedGradientBackground } from "@/components/animated-gradient-background"
import { useIsMobile } from "@hooks/use-mobile"
import { useSetupContext, type SetupStep } from "./layout"

View file

@ -3,15 +3,15 @@
import { motion, AnimatePresence } from "motion/react"
import { cn } from "@lib/utils"
import { InputStep } from "@/components/new/onboarding/welcome/input-step"
import { GreetingStep } from "@/components/new/onboarding/welcome/greeting-step"
import { WelcomeStep } from "@/components/new/onboarding/welcome/welcome-step"
import { OnboardingContentStep } from "@/components/new/onboarding/welcome/continue-step"
import { InputStep } from "@/components/onboarding/welcome/input-step"
import { GreetingStep } from "@/components/onboarding/welcome/greeting-step"
import { WelcomeStep } from "@/components/onboarding/welcome/welcome-step"
import { OnboardingContentStep } from "@/components/onboarding/welcome/continue-step"
import { InitialHeader } from "@/components/initial-header"
import { Logo } from "@ui/assets/Logo"
import NovaOrb from "@/components/nova/nova-orb"
import { AnimatedGradientBackground } from "@/components/new/animated-gradient-background"
import { AnimatedGradientBackground } from "@/components/animated-gradient-background"
import {
useWelcomeContext,

View file

@ -2,17 +2,17 @@
import { useState, useCallback, useEffect } from "react"
import { useQueryState } from "nuqs"
import { Header } from "@/components/new/header"
import { ChatSidebar } from "@/components/new/chat"
import { MemoriesGrid } from "@/components/new/memories-grid"
import { GraphLayoutView } from "@/components/new/graph-layout-view"
import { IntegrationsView } from "@/components/new/integrations-view"
import { AnimatedGradientBackground } from "@/components/new/animated-gradient-background"
import { AddDocumentModal } from "@/components/new/add-document"
import { DocumentModal } from "@/components/new/document-modal"
import { DocumentsCommandPalette } from "@/components/new/documents-command-palette"
import { FullscreenNoteModal } from "@/components/new/fullscreen-note-modal"
import type { HighlightItem } from "@/components/new/highlights-card"
import { Header } from "@/components/header"
import { ChatSidebar } from "@/components/chat"
import { MemoriesGrid } from "@/components/memories-grid"
import { GraphLayoutView } from "@/components/graph-layout-view"
import { IntegrationsView } from "@/components/integrations-view"
import { AnimatedGradientBackground } from "@/components/animated-gradient-background"
import { AddDocumentModal } from "@/components/add-document"
import { DocumentModal } from "@/components/document-modal"
import { DocumentsCommandPalette } from "@/components/documents-command-palette"
import { FullscreenNoteModal } from "@/components/fullscreen-note-modal"
import type { HighlightItem } from "@/components/highlights-card"
import { HotkeysProvider } from "react-hotkeys-hook"
import { useHotkeys } from "react-hotkeys-hook"
import { AnimatePresence } from "motion/react"
@ -313,7 +313,7 @@ export default function NewPage() {
{isGraphMode && (
<div
id="graph-dotted-grid"
className="absolute inset-0 pointer-events-none bg-[radial-gradient(circle_at_center,#69A7F0_0.1px,transparent_1px)] bg-size-[24px_24px] mask-[radial-gradient(ellipse_at_center,black_60%,transparent_100%)]"
className="absolute inset-0 pointer-events-none bg-[radial-gradient(circle_at_center,rgba(105,167,240,0.25)_1px,transparent_1px)] bg-size-[32px_32px] mask-[radial-gradient(ellipse_at_center,black_60%,transparent_100%)]"
/>
)}
<Header
@ -336,33 +336,33 @@ export default function NewPage() {
>
<div className={cn("relative z-10 flex flex-col md:flex-row h-full")}>
<ErrorBoundary fallback={<ViewErrorFallback />}>
{viewMode === "integrations" ? (
<div className="flex-1 p-4 md:p-6 md:pr-0 pt-2!">
<IntegrationsView />
</div>
) : viewMode === "graph" && !isMobile ? (
<div className="flex-1">
<GraphLayoutView isChatOpen={chatOpen} />
</div>
) : (
<div className="flex-1 p-4 md:p-6 md:pr-0 pt-2!">
<MemoriesGrid
isChatOpen={chatOpen}
onOpenDocument={handleOpenDocument}
quickNoteProps={{
onSave: handleQuickNoteSave,
onMaximize: handleMaximize,
isSaving: noteMutation.isPending,
}}
highlightsProps={{
items: highlightsData?.highlights || [],
onChat: handleHighlightsChat,
onShowRelated: handleHighlightsShowRelated,
isLoading: isLoadingHighlights,
}}
/>
</div>
)}
{viewMode === "integrations" ? (
<div className="flex-1 p-4 md:p-6 md:pr-0 pt-2!">
<IntegrationsView />
</div>
) : viewMode === "graph" && !isMobile ? (
<div className="flex-1">
<GraphLayoutView isChatOpen={chatOpen} />
</div>
) : (
<div className="flex-1 p-4 md:p-6 md:pr-0 pt-2!">
<MemoriesGrid
isChatOpen={chatOpen}
onOpenDocument={handleOpenDocument}
quickNoteProps={{
onSave: handleQuickNoteSave,
onMaximize: handleMaximize,
isSaving: noteMutation.isPending,
}}
highlightsProps={{
items: highlightsData?.highlights || [],
onChat: handleHighlightsChat,
onShowRelated: handleHighlightsShowRelated,
isLoading: isLoadingHighlights,
}}
/>
</div>
)}
</ErrorBoundary>
<div className="hidden md:block md:sticky md:top-0 md:h-screen">
<AnimatePresence mode="popLayout">

View file

@ -7,10 +7,10 @@ import NovaOrb from "@/components/nova/nova-orb"
import { useState, useEffect, useRef } from "react"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import Account from "@/components/new/settings/account"
import Integrations from "@/components/new/settings/integrations"
import ConnectionsMCP from "@/components/new/settings/connections-mcp"
import Support from "@/components/new/settings/support"
import Account from "@/components/settings/account"
import Integrations from "@/components/settings/integrations"
import ConnectionsMCP from "@/components/settings/connections-mcp"
import Support from "@/components/settings/support"
import { ErrorBoundary } from "@/components/error-boundary"
import { useRouter } from "next/navigation"
import { useIsMobile } from "@hooks/use-mobile"

View file

@ -13,7 +13,7 @@ import { LinkContent, type LinkData } from "./link"
import { FileContent, type FileData } from "./file"
import { useProject } from "@/stores"
import { toast } from "sonner"
import { useDocumentMutations } from "../../../hooks/use-document-mutations"
import { useDocumentMutations } from "../../hooks/use-document-mutations"
import { useCustomer } from "autumn-js/react"
import { useTokenUsage } from "@/hooks/use-token-usage"
import { tokensToCredits, formatUsageNumber } from "@/lib/billing-utils"
@ -380,40 +380,42 @@ export function AddDocument({
<div
className={cn(
"overflow-auto flex flex-col justify-between px-1 scrollbar-thin flex-1",
"flex flex-col flex-1 min-h-0 px-1",
isMobile ? "w-full" : "w-2/3",
)}
>
{activeTab === "note" && (
<NoteContent
onSubmit={handleNoteSubmit}
onContentChange={handleNoteContentChange}
isSubmitting={noteMutation.isPending}
isOpen={isOpen}
/>
)}
{activeTab === "link" && (
<LinkContent
onSubmit={handleLinkSubmit}
onDataChange={handleLinkDataChange}
isSubmitting={linkMutation.isPending}
isOpen={isOpen}
/>
)}
{activeTab === "file" && (
<FileContent
onSubmit={handleFileSubmit}
onDataChange={handleFileDataChange}
isSubmitting={fileMutation.isPending}
isOpen={isOpen}
/>
)}
{activeTab === "connect" && (
<ConnectContent selectedProject={localSelectedProject} />
)}
<div className="overflow-auto flex-1 min-h-0 scrollbar-thin">
{activeTab === "note" && (
<NoteContent
onSubmit={handleNoteSubmit}
onContentChange={handleNoteContentChange}
isSubmitting={noteMutation.isPending}
isOpen={isOpen}
/>
)}
{activeTab === "link" && (
<LinkContent
onSubmit={handleLinkSubmit}
onDataChange={handleLinkDataChange}
isSubmitting={linkMutation.isPending}
isOpen={isOpen}
/>
)}
{activeTab === "file" && (
<FileContent
onSubmit={handleFileSubmit}
onDataChange={handleFileDataChange}
isSubmitting={fileMutation.isPending}
isOpen={isOpen}
/>
)}
{activeTab === "connect" && (
<ConnectContent selectedProject={localSelectedProject} />
)}
</div>
<div
className={cn(
"flex gap-2 pt-3",
"flex gap-2 pt-3 shrink-0",
isMobile ? "flex-col" : "justify-between",
)}
>
@ -424,6 +426,7 @@ export function AddDocument({
setLocalSelectedProject(projects[0] ?? localSelectedProject)
}
variant="insideOut"
singleSelect
/>
)}
<div

View file

@ -65,9 +65,11 @@ const EMOJI_LIST = [
export function AddSpaceModal({
isOpen,
onClose,
onCreated,
}: {
isOpen: boolean
onClose: () => void
onCreated?: (containerTag: string) => void
}) {
const [spaceName, setSpaceName] = useState("")
const [emoji, setEmoji] = useState("📁")
@ -87,8 +89,11 @@ export function AddSpaceModal({
createProjectMutation.mutate(
{ name: trimmedName, emoji: emoji || undefined },
{
onSuccess: () => {
onSuccess: (data) => {
analytics.spaceCreated()
if (data?.containerTag) {
onCreated?.(data.containerTag)
}
handleClose()
},
},

View file

@ -36,7 +36,7 @@ import ChatModelSelector from "./model-selector"
import { GradientLogo, LogoBgGradient } from "@ui/assets/Logo"
import { useProject } from "@/stores"
import type { ModelId } from "@/lib/models"
import { SuperLoader } from "../../superloader"
import { SuperLoader } from "../superloader"
import { UserMessage } from "./message/user-message"
import { AgentMessage } from "./message/agent-message"
import { ChainOfThought } from "./input/chain-of-thought"

View file

@ -5,7 +5,7 @@ import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { DocumentIcon } from "@/components/new/document-icon"
import { DocumentIcon } from "@/components/document-icon"
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]

View file

@ -4,10 +4,7 @@ import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import {
DocumentIcon,
getDocumentTypeLabel,
} from "@/components/new/document-icon"
import { DocumentIcon, getDocumentTypeLabel } from "@/components/document-icon"
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]

View file

@ -4,7 +4,7 @@ import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { DocumentIcon } from "@/components/new/document-icon"
import { DocumentIcon } from "@/components/document-icon"
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]

View file

@ -1,7 +1,7 @@
"use client"
import type { Tweet } from "react-tweet/api"
import { TweetPreview } from "@/components/new/document-cards/tweet-preview"
import { TweetPreview } from "@/components/document-cards/tweet-preview"
import { ExternalLinkIcon } from "lucide-react"
interface TweetContentProps {

View file

@ -7,4 +7,4 @@ export {
getDocumentIcon,
getDocumentTypeLabel,
type DocumentIconProps,
} from "@/components/new/document-icon"
} from "@/components/document-icon"

View file

@ -1,6 +1,6 @@
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { DocumentIcon } from "@/components/new/document-icon"
import { DocumentIcon } from "@/components/document-icon"
function getFileExtension(documentType: string): string | null {
switch (documentType) {

View file

@ -13,7 +13,7 @@ import { dmSansClassName } from "@/lib/fonts"
import { useIsMobile } from "@hooks/use-mobile"
import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog"
import { SearchIcon, Settings, Home, Plus, Code2, Loader2 } from "lucide-react"
import { DocumentIcon } from "@/components/new/document-icon"
import { DocumentIcon } from "@/components/document-icon"
import { $fetch } from "@lib/api"
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>

View file

@ -22,7 +22,7 @@ import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { Tabs, TabsList, TabsTrigger } from "@ui/components/tabs"
import { GraphIcon } from "@/components/new/integration-icons"
import { GraphIcon, IntegrationsIcon } from "@/components/integration-icons"
import {
DropdownMenu,
DropdownMenuContent,
@ -184,7 +184,7 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
dmSansClassName(),
)}
>
<Sun className="size-4" />
<IntegrationsIcon className="size-4" />
Integrations
</TabsTrigger>
</TabsList>

View file

@ -3,6 +3,47 @@ import { Rotate3d } from "lucide-react"
export { Rotate3d as GraphIcon }
export function IntegrationsIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 22 22"
fill="none"
className={className}
>
<title>Integrations</title>
<path
d="M15.6654 18.0026C16.954 18.0026 17.9987 16.9579 17.9987 15.6693C17.9987 14.3806 16.954 13.3359 15.6654 13.3359C14.3767 13.3359 13.332 14.3806 13.332 15.6693C13.332 16.9579 14.3767 18.0026 15.6654 18.0026Z"
stroke="currentColor"
strokeWidth="1.55556"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M6.33333 8.66667C7.622 8.66667 8.66667 7.622 8.66667 6.33333C8.66667 5.04467 7.622 4 6.33333 4C5.04467 4 4 5.04467 4 6.33333C4 7.622 5.04467 8.66667 6.33333 8.66667Z"
stroke="currentColor"
strokeWidth="1.55556"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M11.7773 6.33594H14.1107C14.5232 6.33594 14.9189 6.49983 15.2106 6.79155C15.5023 7.08327 15.6662 7.47893 15.6662 7.89149V13.3359"
stroke="currentColor"
strokeWidth="1.55556"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M6.33203 8.66406V17.9974"
stroke="currentColor"
strokeWidth="1.55556"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export function ChromeIcon({ className }: { className?: string }) {
return (
<svg

View file

@ -4,18 +4,18 @@ import { useState } from "react"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { Button } from "@ui/components/button"
import { MCPDetailView } from "@/components/new/mcp-modal/mcp-detail-view"
import { XBookmarksDetailView } from "@/components/new/onboarding/x-bookmarks-detail-view"
import { ChromeDetail } from "@/components/new/integrations/chrome-detail"
import { ShortcutsDetail } from "@/components/new/integrations/shortcuts-detail"
import { RaycastDetail } from "@/components/new/integrations/raycast-detail"
import { ConnectionsDetail } from "@/components/new/integrations/connections-detail"
import { PluginsDetail } from "@/components/new/integrations/plugins-detail"
import { MCPDetailView } from "@/components/mcp-modal/mcp-detail-view"
import { XBookmarksDetailView } from "@/components/onboarding/x-bookmarks-detail-view"
import { ChromeDetail } from "@/components/integrations/chrome-detail"
import { ShortcutsDetail } from "@/components/integrations/shortcuts-detail"
import { RaycastDetail } from "@/components/integrations/raycast-detail"
import { ConnectionsDetail } from "@/components/integrations/connections-detail"
import { PluginsDetail } from "@/components/integrations/plugins-detail"
import {
ChromeIcon,
AppleShortcutsIcon,
RaycastIcon,
} from "@/components/new/integration-icons"
} from "@/components/integration-icons"
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
import { ArrowLeft, Sun } from "lucide-react"
import Image from "next/image"

View file

@ -2,7 +2,7 @@
import { cn } from "@lib/utils"
import { dmSans125ClassName } from "@/lib/fonts"
import { ChromeIcon } from "@/components/new/integration-icons"
import { ChromeIcon } from "@/components/integration-icons"
import { Check, Download } from "lucide-react"
import { analytics } from "@/lib/analytics"
@ -61,10 +61,7 @@ export function ChromeDetail() {
Chrome Extension
</p>
<p
className={cn(
dmSans125ClassName(),
"text-[14px] text-[#737373]",
)}
className={cn(dmSans125ClassName(), "text-[14px] text-[#737373]")}
>
Save any webpage directly from your browser
</p>
@ -88,10 +85,7 @@ export function ChromeDetail() {
<div key={text} className="flex items-center gap-2">
<Check className="size-4 shrink-0 text-[#4BA0FA]" />
<span
className={cn(
dmSans125ClassName(),
"text-[14px] text-white",
)}
className={cn(dmSans125ClassName(), "text-[14px] text-white")}
>
{text}
</span>

View file

@ -13,14 +13,18 @@ import { toast } from "sonner"
import type { ConnectionResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { analytics } from "@/lib/analytics"
import { AddDocumentModal } from "@/components/new/add-document"
import { AddDocumentModal } from "@/components/add-document"
import { DEFAULT_PROJECT_ID } from "@repo/lib/constants"
import type { Project } from "@repo/lib/types"
type Connection = z.infer<typeof ConnectionResponseSchema>
const CONNECTORS = {
"google-drive": { title: "Google Drive", icon: GoogleDrive, documentLabel: "documents" },
"google-drive": {
title: "Google Drive",
icon: GoogleDrive,
documentLabel: "documents",
},
notion: { title: "Notion", icon: Notion, documentLabel: "pages" },
onedrive: { title: "OneDrive", icon: OneDrive, documentLabel: "documents" },
} as const
@ -44,7 +48,8 @@ function ConnectionRow({
if (!config) return null
const Icon = config.icon
const isConnected = !connection.expiresAt || new Date(connection.expiresAt) > new Date()
const isConnected =
!connection.expiresAt || new Date(connection.expiresAt) > new Date()
const formatRelativeTime = (date: string | null | undefined) => {
if (!date) return "Never"
@ -61,29 +66,63 @@ function ConnectionRow({
const getProjectName = (tag: string): string => {
if (tag === DEFAULT_PROJECT_ID) return "Default Project"
return projects.find((p) => p.containerTag === tag)?.name ?? tag.replace(/^sm_project_/, "")
return (
projects.find((p) => p.containerTag === tag)?.name ??
tag.replace(/^sm_project_/, "")
)
}
const documentCount = (connection.metadata?.documentCount as number) ?? 0
const containerTags = (connection as Connection & { containerTags?: string[] }).containerTags
const projectName = containerTags?.[0] ? getProjectName(containerTags[0]) : null
const containerTags = (
connection as Connection & { containerTags?: string[] }
).containerTags
const projectName = containerTags?.[0]
? getProjectName(containerTags[0])
: null
return (
<div className={cn("bg-[#14161A] border border-[rgba(82,89,102,0.2)] rounded-[12px] px-4 py-3", "shadow-[0px_1px_2px_0px_rgba(0,43,87,0.1)]")}>
<div
className={cn(
"bg-[#14161A] border border-[rgba(82,89,102,0.2)] rounded-[12px] px-4 py-3",
"shadow-[0px_1px_2px_0px_rgba(0,43,87,0.1)]",
)}
>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-4">
<Icon className="size-6 shrink-0" />
<div className="flex-1 flex flex-col gap-1">
<div className="flex items-center gap-3">
<span className={cn(dmSans125ClassName(), "font-medium text-[16px] text-[#FAFAFA]")}>{config.title}</span>
<span
className={cn(
dmSans125ClassName(),
"font-medium text-[16px] text-[#FAFAFA]",
)}
>
{config.title}
</span>
<div className="flex items-center gap-2">
<div className={cn("size-[7px] rounded-full", isConnected ? "bg-[#00AC3F]" : "bg-[#737373]")} />
<span className={cn(dmSans125ClassName(), "text-[14px]", isConnected ? "text-[#00AC3F]" : "text-[#737373]")}>
<div
className={cn(
"size-[7px] rounded-full",
isConnected ? "bg-[#00AC3F]" : "bg-[#737373]",
)}
/>
<span
className={cn(
dmSans125ClassName(),
"text-[14px]",
isConnected ? "text-[#00AC3F]" : "text-[#737373]",
)}
>
{isConnected ? "Connected" : "Disconnected"}
</span>
</div>
</div>
<span className={cn(dmSans125ClassName(), "text-[14px] text-[#737373]")}>{connection.email || "Unknown"}</span>
<span
className={cn(dmSans125ClassName(), "text-[14px] text-[#737373]")}
>
{connection.email || "Unknown"}
</span>
</div>
<button
type="button"
@ -97,13 +136,28 @@ function ConnectionRow({
<div className="flex items-center gap-2 flex-wrap">
{projectName && (
<>
<span className={cn(dmSans125ClassName(), "text-[14px] text-[#737373]")}>Project: {projectName}</span>
<span
className={cn(
dmSans125ClassName(),
"text-[14px] text-[#737373]",
)}
>
Project: {projectName}
</span>
<div className="size-[3px] rounded-full bg-[#737373]" />
</>
)}
<span className={cn(dmSans125ClassName(), "text-[14px] text-[#737373]")}>Added: {formatRelativeTime(connection.createdAt)}</span>
<span
className={cn(dmSans125ClassName(), "text-[14px] text-[#737373]")}
>
Added: {formatRelativeTime(connection.createdAt)}
</span>
<div className="size-[3px] rounded-full bg-[#737373]" />
<span className={cn(dmSans125ClassName(), "text-[14px] text-[#737373]")}>{documentCount} {config.documentLabel} connected</span>
<span
className={cn(dmSans125ClassName(), "text-[14px] text-[#737373]")}
>
{documentCount} {config.documentLabel} connected
</span>
</div>
</div>
</div>
@ -115,10 +169,13 @@ export function ConnectionsDetail() {
const autumn = useCustomer()
const [isAddDocumentOpen, setIsAddDocumentOpen] = useState(false)
const projects = (queryClient.getQueryData<Project[]>(["projects"]) || []) as Project[]
const projects = (queryClient.getQueryData<Project[]>(["projects"]) ||
[]) as Project[]
const { data: status = { api_pro: { allowed: false, status: null } }, isLoading: isCheckingStatus } =
fetchSubscriptionStatus(autumn, !autumn.isLoading)
const {
data: status = { api_pro: { allowed: false, status: null } },
isLoading: isCheckingStatus,
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
const hasProProduct = status.api_pro?.status !== null
@ -127,11 +184,18 @@ export function ConnectionsDetail() {
const connectionsLimit = connectionsFeature?.included_usage ?? 10
const canAddConnection = connectionsUsed < connectionsLimit
const { data: connections = [], isLoading: isLoadingConnections, error: connectionsError } = useQuery({
const {
data: connections = [],
isLoading: isLoadingConnections,
error: connectionsError,
} = useQuery({
queryKey: ["connections"],
queryFn: async () => {
const response = await $fetch("@post/connections/list", { body: { containerTags: [] } })
if (response.error) throw new Error(response.error?.message || "Failed to load connections")
const response = await $fetch("@post/connections/list", {
body: { containerTags: [] },
})
if (response.error)
throw new Error(response.error?.message || "Failed to load connections")
return response.data as Connection[]
},
staleTime: 30 * 1000,
@ -142,7 +206,10 @@ export function ConnectionsDetail() {
useEffect(() => {
if (connectionsError) {
toast.error("Failed to load connections", {
description: connectionsError instanceof Error ? connectionsError.message : "Unknown error",
description:
connectionsError instanceof Error
? connectionsError.message
: "Unknown error",
})
}
}, [connectionsError])
@ -153,7 +220,9 @@ export function ConnectionsDetail() {
},
onSuccess: () => {
analytics.connectionDeleted()
toast.success("Connection removal has started. Documents will be permanently deleted in the next few minutes.")
toast.success(
"Connection removal has started. Documents will be permanently deleted in the next few minutes.",
)
queryClient.invalidateQueries({ queryKey: ["connections"] })
},
onError: (error) => {
@ -165,7 +234,10 @@ export function ConnectionsDetail() {
const handleUpgrade = async () => {
try {
await autumn.attach({ productId: "api_pro", successUrl: "https://app.supermemory.ai/?view=integrations" })
await autumn.attach({
productId: "api_pro",
successUrl: "https://app.supermemory.ai/?view=integrations",
})
window.location.reload()
} catch (error) {
console.error(error)
@ -176,21 +248,44 @@ export function ConnectionsDetail() {
return (
<>
<div className={cn("bg-[#14161A] rounded-[14px] p-6 relative overflow-hidden", "shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]")}>
<div
className={cn(
"bg-[#14161A] rounded-[14px] p-6 relative overflow-hidden",
"shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]",
)}
>
{!hasProProduct && !isLoading && (
<>
<div className="absolute inset-0 bg-[#14161A]/80 backdrop-blur-sm z-5" />
<div className="absolute inset-0 flex items-center justify-center z-10">
<div className="flex flex-col items-center gap-4">
<Zap className="size-6 text-[#737373]" />
<p className={cn(dmSans125ClassName(), "text-[14px] text-[#737373] text-center max-w-[220px]")}>
Connect Google Drive, Notion, and OneDrive to import your knowledge
<p
className={cn(
dmSans125ClassName(),
"text-[14px] text-[#737373] text-center max-w-[220px]",
)}
>
Connect Google Drive, Notion, and OneDrive to import your
knowledge
</p>
<div className="flex flex-col gap-2">
{["Unlimited memories", "10 connections", "Advanced search", "Priority support"].map((text) => (
{[
"Unlimited memories",
"10 connections",
"Advanced search",
"Priority support",
].map((text) => (
<div key={text} className="flex items-center gap-2">
<Check className="size-4 shrink-0 text-[#4BA0FA]" />
<span className={cn(dmSans125ClassName(), "text-[14px] text-white")}>{text}</span>
<span
className={cn(
dmSans125ClassName(),
"text-[14px] text-white",
)}
>
{text}
</span>
</div>
))}
</div>
@ -211,12 +306,27 @@ export function ConnectionsDetail() {
</>
)}
<div className={cn("flex flex-col gap-4", !hasProProduct && !isLoading && "opacity-30 pointer-events-none")}>
<div
className={cn(
"flex flex-col gap-4",
!hasProProduct && !isLoading && "opacity-30 pointer-events-none",
)}
>
<div className="flex items-center justify-between">
<span className={cn(dmSans125ClassName(), "font-semibold text-[16px] text-[#FAFAFA]")}>
<span
className={cn(
dmSans125ClassName(),
"font-semibold text-[16px] text-[#FAFAFA]",
)}
>
Connected to Supermemory
</span>
<span className={cn(dmSans125ClassName(), "font-semibold text-[16px] text-[#737373]")}>
<span
className={cn(
dmSans125ClassName(),
"font-semibold text-[16px] text-[#737373]",
)}
>
{connections.length}/{connectionsLimit} connections used
</span>
</div>
@ -231,7 +341,9 @@ export function ConnectionsDetail() {
<ConnectionRow
key={connection.id}
connection={connection}
onDelete={() => deleteConnectionMutation.mutate(connection.id)}
onDelete={() =>
deleteConnectionMutation.mutate(connection.id)
}
isDeleting={deleteConnectionMutation.isPending}
disabled={!hasProProduct}
projects={projects}
@ -240,8 +352,22 @@ export function ConnectionsDetail() {
) : (
<div className="flex flex-col items-center justify-center py-8 text-center">
<Zap className="size-6 text-[#737373] mb-2" />
<p className={cn(dmSans125ClassName(), "text-[14px] text-[#737373]")}>No connections yet</p>
<p className={cn(dmSans125ClassName(), "text-[12px] text-[#737373]")}>Connect a service below to import your knowledge</p>
<p
className={cn(
dmSans125ClassName(),
"text-[14px] text-[#737373]",
)}
>
No connections yet
</p>
<p
className={cn(
dmSans125ClassName(),
"text-[12px] text-[#737373]",
)}
>
Connect a service below to import your knowledge
</p>
</div>
)}
</div>
@ -260,12 +386,18 @@ export function ConnectionsDetail() {
)}
>
<Plus className="size-[10px] text-[#FAFAFA]" />
<span className="text-[14px] text-[#FAFAFA] font-medium">Connect knowledge bases</span>
<span className="text-[14px] text-[#FAFAFA] font-medium">
Connect knowledge bases
</span>
</button>
</div>
</div>
<AddDocumentModal isOpen={isAddDocumentOpen} onClose={() => setIsAddDocumentOpen(false)} defaultTab="connect" />
<AddDocumentModal
isOpen={isAddDocumentOpen}
onClose={() => setIsAddDocumentOpen(false)}
defaultTab="connect"
/>
</>
)
}

View file

@ -2,7 +2,7 @@
import { cn } from "@lib/utils"
import { dmSans125ClassName } from "@/lib/fonts"
import { RaycastIcon } from "@/components/new/integration-icons"
import { RaycastIcon } from "@/components/integration-icons"
import { authClient } from "@lib/auth"
import { useAuth } from "@lib/auth-context"
import { generateId } from "@lib/generate-id"
@ -101,17 +101,30 @@ export function RaycastDetail() {
<div className="flex items-center gap-4">
<RaycastIcon className="shrink-0 w-10 h-10" />
<div className="flex flex-col gap-1">
<p className={cn(dmSans125ClassName(), "font-semibold text-[16px] text-[#FAFAFA]")}>
<p
className={cn(
dmSans125ClassName(),
"font-semibold text-[16px] text-[#FAFAFA]",
)}
>
Raycast Extension
</p>
<p className={cn(dmSans125ClassName(), "text-[14px] text-[#737373]")}>
<p
className={cn(
dmSans125ClassName(),
"text-[14px] text-[#737373]",
)}
>
Add and search memories from Mac and Windows
</p>
</div>
</div>
<div className="flex gap-4">
<PillButton onClick={() => createKeyMutation.mutate()} disabled={createKeyMutation.isPending}>
<PillButton
onClick={() => createKeyMutation.mutate()}
disabled={createKeyMutation.isPending}
>
{createKeyMutation.isPending ? (
<Loader className="size-4 text-[#FAFAFA] animate-spin" />
) : (
@ -121,28 +134,49 @@ export function RaycastDetail() {
{createKeyMutation.isPending ? "Generating..." : "Get API key"}
</span>
</PillButton>
<PillButton onClick={() => window.open(RAYCAST_EXTENSION_URL, "_blank")}>
<PillButton
onClick={() => window.open(RAYCAST_EXTENSION_URL, "_blank")}
>
<Download className="size-4 text-[#FAFAFA]" />
<span className="text-[14px] text-[#FAFAFA] font-medium">Install extension</span>
<span className="text-[14px] text-[#FAFAFA] font-medium">
Install extension
</span>
</PillButton>
</div>
</div>
</div>
<Dialog open={showModal} onOpenChange={(open) => {
setShowModal(open)
if (!open) { setApiKey(""); setCopied(false) }
}}>
<Dialog
open={showModal}
onOpenChange={(open) => {
setShowModal(open)
if (!open) {
setApiKey("")
setCopied(false)
}
}}
>
<DialogPortal>
<DialogContent className="bg-[#14161A] border border-white/10 text-[#FAFAFA] md:max-w-md z-100">
<DialogHeader>
<DialogTitle className={cn(dmSans125ClassName(), "text-[#FAFAFA] text-lg font-semibold")}>
<DialogTitle
className={cn(
dmSans125ClassName(),
"text-[#FAFAFA] text-lg font-semibold",
)}
>
Setup Raycast Extension
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<label htmlFor={apiKeyId} className={cn(dmSans125ClassName(), "text-sm font-medium text-[#737373]")}>
<label
htmlFor={apiKeyId}
className={cn(
dmSans125ClassName(),
"text-sm font-medium text-[#737373]",
)}
>
Your Raycast API Key
</label>
<div className="flex items-center gap-2">
@ -151,30 +185,51 @@ export function RaycastDetail() {
type="text"
value={apiKey}
readOnly
className={cn("flex-1 bg-[#0D121A] border border-white/10 rounded-lg px-3 py-2 text-sm text-[#FAFAFA] font-mono", dmSans125ClassName())}
className={cn(
"flex-1 bg-[#0D121A] border border-white/10 rounded-lg px-3 py-2 text-sm text-[#FAFAFA] font-mono",
dmSans125ClassName(),
)}
/>
<button
type="button"
onClick={() => handleCopy(apiKey)}
className="p-2 rounded-lg bg-[#0D121A] border border-white/10 text-[#737373] hover:text-[#FAFAFA] transition-colors"
>
{copied ? <Check className="h-4 w-4 text-[#4BA0FA]" /> : <Copy className="h-4 w-4" />}
{copied ? (
<Check className="h-4 w-4 text-[#4BA0FA]" />
) : (
<Copy className="h-4 w-4" />
)}
</button>
</div>
</div>
<div className="space-y-3">
<h4 className={cn(dmSans125ClassName(), "text-sm font-medium text-[#737373]")}>Follow these steps:</h4>
<h4
className={cn(
dmSans125ClassName(),
"text-sm font-medium text-[#737373]",
)}
>
Follow these steps:
</h4>
<div className="space-y-2">
{[
"Install the Raycast extension from the Raycast Store",
"Open Raycast preferences and paste your API key",
"Use \"Add Memory\" or \"Search Memories\" commands!",
'Use "Add Memory" or "Search Memories" commands!',
].map((text, i) => (
<div key={text} className="flex items-start gap-3">
<div className="shrink-0 w-6 h-6 bg-[#FF6363]/20 text-[#FF6363] rounded-full flex items-center justify-center text-xs font-medium">
{i + 1}
</div>
<p className={cn(dmSans125ClassName(), "text-sm text-[#737373]")}>{text}</p>
<p
className={cn(
dmSans125ClassName(),
"text-sm text-[#737373]",
)}
>
{text}
</p>
</div>
))}
</div>

View file

@ -2,7 +2,7 @@
import { cn } from "@lib/utils"
import { dmSans125ClassName } from "@/lib/fonts"
import { AppleShortcutsIcon } from "@/components/new/integration-icons"
import { AppleShortcutsIcon } from "@/components/integration-icons"
import { authClient } from "@lib/auth"
import { useAuth } from "@lib/auth-context"
import { generateId } from "@lib/generate-id"
@ -120,10 +120,20 @@ export function ShortcutsDetail() {
<div className="flex items-center gap-4">
<AppleShortcutsIcon />
<div className="flex flex-col gap-1">
<p className={cn(dmSans125ClassName(), "font-semibold text-[16px] text-[#FAFAFA]")}>
<p
className={cn(
dmSans125ClassName(),
"font-semibold text-[16px] text-[#FAFAFA]",
)}
>
Apple Shortcuts
</p>
<p className={cn(dmSans125ClassName(), "text-[14px] text-[#737373]")}>
<p
className={cn(
dmSans125ClassName(),
"text-[14px] text-[#737373]",
)}
>
Add memories directly from iPhone, iPad or Mac
</p>
</div>
@ -134,13 +144,15 @@ export function ShortcutsDetail() {
onClick={() => handleShortcutClick("add")}
disabled={createApiKeyMutation.isPending}
>
{createApiKeyMutation.isPending && selectedShortcutType === "add" ? (
{createApiKeyMutation.isPending &&
selectedShortcutType === "add" ? (
<Loader className="size-4 text-[#FAFAFA] animate-spin" />
) : (
<Plus className="size-4 text-[#FAFAFA]" />
)}
<span className="text-[14px] text-[#FAFAFA] font-medium">
{createApiKeyMutation.isPending && selectedShortcutType === "add"
{createApiKeyMutation.isPending &&
selectedShortcutType === "add"
? "Creating..."
: "Add memory shortcut"}
</span>
@ -149,13 +161,15 @@ export function ShortcutsDetail() {
onClick={() => handleShortcutClick("search")}
disabled={createApiKeyMutation.isPending}
>
{createApiKeyMutation.isPending && selectedShortcutType === "search" ? (
{createApiKeyMutation.isPending &&
selectedShortcutType === "search" ? (
<Loader className="size-4 text-[#FAFAFA] animate-spin" />
) : (
<Search className="size-4 text-[#FAFAFA]" />
)}
<span className="text-[14px] text-[#FAFAFA] font-medium">
{createApiKeyMutation.isPending && selectedShortcutType === "search"
{createApiKeyMutation.isPending &&
selectedShortcutType === "search"
? "Creating..."
: "Search memory shortcut"}
</span>
@ -164,20 +178,38 @@ export function ShortcutsDetail() {
</div>
</div>
<Dialog open={showApiKeyModal} onOpenChange={(open) => {
setShowApiKeyModal(open)
if (!open) { setSelectedShortcutType(null); setApiKey(""); setCopied(false) }
}}>
<Dialog
open={showApiKeyModal}
onOpenChange={(open) => {
setShowApiKeyModal(open)
if (!open) {
setSelectedShortcutType(null)
setApiKey("")
setCopied(false)
}
}}
>
<DialogPortal>
<DialogContent className="bg-[#14161A] border border-white/10 text-[#FAFAFA] md:max-w-md z-100">
<DialogHeader>
<DialogTitle className={cn(dmSans125ClassName(), "text-[#FAFAFA] text-lg font-semibold")}>
<DialogTitle
className={cn(
dmSans125ClassName(),
"text-[#FAFAFA] text-lg font-semibold",
)}
>
Setup Apple Shortcut
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<label htmlFor={apiKeyId} className={cn(dmSans125ClassName(), "text-sm font-medium text-[#737373]")}>
<label
htmlFor={apiKeyId}
className={cn(
dmSans125ClassName(),
"text-sm font-medium text-[#737373]",
)}
>
Your API Key
</label>
<div className="flex items-center gap-2">
@ -186,26 +218,51 @@ export function ShortcutsDetail() {
type="text"
value={apiKey}
readOnly
className={cn("flex-1 bg-[#0D121A] border border-white/10 rounded-lg px-3 py-2 text-sm text-[#FAFAFA] font-mono", dmSans125ClassName())}
className={cn(
"flex-1 bg-[#0D121A] border border-white/10 rounded-lg px-3 py-2 text-sm text-[#FAFAFA] font-mono",
dmSans125ClassName(),
)}
/>
<button
type="button"
onClick={() => handleCopyApiKey(apiKey)}
className="p-2 rounded-lg bg-[#0D121A] border border-white/10 text-[#737373] hover:text-[#FAFAFA] transition-colors"
>
{copied ? <Check className="h-4 w-4 text-[#4BA0FA]" /> : <Copy className="h-4 w-4" />}
{copied ? (
<Check className="h-4 w-4 text-[#4BA0FA]" />
) : (
<Copy className="h-4 w-4" />
)}
</button>
</div>
</div>
<div className="space-y-3">
<h4 className={cn(dmSans125ClassName(), "text-sm font-medium text-[#737373]")}>Follow these steps:</h4>
<h4
className={cn(
dmSans125ClassName(),
"text-sm font-medium text-[#737373]",
)}
>
Follow these steps:
</h4>
<div className="space-y-2">
{["Click \"Add to Shortcuts\" below to open the shortcut", "Paste your API key when prompted", "Start using your shortcut!"].map((text, i) => (
{[
'Click "Add to Shortcuts" below to open the shortcut',
"Paste your API key when prompted",
"Start using your shortcut!",
].map((text, i) => (
<div key={text} className="flex items-start gap-3">
<div className="shrink-0 w-6 h-6 bg-[#4BA0FA]/20 text-[#4BA0FA] rounded-full flex items-center justify-center text-xs font-medium">
{i + 1}
</div>
<p className={cn(dmSans125ClassName(), "text-sm text-[#737373]")}>{text}</p>
<p
className={cn(
dmSans125ClassName(),
"text-sm text-[#737373]",
)}
>
{text}
</p>
</div>
))}
</div>
@ -222,7 +279,12 @@ export function ShortcutsDetail() {
dmSans125ClassName(),
)}
>
<Image src="/images/ios-shortcuts.png" alt="iOS Shortcuts" width={16} height={16} />
<Image
src="/images/ios-shortcuts.png"
alt="iOS Shortcuts"
width={16}
height={16}
/>
Add to Shortcuts
</button>
</div>

View file

@ -2,8 +2,8 @@
import { useState } from "react"
import { Button } from "@ui/components/button"
import { MCPDetailView } from "@/components/new/mcp-modal/mcp-detail-view"
import { XBookmarksDetailView } from "@/components/new/onboarding/x-bookmarks-detail-view"
import { MCPDetailView } from "@/components/mcp-modal/mcp-detail-view"
import { XBookmarksDetailView } from "@/components/onboarding/x-bookmarks-detail-view"
import { useRouter } from "next/navigation"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"

View file

@ -1,7 +1,6 @@
"use client"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
import { useState } from "react"
export const QueryProvider = ({ children }: { children: React.ReactNode }) => {

Some files were not shown because too many files have changed in this diff Show more