mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-10 03:28:14 +00:00
Merge branch 'main' of https://github.com/supermemoryai/supermemory into fix/chat-history-mobile-view
This commit is contained in:
commit
581b946c38
114 changed files with 767 additions and 1025 deletions
|
|
@ -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
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/*",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
},
|
||||
},
|
||||
|
|
@ -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"
|
||||
|
|
@ -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]
|
||||
|
|
@ -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]
|
||||
|
|
@ -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]
|
||||
|
|
@ -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 {
|
||||
|
|
@ -7,4 +7,4 @@ export {
|
|||
getDocumentIcon,
|
||||
getDocumentTypeLabel,
|
||||
type DocumentIconProps,
|
||||
} from "@/components/new/document-icon"
|
||||
} from "@/components/document-icon"
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
Loading…
Add table
Reference in a new issue