From 645f89310caf6b67b113b3c7ad7904b7856fbf18 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Tue, 13 Jan 2026 00:54:56 -0800 Subject: PR: nova alpha release (#670) Co-authored-by: Dhravya Shah --- apps/browser-extension/entrypoints/background.ts | 47 + .../browser-extension/entrypoints/content/index.ts | 23 +- .../entrypoints/content/selection-search.ts | 647 ++ apps/browser-extension/entrypoints/popup/App.tsx | 295 +- apps/browser-extension/utils/api.ts | 5 +- apps/browser-extension/utils/constants.ts | 9 + apps/memory-graph-playground/next-env.d.ts | 6 + apps/web/app/(auth)/login/new/page.tsx | 479 ++ apps/web/app/(auth)/login/page.tsx | 11 +- apps/web/app/(navigation)/page.tsx | 15 +- apps/web/app/api/exa/fetch-content/route.ts | 61 + apps/web/app/layout.tsx | 6 +- apps/web/app/new/layout.tsx | 22 + apps/web/app/new/onboarding/page.tsx | 267 + apps/web/app/new/onboarding/setup/chat-sidebar.tsx | 424 ++ apps/web/app/new/onboarding/setup/header.tsx | 47 + .../app/new/onboarding/setup/integrations-step.tsx | 181 + .../new/onboarding/setup/relatable-question.tsx | 159 + .../app/new/onboarding/welcome/continue-step.tsx | 45 + .../app/new/onboarding/welcome/features-step.tsx | 98 + .../app/new/onboarding/welcome/greeting-step.tsx | 23 + apps/web/app/new/onboarding/welcome/input-step.tsx | 96 + .../app/new/onboarding/welcome/memories-step.tsx | 299 + .../app/new/onboarding/welcome/welcome-step.tsx | 16 + apps/web/app/new/page.tsx | 57 + apps/web/app/new/settings/page.tsx | 243 + apps/web/components/chat-loader.tsx | 228 + apps/web/components/connect-ai-modal.tsx | 10 +- apps/web/components/header.tsx | 1 - apps/web/components/initial-header.tsx | 34 + .../components/memories-utils/memory-detail.tsx | 2 +- apps/web/components/memory-list-view.tsx | 2 +- .../components/new/add-document/connections.tsx | 408 ++ apps/web/components/new/add-document/file.tsx | 164 + apps/web/components/new/add-document/index.tsx | 464 ++ apps/web/components/new/add-document/link.tsx | 130 + apps/web/components/new/add-document/note.tsx | 51 + .../new/add-document/useDocumentMutations.ts | 313 + .../new/animated-gradient-background.tsx | 68 + apps/web/components/new/chat/index.tsx | 551 ++ apps/web/components/new/chat/input/actions.tsx | 50 + .../components/new/chat/input/chain-of-thought.tsx | 215 + apps/web/components/new/chat/input/index.tsx | 133 + .../components/new/chat/message/agent-message.tsx | 108 + .../new/chat/message/follow-up-questions.tsx | 56 + .../new/chat/message/message-actions.tsx | 80 + .../new/chat/message/related-memories.tsx | 135 + .../components/new/chat/message/user-message.tsx | 41 + apps/web/components/new/chat/model-selector.tsx | 101 + .../components/new/document-cards/file-preview.tsx | 188 + .../new/document-cards/google-docs-preview.tsx | 62 + .../components/new/document-cards/mcp-preview.tsx | 36 + .../components/new/document-cards/note-preview.tsx | 109 + .../new/document-cards/tweet-preview.tsx | 51 + .../new/document-cards/website-preview.tsx | 52 + .../new/document-cards/youtube-preview.tsx | 52 + .../new/document-modal/content/notion-doc.tsx | 9 + .../components/new/document-modal/content/pdf.tsx | 83 + .../new/document-modal/content/tweet.tsx | 37 + .../new/document-modal/content/yt-video.tsx | 89 + .../new/document-modal/document-icon.tsx | 263 + .../new/document-modal/graph-list-memories.tsx | 322 + apps/web/components/new/document-modal/index.tsx | 143 + apps/web/components/new/document-modal/summary.tsx | 84 + apps/web/components/new/document-modal/title.tsx | 55 + apps/web/components/new/header.tsx | 225 + apps/web/components/new/mcp-modal/index.tsx | 70 + .../components/new/mcp-modal/mcp-detail-view.tsx | 629 ++ apps/web/components/new/memories-grid.tsx | 423 ++ apps/web/components/new/settings/account.tsx | 694 +++ .../components/new/settings/connections-mcp.tsx | 568 ++ apps/web/components/new/settings/integrations.tsx | 761 +++ apps/web/components/new/settings/support.tsx | 217 + apps/web/components/new/utils.ts | 78 + apps/web/components/nova/bg-grad.tsx | 150 + apps/web/components/nova/nova-orb.tsx | 42 + apps/web/components/project-selector.tsx | 10 +- apps/web/components/query-client.tsx | 24 + apps/web/components/superloader.tsx | 98 + apps/web/components/views/chat/chat-messages.tsx | 2 +- apps/web/components/views/integrations.tsx | 10 +- .../views/mcp/installation-dialog-content.tsx | 10 +- apps/web/components/x-bookmarks-detail-view.tsx | 113 + apps/web/globals.css | 80 + apps/web/lib/document-icon.tsx | 119 - apps/web/lib/models.tsx | 8 +- apps/web/middleware.ts | 16 +- apps/web/package.json | 6 +- apps/web/public/bg-rectangle.png | Bin 0 -> 6008962 bytes apps/web/public/dot-pattern.svg | 2126 +++++++ apps/web/public/onboarding/bg-gradient-0.png | Bin 0 -> 3874273 bytes apps/web/public/onboarding/bg-gradient-1.png | Bin 0 -> 3881693 bytes apps/web/public/onboarding/chrome-ext-1.png | Bin 0 -> 581919 bytes apps/web/public/onboarding/chrome-ext-2.png | Bin 0 -> 376422 bytes apps/web/public/onboarding/chrome-ext-3.png | Bin 0 -> 733974 bytes apps/web/public/onboarding/chrome.png | Bin 0 -> 7284 bytes apps/web/public/onboarding/connectors.png | Bin 0 -> 13524 bytes apps/web/public/onboarding/human-brain.png | Bin 0 -> 6793 bytes apps/web/public/onboarding/mcp.png | Bin 0 -> 39215 bytes apps/web/public/onboarding/plant.png | Bin 0 -> 2921 bytes apps/web/public/onboarding/search.png | Bin 0 -> 3637 bytes apps/web/public/onboarding/x.png | Bin 0 -> 8972 bytes apps/web/utils/fonts.ts | 55 + apps/web/utils/url-helpers.ts | 59 + bun.lock | 6350 ++++++++++++++++++++ packages/ai-sdk/package.json | 2 +- packages/lib/api.ts | 4 + packages/lib/package.json | 2 + packages/lib/posthog.tsx | 2 +- packages/lib/types.ts | 12 + packages/tools/package.json | 2 +- packages/ui/assets/Logo.tsx | 266 +- packages/ui/assets/icons.tsx | 162 +- packages/ui/button/external-auth.tsx | 8 +- packages/ui/components/button.tsx | 12 +- packages/ui/components/text-separator.tsx | 4 +- packages/ui/globals.css | 5 +- packages/ui/input/labeled-input.tsx | 16 +- packages/ui/pages/login.tsx | 2 +- packages/validation/api.ts | 1 + packages/validation/schemas.ts | 1 + 121 files changed, 21683 insertions(+), 322 deletions(-) create mode 100644 apps/browser-extension/entrypoints/content/selection-search.ts create mode 100644 apps/memory-graph-playground/next-env.d.ts create mode 100644 apps/web/app/(auth)/login/new/page.tsx create mode 100644 apps/web/app/api/exa/fetch-content/route.ts create mode 100644 apps/web/app/new/layout.tsx create mode 100644 apps/web/app/new/onboarding/page.tsx create mode 100644 apps/web/app/new/onboarding/setup/chat-sidebar.tsx create mode 100644 apps/web/app/new/onboarding/setup/header.tsx create mode 100644 apps/web/app/new/onboarding/setup/integrations-step.tsx create mode 100644 apps/web/app/new/onboarding/setup/relatable-question.tsx create mode 100644 apps/web/app/new/onboarding/welcome/continue-step.tsx create mode 100644 apps/web/app/new/onboarding/welcome/features-step.tsx create mode 100644 apps/web/app/new/onboarding/welcome/greeting-step.tsx create mode 100644 apps/web/app/new/onboarding/welcome/input-step.tsx create mode 100644 apps/web/app/new/onboarding/welcome/memories-step.tsx create mode 100644 apps/web/app/new/onboarding/welcome/welcome-step.tsx create mode 100644 apps/web/app/new/page.tsx create mode 100644 apps/web/app/new/settings/page.tsx create mode 100644 apps/web/components/chat-loader.tsx create mode 100644 apps/web/components/initial-header.tsx create mode 100644 apps/web/components/new/add-document/connections.tsx create mode 100644 apps/web/components/new/add-document/file.tsx create mode 100644 apps/web/components/new/add-document/index.tsx create mode 100644 apps/web/components/new/add-document/link.tsx create mode 100644 apps/web/components/new/add-document/note.tsx create mode 100644 apps/web/components/new/add-document/useDocumentMutations.ts create mode 100644 apps/web/components/new/animated-gradient-background.tsx create mode 100644 apps/web/components/new/chat/index.tsx create mode 100644 apps/web/components/new/chat/input/actions.tsx create mode 100644 apps/web/components/new/chat/input/chain-of-thought.tsx create mode 100644 apps/web/components/new/chat/input/index.tsx create mode 100644 apps/web/components/new/chat/message/agent-message.tsx create mode 100644 apps/web/components/new/chat/message/follow-up-questions.tsx create mode 100644 apps/web/components/new/chat/message/message-actions.tsx create mode 100644 apps/web/components/new/chat/message/related-memories.tsx create mode 100644 apps/web/components/new/chat/message/user-message.tsx create mode 100644 apps/web/components/new/chat/model-selector.tsx create mode 100644 apps/web/components/new/document-cards/file-preview.tsx create mode 100644 apps/web/components/new/document-cards/google-docs-preview.tsx create mode 100644 apps/web/components/new/document-cards/mcp-preview.tsx create mode 100644 apps/web/components/new/document-cards/note-preview.tsx create mode 100644 apps/web/components/new/document-cards/tweet-preview.tsx create mode 100644 apps/web/components/new/document-cards/website-preview.tsx create mode 100644 apps/web/components/new/document-cards/youtube-preview.tsx create mode 100644 apps/web/components/new/document-modal/content/notion-doc.tsx create mode 100644 apps/web/components/new/document-modal/content/pdf.tsx create mode 100644 apps/web/components/new/document-modal/content/tweet.tsx create mode 100644 apps/web/components/new/document-modal/content/yt-video.tsx create mode 100644 apps/web/components/new/document-modal/document-icon.tsx create mode 100644 apps/web/components/new/document-modal/graph-list-memories.tsx create mode 100644 apps/web/components/new/document-modal/index.tsx create mode 100644 apps/web/components/new/document-modal/summary.tsx create mode 100644 apps/web/components/new/document-modal/title.tsx create mode 100644 apps/web/components/new/header.tsx create mode 100644 apps/web/components/new/mcp-modal/index.tsx create mode 100644 apps/web/components/new/mcp-modal/mcp-detail-view.tsx create mode 100644 apps/web/components/new/memories-grid.tsx create mode 100644 apps/web/components/new/settings/account.tsx create mode 100644 apps/web/components/new/settings/connections-mcp.tsx create mode 100644 apps/web/components/new/settings/integrations.tsx create mode 100644 apps/web/components/new/settings/support.tsx create mode 100644 apps/web/components/new/utils.ts create mode 100644 apps/web/components/nova/bg-grad.tsx create mode 100644 apps/web/components/nova/nova-orb.tsx create mode 100644 apps/web/components/query-client.tsx create mode 100644 apps/web/components/superloader.tsx create mode 100644 apps/web/components/x-bookmarks-detail-view.tsx delete mode 100644 apps/web/lib/document-icon.tsx create mode 100644 apps/web/public/bg-rectangle.png create mode 100644 apps/web/public/dot-pattern.svg create mode 100644 apps/web/public/onboarding/bg-gradient-0.png create mode 100644 apps/web/public/onboarding/bg-gradient-1.png create mode 100644 apps/web/public/onboarding/chrome-ext-1.png create mode 100644 apps/web/public/onboarding/chrome-ext-2.png create mode 100644 apps/web/public/onboarding/chrome-ext-3.png create mode 100644 apps/web/public/onboarding/chrome.png create mode 100644 apps/web/public/onboarding/connectors.png create mode 100644 apps/web/public/onboarding/human-brain.png create mode 100644 apps/web/public/onboarding/mcp.png create mode 100644 apps/web/public/onboarding/plant.png create mode 100644 apps/web/public/onboarding/search.png create mode 100644 apps/web/public/onboarding/x.png create mode 100644 apps/web/utils/fonts.ts create mode 100644 apps/web/utils/url-helpers.ts create mode 100644 bun.lock create mode 100644 packages/lib/types.ts diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts index 131207c2..0ee09e3f 100644 --- a/apps/browser-extension/entrypoints/background.ts +++ b/apps/browser-extension/entrypoints/background.ts @@ -32,6 +32,12 @@ export default defineBackground(() => { 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, @@ -67,6 +73,22 @@ export default defineBackground(() => { } } } + + 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. @@ -296,6 +318,31 @@ 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 + } }, ) }) diff --git a/apps/browser-extension/entrypoints/content/index.ts b/apps/browser-extension/entrypoints/content/index.ts index a79f50fb..d67b37b0 100644 --- a/apps/browser-extension/entrypoints/content/index.ts +++ b/apps/browser-extension/entrypoints/content/index.ts @@ -2,9 +2,21 @@ import { DOMAINS, MESSAGE_TYPES } from "../../utils/constants" import { DOMUtils } from "../../utils/ui-components" import { initializeChatGPT } from "./chatgpt" import { initializeClaude } from "./claude" -import { saveMemory, setupGlobalKeyboardShortcut, setupStorageListener } from "./shared" +import { + handleOpenSearchPanel, + initializeSelectionSearch, +} from "./selection-search" +import { + saveMemory, + setupGlobalKeyboardShortcut, + setupStorageListener, +} from "./shared" import { initializeT3 } from "./t3" -import { handleTwitterNavigation, initializeTwitter, updateTwitterImportUI } from "./twitter" +import { + handleTwitterNavigation, + initializeTwitter, + updateTwitterImportUI, +} from "./twitter" export default defineContentScript({ matches: [""], @@ -15,6 +27,8 @@ 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.type === MESSAGE_TYPES.IMPORT_UPDATE) { updateTwitterImportUI(message) } else if (message.type === MESSAGE_TYPES.IMPORT_DONE) { @@ -57,6 +71,9 @@ export default defineContentScript({ initializeT3() initializeTwitter() + // Initialize universal selection search + initializeSelectionSearch() + // Start observing for dynamic changes if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", observeForDynamicChanges) @@ -64,4 +81,4 @@ export default defineContentScript({ observeForDynamicChanges() } }, -}) \ No newline at end of file +}) diff --git a/apps/browser-extension/entrypoints/content/selection-search.ts b/apps/browser-extension/entrypoints/content/selection-search.ts new file mode 100644 index 00000000..6473b84c --- /dev/null +++ b/apps/browser-extension/entrypoints/content/selection-search.ts @@ -0,0 +1,647 @@ +import { ELEMENT_IDS, MESSAGE_TYPES, UI_CONFIG } from "../../utils/constants" + +// State +let currentQuery = "" +let fabElement: HTMLElement | null = null +let panelElement: HTMLElement | null = null +let selectedResults: Set = 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 = ` + Search + Search + ` + + 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 = ` +
+ supermemory + Search Results +
+ + ` + + 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 = ` +
Searching for:
+
${escapeHtml(query)}
+ ` + 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 = ` +
+
+ Searching memories... +
+ ` + } else if (state === "auth_error") { + content.innerHTML = ` +
+
Sign in required
+

Please sign in to supermemory to search your memories.

+ +
+ ` + } else if (state === "error") { + content.innerHTML = ` +
+
Search failed
+

${escapeHtml(errorMessage || "An error occurred")}

+
+ ` + } 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 = ` +
+
No memories found
+

Try a different search query.

+
+ ` + } else { + content.innerHTML = results + .map( + (result, index) => ` +
+ +
+

${escapeHtml(result.memory || "")}

+
+
+ `, + ) + .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 = ` + 0 selected + + ` + 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) + } + }) +} diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx index 78864783..c8472da4 100644 --- a/apps/browser-extension/entrypoints/popup/App.tsx +++ b/apps/browser-extension/entrypoints/popup/App.tsx @@ -41,7 +41,7 @@ const Tooltip = ({ type="button" onMouseEnter={() => setIsVisible(true)} onMouseLeave={() => setIsVisible(false)} - className="cursor-help bg-transparent border-none p-0 text-gray-400 hover:text-gray-600 transition-colors" + className="cursor-help bg-transparent border-none p-0 text-[#737373] transition-colors" > {isVisible && ( -
+
{content}
@@ -266,7 +266,7 @@ function App() {
supermemory @@ -302,49 +302,52 @@ function App() { } return ( -
-
- supermemory +
+
+
supermemory
{userSignedIn && ( )}
-
+
{userSignedIn ? (
{/* Tab Navigation */} -
+
@@ -430,12 +450,43 @@ function App() { {/* Save Button at Bottom */}
@@ -445,7 +496,11 @@ function App() {
) : ( -
+
{/* Account Section */}
-

- Account -

-
+
{loadingUserData ? ( -
+
Loading account data...
) : userData?.email ? ( -
- - Email - - + <> + Email + {userData.email} -
+ ) : ( -
+
No email found
)} @@ -545,13 +608,13 @@ function App() { {/* Chat Integration Section */}
-

+

Chat Integration

-
-
+
+
- + Auto Search Memories @@ -565,13 +628,13 @@ function App() { } type="checkbox" /> -
+
-
-
+
+
- + Auto Capture Prompts @@ -585,7 +648,7 @@ function App() { } type="checkbox" /> -
+
@@ -593,8 +656,8 @@ function App() { )} {showProjectSelector && ( -
-
+
+
Select the Project + +
+ + ) : ( + +
+ {params.get("error") && ( +
+ Error: {params.get("error")}. Please try again! +
+ )} + +
+ {process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || + !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ? ( +
+ + Google + + + + + + } + authProvider="Google" + className="w-full" + disabled={isLoading} + onClick={() => { + if (isLoading) return + setIsLoading(true) + posthog.capture("login_attempt", { + method: "social", + provider: "google", + }) + setPendingLoginMethod("google") + signIn + .social({ + callbackURL: getCallbackURL(), + provider: "google", + }) + .finally(() => { + setIsLoading(false) + }) + }} + /> + {lastUsedMethod === "google" && ( +
+ + Last used + +
+ )} +
+ ) : null} + {process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || + !process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? ( +
+ + Github + + + + + + + + + + } + authProvider="Github" + className="w-full" + disabled={isLoading} + onClick={() => { + if (isLoading) return + setIsLoading(true) + posthog.capture("login_attempt", { + method: "social", + provider: "github", + }) + setPendingLoginMethod("github") + signIn + .social({ + callbackURL: getCallbackURL(), + provider: "github", + }) + .finally(() => { + setIsLoading(false) + }) + }} + /> + {lastUsedMethod === "github" && ( +
+ + Last used + +
+ )} +
+ ) : null} +
+ + + +
+
+ { + setEmail(e.target.value) + error && setError(null) + }, + required: true, + value: email, + }} + inputType="email" + /> + +
+ + {lastUsedMethod === "magic_link" && ( +
+ + Last used + +
+ )} +
+ + + + By continuing, you agree to our{" "} + + + Terms + {" "} + and{" "} + + Privacy Policy + + . + + +
+
+
+ )} + +
+ + ) +} diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx index df052a3d..70b53606 100644 --- a/apps/web/app/(auth)/login/page.tsx +++ b/apps/web/app/(auth)/login/page.tsx @@ -1,12 +1,5 @@ -import { LoginPage } from "@repo/ui/pages/login"; +import { LoginPage } from "@repo/ui/pages/login" export default function Page() { - return ( - - ); + return } diff --git a/apps/web/app/(navigation)/page.tsx b/apps/web/app/(navigation)/page.tsx index 212f33c0..73da4f3c 100644 --- a/apps/web/app/(navigation)/page.tsx +++ b/apps/web/app/(navigation)/page.tsx @@ -10,12 +10,14 @@ import { ChromeExtensionButton } from "@/components/chrome-extension-button" import { ChatInput } from "@/components/chat-input" import { BackgroundPlus } from "@ui/components/grid-plus" import { Memories } from "@/components/memories" +import { useFeatureFlagEnabled } from "posthog-js/react" export default function Page() { const { user, session } = useAuth() const { shouldShowOnboarding, isLoading: onboardingLoading } = useOnboardingStorage() const router = useRouter() + const flagEnabled = useFeatureFlagEnabled("nova-alpha-access") useEffect(() => { const url = new URL(window.location.href) @@ -33,7 +35,10 @@ export default function Page() { if (sessionToken && userData?.email) { const encodedToken = encodeURIComponent(sessionToken) - window.postMessage({ token: encodedToken, userData }, window.location.origin) + window.postMessage( + { token: encodedToken, userData }, + window.location.origin, + ) url.searchParams.delete("extension-auth-success") window.history.replaceState({}, "", url.toString()) } @@ -42,9 +47,13 @@ export default function Page() { useEffect(() => { if (user && !onboardingLoading && shouldShowOnboarding()) { - router.push("/onboarding") + if (flagEnabled) { + router.push("/new/onboarding?step=input&flow=welcome") + } else { + router.push("/onboarding") + } } - }, [user, shouldShowOnboarding, onboardingLoading, router]) + }, [user, shouldShowOnboarding, onboardingLoading, router, flagEnabled]) if (!user || onboardingLoading) { return ( diff --git a/apps/web/app/api/exa/fetch-content/route.ts b/apps/web/app/api/exa/fetch-content/route.ts new file mode 100644 index 00000000..6cdb40d5 --- /dev/null +++ b/apps/web/app/api/exa/fetch-content/route.ts @@ -0,0 +1,61 @@ +export interface ExaContentResult { + url: string + text: string + title: string + author?: string +} + +interface ExaApiResponse { + results: ExaContentResult[] +} + +export async function POST(request: Request) { + try { + const { urls } = await request.json() + + if (!Array.isArray(urls) || urls.length === 0) { + return Response.json( + { error: "Invalid input: urls must be a non-empty array" }, + { status: 400 }, + ) + } + + if (!urls.every((url) => typeof url === "string" && url.trim())) { + return Response.json( + { error: "Invalid input: all urls must be non-empty strings" }, + { status: 400 }, + ) + } + + const response = await fetch("https://api.exa.ai/contents", { + method: "POST", + headers: { + "x-api-key": process.env.EXA_API_KEY ?? "", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + urls, + text: true, + livecrawl: "fallback", + }), + }) + + if (!response.ok) { + console.error( + "Exa API request failed:", + response.status, + response.statusText, + ) + return Response.json( + { error: "Failed to fetch content from Exa API" }, + { status: 500 }, + ) + } + + const data: ExaApiResponse = await response.json() + return Response.json({ results: data.results }) + } catch (error) { + console.error("Exa API request error:", error) + return Response.json({ error: "Internal server error" }, { status: 500 }) + } +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 707e4185..030b5b90 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -5,7 +5,7 @@ import "@ui/globals.css" import { AuthProvider } from "@lib/auth-context" import { ErrorTrackingProvider } from "@lib/error-tracking" import { PostHogProvider } from "@lib/posthog" -import { QueryProvider } from "@lib/query-client" +import { QueryProvider } from "../components/query-client" import { AutumnProvider } from "autumn-js/react" import { Suspense } from "react" import { Toaster } from "sonner" @@ -39,8 +39,8 @@ export default function RootLayout({ > { + if (!flagEnabled) { + router.push("/") + } + }, [flagEnabled, router]) + + if (!flagEnabled) { + return null + } + + return <>{children} +} diff --git a/apps/web/app/new/onboarding/page.tsx b/apps/web/app/new/onboarding/page.tsx new file mode 100644 index 00000000..57b5b4fb --- /dev/null +++ b/apps/web/app/new/onboarding/page.tsx @@ -0,0 +1,267 @@ +"use client" + +import { useSearchParams } from "next/navigation" +import { motion, AnimatePresence } from "motion/react" +import { useState, useEffect } from "react" +import { useAuth } from "@lib/auth-context" +import { cn } from "@lib/utils" + +import { InputStep } from "./welcome/input-step" +import { GreetingStep } from "./welcome/greeting-step" +import { WelcomeStep } from "./welcome/welcome-step" +import { ContinueStep } from "./welcome/continue-step" +import { FeaturesStep } from "./welcome/features-step" +import { MemoriesStep } from "./welcome/memories-step" +import { RelatableQuestion } from "./setup/relatable-question" +import { IntegrationsStep } from "./setup/integrations-step" + +import { InitialHeader } from "@/components/initial-header" +import { SetupHeader } from "./setup/header" +import { ChatSidebar } from "./setup/chat-sidebar" +import { Logo } from "@ui/assets/Logo" +import NovaOrb from "@/components/nova/nova-orb" +import { AnimatedGradientBackground } from "@/components/new/animated-gradient-background" + +function UserSupermemory({ name }: { name: string }) { + return ( + + +
+

+ {name.split(" ")[0]}'s +

+

+ supermemory +

+
+
+ ) +} + +export default function OnboardingPage() { + const searchParams = useSearchParams() + const { user } = useAuth() + + const flow = searchParams.get("flow") as "welcome" | "setup" | null + const step = searchParams.get("step") as string | null + + const [name, setName] = useState(user?.name ?? "") + const [isSubmitting, setIsSubmitting] = useState(false) + const [memoryFormData, setMemoryFormData] = useState<{ + twitter: string + linkedin: string + description: string + otherLinks: string[] + } | null>(null) + const [showWelcomeContent, setShowWelcomeContent] = useState(false) + + const currentFlow = flow || "welcome" + const currentStep = step || "input" + + useEffect(() => { + if (user?.name) { + setName(user.name) + localStorage.setItem("username", user.name) + } + }, [user?.name]) + + useEffect(() => { + if (currentFlow === "welcome" && currentStep === "input") { + setShowWelcomeContent(false) + const timer = setTimeout(() => { + setShowWelcomeContent(true) + }, 1250) + return () => clearTimeout(timer) + } + }, [currentFlow, currentStep]) + + useEffect(() => { + if (currentFlow !== "welcome") return + + const timers: NodeJS.Timeout[] = [] + + switch (currentStep) { + case "greeting": + timers.push( + setTimeout(() => { + // Auto-advance to welcome step + window.history.replaceState( + null, + "", + "/new/onboarding?flow=welcome&step=welcome", + ) + }, 2000), + ) + break + case "welcome": + timers.push( + setTimeout(() => { + // Auto-advance to username step + window.history.replaceState( + null, + "", + "/new/onboarding?flow=welcome&step=username", + ) + }, 2000), + ) + break + } + + return () => { + timers.forEach(clearTimeout) + } + }, [currentStep, currentFlow]) + + const handleSubmit = () => { + localStorage.setItem("username", name) + if (name.trim()) { + setIsSubmitting(true) + window.history.replaceState( + null, + "", + "/new/onboarding?flow=welcome&step=greeting", + ) + setIsSubmitting(false) + } + } + + const renderWelcomeStep = () => { + switch (currentStep) { + case "input": + return ( + + ) + case "greeting": + return + case "welcome": + return + case "username": + return + case "features": + return + case "memories": + return + default: + return null + } + } + + const renderSetupStep = () => { + switch (currentStep) { + case "relatable": + return + case "integrations": + return + default: + return null + } + } + + const isWelcomeFlow = currentFlow === "welcome" + const isSetupFlow = currentFlow === "setup" + + const minimizeNovaOrb = + isWelcomeFlow && ["features", "memories"].includes(currentStep) + const novaSize = currentStep === "memories" ? 150 : 300 + + const showUserSupermemory = isWelcomeFlow && currentStep === "username" + + return ( +
+ {isWelcomeFlow && ( + + )} + {isSetupFlow && } + + {isSetupFlow && } + + {isWelcomeFlow && currentStep === "input" && ( + + )} + + {isWelcomeFlow && showWelcomeContent && ( +
+ + + + + {showUserSupermemory && } + + + {renderWelcomeStep()} + +
+ )} + + {isSetupFlow && ( +
+
+
+
+ + {renderSetupStep()} + +
+ + + + +
+
+
+ )} +
+ ) +} diff --git a/apps/web/app/new/onboarding/setup/chat-sidebar.tsx b/apps/web/app/new/onboarding/setup/chat-sidebar.tsx new file mode 100644 index 00000000..d35ce73d --- /dev/null +++ b/apps/web/app/new/onboarding/setup/chat-sidebar.tsx @@ -0,0 +1,424 @@ +"use client" + +import { useState, useEffect, useCallback, useRef } from "react" +import { motion, AnimatePresence } from "motion/react" +import NovaOrb from "@/components/nova/nova-orb" +import { Button } from "@ui/components/button" +import { PanelRightCloseIcon, SendIcon } from "lucide-react" +import { collectValidUrls } from "@/utils/url-helpers" +import { $fetch } from "@lib/api" +import { cn } from "@lib/utils" +import { dmSansClassName } from "@/utils/fonts" + +interface ChatSidebarProps { + formData: { + twitter: string + linkedin: string + description: string + otherLinks: string[] + } | null +} + +export function ChatSidebar({ formData }: ChatSidebarProps) { + const [message, setMessage] = useState("") + const [isChatOpen, setIsChatOpen] = useState(true) + const [messages, setMessages] = useState< + { + message: string + type?: "formData" | "exa" | "memory" | "waiting" + memories?: { + url: string + title: string + description: string + fullContent: string + }[] + url?: string + title?: string + description?: string + }[] + >([]) + const [isLoading, setIsLoading] = useState(false) + const displayedMemoriesRef = useRef>(new Set()) + + const handleSend = () => { + console.log("Message:", message) + setMessage("") + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + handleSend() + } + } + + const toggleChat = () => { + setIsChatOpen(!isChatOpen) + } + + const pollForMemories = useCallback( + async (documentIds: string[]) => { + const maxAttempts = 30 // 30 attempts * 3 seconds = 90 seconds max + const pollInterval = 3000 // 3 seconds + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + const response = await $fetch("@get/documents/:id", { + params: { id: documentIds[0] ?? "" }, + disableValidation: true, + }) + + console.log("response", response) + + if (response.data) { + const document = response.data + + if (document.memories && document.memories.length > 0) { + const newMemories: { + url: string + title: string + description: string + fullContent: string + }[] = [] + + document.memories.forEach( + (memory: { memory: string; title?: string }) => { + if (!displayedMemoriesRef.current.has(memory.memory)) { + displayedMemoriesRef.current.add(memory.memory) + newMemories.push({ + url: document.url || "", + title: memory.title || document.title || "Memory", + description: memory.memory || "", + fullContent: memory.memory || "", + }) + } + }, + ) + + if (newMemories.length > 0 && messages.length < 10) { + setMessages((prev) => [ + ...prev, + { + message: newMemories + .map((memory) => memory.description) + .join("\n"), + type: "memory" as const, + memories: newMemories, + }, + ]) + } + } + + if (document.memories && document.memories.length > 0) { + break + } + } + + await new Promise((resolve) => setTimeout(resolve, pollInterval)) + } catch (error) { + console.warn("Error polling for memories:", error) + await new Promise((resolve) => setTimeout(resolve, pollInterval)) + } + } + }, + [messages.length], + ) + + useEffect(() => { + if (!formData) return + + const urls = collectValidUrls(formData.linkedin, formData.otherLinks) + + console.log("urls", urls) + + const processContent = async () => { + setIsLoading(true) + + try { + const documentIds: string[] = [] + + // Step 1: Fetch content from Exa if URLs exist + if (urls.length > 0) { + const response = await fetch("/api/exa/fetch-content", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ urls }), + }) + const { results } = await response.json() + console.log("results", results) + + // Create documents from Exa results + for (const result of results) { + try { + const docResponse = await $fetch("@post/documents", { + body: { + content: result.text || result.description || "", + containerTags: ["sm_project_default"], + metadata: { + sm_source: "consumer", + exa_url: result.url, + exa_title: result.title, + }, + }, + }) + + if (docResponse.data?.id) { + documentIds.push(docResponse.data.id) + } + } catch (error) { + console.warn("Error creating document:", error) + } + } + } + + // Step 2: Create document from description if it exists + if (formData.description?.trim()) { + try { + const descDocResponse = await $fetch("@post/documents", { + body: { + content: formData.description, + containerTags: ["sm_project_default"], + metadata: { + sm_source: "consumer", + description_source: "user_input", + }, + }, + }) + + if (descDocResponse.data?.id) { + documentIds.push(descDocResponse.data.id) + } + } catch (error) { + console.warn("Error creating description document:", error) + } + } + + // Step 3: Poll for memories or show form data + if (documentIds.length > 0) { + await pollForMemories(documentIds) + } else { + // No documents created, show form data or waiting + const formDataMessages = [] + + if (formData.twitter) { + formDataMessages.push({ + message: `Twitter: ${formData.twitter}`, + url: formData.twitter, + title: "Twitter Profile", + description: `Twitter: ${formData.twitter}`, + type: "formData" as const, + }) + } + + if (formData.linkedin) { + formDataMessages.push({ + message: `LinkedIn: ${formData.linkedin}`, + url: formData.linkedin, + title: "LinkedIn Profile", + description: `LinkedIn: ${formData.linkedin}`, + type: "formData" as const, + }) + } + + if (formData.otherLinks.length > 0) { + formData.otherLinks.forEach((link) => { + formDataMessages.push({ + message: `Link: ${link}`, + url: link, + title: "Other Link", + description: `Link: ${link}`, + type: "formData" as const, + }) + }) + } + + const waitingMessage = { + message: "Waiting for your input", + url: "", + title: "", + description: "Waiting for your input", + type: "waiting" as const, + } + + setMessages([...formDataMessages, waitingMessage]) + } + } catch (error) { + console.warn("Error processing content:", error) + + const waitingMessage = { + message: "Waiting for your input", + url: "", + title: "", + description: "Waiting for your input", + type: "waiting" as const, + } + + setMessages([waitingMessage]) + } + setIsLoading(false) + } + + processContent() + }, [formData, pollForMemories]) + + return ( + + {!isChatOpen ? ( + + + + Chat with Nova + + + ) : ( + + + + Close chat + +
+ {messages.map((msg, i) => ( +
+ {msg.type === "waiting" ? ( +
+ + {msg.message} +
+ ) : ( + <> +
+ {i === 0 && ( +
+ )} +
+
+ {msg.type === "memory" && ( +
+ {msg.memories?.map((memory) => ( +
+ {memory.title && ( +

+ {memory.title} +

+ )} + {memory.url && ( + + {memory.url} + + )} + {memory.description && ( +

+ {memory.description} +

+ )} +
+ ))} +
+ )} + + )} +
+ ))} + {messages.length === 0 && !isLoading && !formData && ( +
+ + Waiting for your input +
+ )} + {isLoading && ( +
+ + Fetching your memories... +
+ )} +
+ +
+
{ + e.preventDefault() + if (message.trim()) { + handleSend() + } + }} + > + setMessage(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Chat with your Supermemory" + className="w-full text-white placeholder:text-white/20 rounded-sm outline-none resize-none text-base leading-relaxed bg-transparent px-2 h-10" + /> +
+ +
+
+
+ + )} + + ) +} diff --git a/apps/web/app/new/onboarding/setup/header.tsx b/apps/web/app/new/onboarding/setup/header.tsx new file mode 100644 index 00000000..4981c6aa --- /dev/null +++ b/apps/web/app/new/onboarding/setup/header.tsx @@ -0,0 +1,47 @@ +import { motion } from "motion/react" +import { Logo } from "@ui/assets/Logo" +import { useAuth } from "@lib/auth-context" +import { useEffect, useState } from "react" +import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar" + +export function SetupHeader() { + const { user } = useAuth() + const [name, setName] = useState("") + + useEffect(() => { + const storedName = + localStorage.getItem("username") || localStorage.getItem("userName") || "" + setName(storedName) + }, []) + + const userName = name ? `${name.split(" ")[0]}'s` : "My" + + return ( + +
+ + {name && ( +
+

+ {userName} +

+

+ supermemory +

+
+ )} +
+ {user && ( + + + {user?.name?.charAt(0)} + + )} +
+ ) +} diff --git a/apps/web/app/new/onboarding/setup/integrations-step.tsx b/apps/web/app/new/onboarding/setup/integrations-step.tsx new file mode 100644 index 00000000..ff1ce96f --- /dev/null +++ b/apps/web/app/new/onboarding/setup/integrations-step.tsx @@ -0,0 +1,181 @@ +"use client" + +import { useState } from "react" +import { Button } from "@ui/components/button" +import { MCPDetailView } from "@/components/new/mcp-modal/mcp-detail-view" +import { XBookmarksDetailView } from "@/components/x-bookmarks-detail-view" +import { useRouter } from "next/navigation" +import { cn } from "@lib/utils" +import { dmSansClassName } from "@/utils/fonts" +import { useOnboardingStorage } from "@hooks/use-onboarding-storage" + +const integrationCards = [ + { + title: "Capture", + description: "Add the Chrome extension for one-click saves", + icon: ( +
+ Chrome +
+ ), + }, + { + title: "Connect to AI", + description: "Set up once and use your memory in Cursor, Claude, etc", + icon: ( +
+ MCP +
+ ), + }, + { + title: "Connect", + description: "Link Notion, Google Drive, or OneDrive to import your docs", + icon: ( +
+ Connectors +
+ ), + }, + { + title: "Import", + description: + "Bring in X/Twitter bookmarks, and turn them into useful memories", + icon: ( +
+ X +
+ ), + }, +] + +export function IntegrationsStep() { + const router = useRouter() + const [selectedCard, setSelectedCard] = useState(null) + const { markOnboardingCompleted } = useOnboardingStorage() + + const handleContinue = () => { + markOnboardingCompleted() + router.push("/new") + } + + if (selectedCard === "Connect to AI") { + return setSelectedCard(null)} /> + } + if (selectedCard === "Import") { + return setSelectedCard(null)} /> + } + return ( +
+
+

+ Build your personal memory +

+

+ Your supermemory comes alive when you
capture and connect + what's important +

+
+ +
+ {integrationCards.map((card) => { + const isClickable = + card.title === "Connect to AI" || + card.title === "Capture" || + card.title === "Import" + + if (isClickable) { + return ( + + ) + } + + return ( +
+
+

{card.title}

+

+ {card.description} +

+
+
{card.icon}
+
+ ) + })} +
+ +
+ + +
+
+ ) +} diff --git a/apps/web/app/new/onboarding/setup/relatable-question.tsx b/apps/web/app/new/onboarding/setup/relatable-question.tsx new file mode 100644 index 00000000..c853985d --- /dev/null +++ b/apps/web/app/new/onboarding/setup/relatable-question.tsx @@ -0,0 +1,159 @@ +"use client" + +import { useState } from "react" +import { motion, AnimatePresence } from "motion/react" +import { Button } from "@ui/components/button" +import { useRouter } from "next/navigation" +import { cn } from "@lib/utils" +import { dmSansClassName } from "@/utils/fonts" + +const relatableOptions = [ + { + emoji: "😔", + text: "I always forget what I save in my twitter bookmarks", + }, + { + emoji: "😭", + text: "Going through e-books manually is so tedious", + }, + { + emoji: "🥲", + text: "I always have to feed every AI app with my data", + }, + { + emoji: "😵‍💫", + text: "Referring meeting notes makes my AI chat hallucinate", + }, + { + emoji: "🫤", + text: "I save nothing on my browser, it's just useless", + }, +] + +export function RelatableQuestion() { + const router = useRouter() + const [selectedOptions, setSelectedOptions] = useState([]) + + const handleContinueOrSkip = () => { + router.push("/new/onboarding?flow=setup&step=integrations") + } + + return ( + + + Which of these sound most relatable? + + +
+ {relatableOptions.map((option, index) => ( +
+ +
+ ))} +
+
+
+ +
+
+
+ ) +} diff --git a/apps/web/app/new/onboarding/welcome/continue-step.tsx b/apps/web/app/new/onboarding/welcome/continue-step.tsx new file mode 100644 index 00000000..eefab753 --- /dev/null +++ b/apps/web/app/new/onboarding/welcome/continue-step.tsx @@ -0,0 +1,45 @@ +import { dmSansClassName } from "@/utils/fonts" +import { cn } from "@lib/utils" +import { Button } from "@ui/components/button" +import { motion } from "motion/react" +import { useRouter } from "next/navigation" + +export function ContinueStep() { + const router = useRouter() + + const handleContinue = () => { + router.push("/new/onboarding?flow=welcome&step=features") + } + + return ( + +

+ I'm built with Supermemory's super fast memory API, +
so you never have to worry about forgetting
what matters + across your AI apps. +

+ +
+ ) +} diff --git a/apps/web/app/new/onboarding/welcome/features-step.tsx b/apps/web/app/new/onboarding/welcome/features-step.tsx new file mode 100644 index 00000000..6d15e2f8 --- /dev/null +++ b/apps/web/app/new/onboarding/welcome/features-step.tsx @@ -0,0 +1,98 @@ +import { motion } from "motion/react" +import { Button } from "@ui/components/button" +import { useRouter } from "next/navigation" +import { cn } from "@lib/utils" +import { dmSansClassName } from "@/utils/fonts" + +export function FeaturesStep() { + const router = useRouter() + + const handleContinue = () => { + router.push("/new/onboarding?flow=welcome&step=memories") + } + return ( + +

+ What I can do for you +

+ +
+
+
+ Brain icon +
+
+

Remember every context

+

+ I keep track of what you've saved and shared with your + supermemory. +

+
+
+ +
+
+ Search icon +
+
+

Find when you need it

+

+ I surface the right memories inside
your supermemory, + superfast. +

+
+
+ +
+
+ Growth icon +
+
+

Grow with your supermemory

+

+ I learn and personalize over time, so every interaction feels + natural. +

+
+
+
+ + + + +
+ ) +} diff --git a/apps/web/app/new/onboarding/welcome/greeting-step.tsx b/apps/web/app/new/onboarding/welcome/greeting-step.tsx new file mode 100644 index 00000000..744e3719 --- /dev/null +++ b/apps/web/app/new/onboarding/welcome/greeting-step.tsx @@ -0,0 +1,23 @@ +import { motion } from "motion/react" + +interface GreetingStepProps { + name: string +} + +export function GreetingStep({ name }: GreetingStepProps) { + const userName = name ? `${name.split(" ")[0]}` : "" + return ( + +

+ Hi {userName}, I'm Nova +

+
+ ) +} diff --git a/apps/web/app/new/onboarding/welcome/input-step.tsx b/apps/web/app/new/onboarding/welcome/input-step.tsx new file mode 100644 index 00000000..077d4944 --- /dev/null +++ b/apps/web/app/new/onboarding/welcome/input-step.tsx @@ -0,0 +1,96 @@ +import { motion } from "motion/react" +import { LabeledInput } from "@ui/input/labeled-input" +import { Button } from "@ui/components/button" + +interface InputStepProps { + name: string + setName: (name: string) => void + handleSubmit: () => void + isSubmitting: boolean +} + +export function InputStep({ + name, + setName, + handleSubmit, + isSubmitting, +}: InputStepProps) { + return ( + +

+ What should I call you? +

+
+ { + if (e.key === "Enter") { + handleSubmit() + } + }, + className: "!text-white placeholder:!text-[#525966] !h-[40px] pl-4", + }} + onChange={(e) => setName((e.target as HTMLInputElement).value)} + style={{ + background: + "linear-gradient(0deg, rgba(91, 126, 245, 0.04) 0%, rgba(91, 126, 245, 0.04) 100%)", + }} + /> + +
+
+ ) +} diff --git a/apps/web/app/new/onboarding/welcome/memories-step.tsx b/apps/web/app/new/onboarding/welcome/memories-step.tsx new file mode 100644 index 00000000..4230f510 --- /dev/null +++ b/apps/web/app/new/onboarding/welcome/memories-step.tsx @@ -0,0 +1,299 @@ +import { motion } from "motion/react" +import { Button } from "@ui/components/button" +import { useState } from "react" +import { useRouter } from "next/navigation" +import { cn } from "@lib/utils" +import { dmSansClassName } from "@/utils/fonts" + +interface MemoriesStepProps { + onSubmit: (data: { + twitter: string + linkedin: string + description: string + otherLinks: string[] + }) => void +} + +type ValidationError = { + twitter: string | null + linkedin: string | null +} + +export function MemoriesStep({ onSubmit }: MemoriesStepProps) { + const router = useRouter() + const [otherLinks, setOtherLinks] = useState([""]) + const [twitterHandle, setTwitterHandle] = useState("") + const [linkedinProfile, setLinkedinProfile] = useState("") + const [description, setDescription] = useState("") + const [isSubmitting] = useState(false) + const [errors, setErrors] = useState({ + twitter: null, + linkedin: null, + }) + + const addOtherLink = () => { + if (otherLinks.length < 3) { + setOtherLinks([...otherLinks, ""]) + } + } + + const updateOtherLink = (index: number, value: string) => { + const updated = [...otherLinks] + updated[index] = value + setOtherLinks(updated) + } + + const validateTwitterLink = (value: string): string | null => { + if (!value.trim()) return null + + const normalized = value.trim().toLowerCase() + const isXDomain = + normalized.includes("x.com") || normalized.includes("twitter.com") + + if (!isXDomain) { + return "share your X profile link" + } + + // Check if it's a profile link (not a status/tweet link) + const profilePattern = + /^(https?:\/\/)?(www\.)?(x\.com|twitter\.com)\/[^\/]+$/ + const statusPattern = /\/status\//i + + if (statusPattern.test(normalized) || !profilePattern.test(normalized)) { + return "share your X profile link" + } + + // Note: 404 validation would require a backend API endpoint + // Format validation is handled above + return null + } + + const validateLinkedInLink = (value: string): string | null => { + if (!value.trim()) return null + + const normalized = value.trim().toLowerCase() + const isLinkedInDomain = normalized.includes("linkedin.com") + + if (!isLinkedInDomain) { + return "share your Linkedin profile link" + } + + // Check if it's a profile link (should have /in/ or /pub/) + const profilePattern = + /^(https?:\/\/)?(www\.)?linkedin\.com\/(in|pub)\/[^\/]+/ + + if (!profilePattern.test(normalized)) { + return "share your Linkedin profile link" + } + + // Note: 404 validation would require a backend API endpoint + // Format validation is handled above + return null + } + + const handleTwitterChange = (value: string) => { + setTwitterHandle(value) + const error = validateTwitterLink(value) + setErrors((prev) => ({ ...prev, twitter: error })) + } + + const handleLinkedInChange = (value: string) => { + setLinkedinProfile(value) + const error = validateLinkedInLink(value) + setErrors((prev) => ({ ...prev, linkedin: error })) + } + + return ( + +

+ Let's add your memories +

+ +
+
+ +
+ handleTwitterChange(e.target.value)} + onBlur={() => { + if (twitterHandle.trim()) { + const error = validateTwitterLink(twitterHandle) + setErrors((prev) => ({ ...prev, twitter: error })) + } + }} + className={`w-full px-4 py-2 bg-[#070E1B] border rounded-xl text-white placeholder-onboarding focus:outline-none transition-colors h-[40px] ${ + errors.twitter + ? "border-[#52596633] bg-[#290F0A]" + : "border-onboarding/20" + }`} + /> + {errors.twitter && ( +
+
+
+

{errors.twitter}

+
+
+ )} +
+
+ +
+ +
+ handleLinkedInChange(e.target.value)} + onBlur={() => { + if (linkedinProfile.trim()) { + const error = validateLinkedInLink(linkedinProfile) + setErrors((prev) => ({ ...prev, linkedin: error })) + } + }} + className={`w-full px-4 py-2 bg-[#070E1B] border rounded-xl text-white placeholder-onboarding focus:outline-none transition-colors h-[40px] ${ + errors.linkedin + ? "border-[#52596633] bg-[#290F0A]" + : "border-onboarding/20" + }`} + /> + {errors.linkedin && ( +
+
+
+

{errors.linkedin}

+
+
+ )} +
+
+ + + +
+ +