diff --git a/apps/browser-extension/.gitignore b/apps/browser-extension/.gitignore new file mode 100644 index 00000000..a2569538 --- /dev/null +++ b/apps/browser-extension/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.output +stats.html +stats-*.json +.wxt +web-ext.config.ts + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/browser-extension/README.md b/apps/browser-extension/README.md new file mode 100644 index 00000000..b19db00d --- /dev/null +++ b/apps/browser-extension/README.md @@ -0,0 +1 @@ +## Supermemory Browser Extension \ No newline at end of file diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts new file mode 100644 index 00000000..2853ec45 --- /dev/null +++ b/apps/browser-extension/entrypoints/background.ts @@ -0,0 +1,84 @@ +export default defineBackground(() => { + browser.runtime.onInstalled.addListener(() => { + browser.contextMenus.create({ + id: 'save-to-supermemory', + title: 'Save to Supermemory', + contexts: ['selection', 'page', 'link'], + }); + }); + + browser.contextMenus.onClicked.addListener(async (info, tab) => { + if (info.menuItemId === 'save-to-supermemory') { + if (tab?.id) { + try { + await browser.tabs.sendMessage(tab.id, { + action: 'saveMemory', + }); + } catch (error) { + console.error('Failed to send message to content script:', error); + console.log('Content script may not be injected on this page'); + } + } + } + }); + + browser.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === 'saveMemory') { + (async () => { + try { + const result = await browser.storage.local.get(['bearerToken']); + const bearerToken = result.bearerToken; + //const backendURL = 'http://localhost:8787'; + const backendURL = 'https://api.supermemory.ai'; + + if (!bearerToken) { + console.error('No bearer token found'); + sendResponse({ success: false, error: 'No authentication token found' }); + return; + } + + const response = await fetch(`${backendURL}/v3/memories`, { + method: 'POST', + credentials: 'omit', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${bearerToken}`, + }, + body: JSON.stringify({ + containerTags: ['sm_project_default'], + content: + message.data.highlightedText + + '\n\n' + + message.data.html + + '\n\n' + + message.data.url, + metadata: { sm_source: 'consumer' }, + }), + }); + + if (!response.ok) { + const errorData = await response.text(); + console.error('API call failed:', response.status, errorData); + sendResponse({ + success: false, + error: `API call failed: ${response.status}`, + }); + return; + } + + const data = await response.json(); + console.log('Memory saved successfully:', data); + sendResponse({ success: true, data }); + } catch (error) { + console.error('Error saving memory:', error); + sendResponse({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + })(); + + return true; + } + }); +}); diff --git a/apps/browser-extension/entrypoints/content.ts b/apps/browser-extension/entrypoints/content.ts new file mode 100644 index 00000000..cd621d9c --- /dev/null +++ b/apps/browser-extension/entrypoints/content.ts @@ -0,0 +1,212 @@ +export default defineContentScript({ + matches: [''], + main() { + let currentToast: HTMLElement | null = null; + + browser.runtime.onMessage.addListener(async (message) => { + if (message.action === 'showToast') { + showToast(message.state); + } else if (message.action === 'saveMemory') { + await saveMemory(); + } + }); + + async function saveMemory() { + try { + showToast('loading'); + + const highlightedText = window.getSelection()?.toString() || ''; + + const url = window.location.href; + + const html = document.documentElement.outerHTML; + + const response = await browser.runtime.sendMessage({ + action: 'saveMemory', + data: { + html, + highlightedText, + url, + }, + }); + + console.log('Response from enxtension:', response); + if (response.success) { + showToast('success'); + } else { + showToast('error'); + } + } catch (error) { + console.error('Error saving memory:', error); + showToast('error'); + } + } + + function showToast(state: 'loading' | 'success' | 'error') { + if (currentToast) { + currentToast.remove(); + } + + const toast = document.createElement('div'); + toast.id = 'supermemory-toast'; + + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + z-index: 2147483647; + background: #ffffff; + border-radius: 9999px; + padding: 12px 16px; + display: flex; + align-items: center; + gap: 12px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + color: #374151; + min-width: 200px; + max-width: 300px; + animation: slideIn 0.3s ease-out; + `; + + if (!document.getElementById('supermemory-toast-styles')) { + const style = document.createElement('style'); + style.id = 'supermemory-toast-styles'; + style.textContent = ` + @keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + @keyframes fadeOut { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } + } + `; + document.head.appendChild(style); + } + + const icon = document.createElement('div'); + icon.style.cssText = ` + width: 20px; + height: 20px; + flex-shrink: 0; + `; + + const text = document.createElement('span'); + text.style.cssText = ` + font-weight: 500; + `; + + if (state === 'loading') { + icon.innerHTML = ` + + + + + + + + + + + `; + icon.style.animation = 'spin 1s linear infinite'; + if (!document.getElementById('supermemory-spinner-styles')) { + const spinStyle = document.createElement('style'); + spinStyle.id = 'supermemory-spinner-styles'; + spinStyle.textContent = ` + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + `; + document.head.appendChild(spinStyle); + } + text.textContent = 'Adding to Memory...'; + } else if (state === 'success') { + const iconUrl = browser.runtime.getURL('/icon-16.png'); + icon.innerHTML = ` + Success + `; + text.textContent = 'Added to Memory'; + } else if (state === 'error') { + icon.innerHTML = ` + + + + + + `; + text.textContent = 'Failed to save memory / Make sure you are logged in'; + } + + toast.appendChild(icon); + toast.appendChild(text); + document.body.appendChild(toast); + currentToast = toast; + + if (state === 'success' || state === 'error') { + setTimeout(() => { + if (currentToast === toast) { + toast.style.animation = 'fadeOut 0.3s ease-out'; + setTimeout(() => { + if (toast.parentNode) { + toast.remove(); + } + if (currentToast === toast) { + currentToast = null; + } + }, 300); + } + }, 3000); + } + } + + document.addEventListener('keydown', async (event) => { + if ( + (event.ctrlKey || event.metaKey) && + event.shiftKey && + event.key === 'm' + ) { + event.preventDefault(); + await saveMemory(); + } + }); + + window.addEventListener('message', (event) => { + if (event.source !== window) { + return; + } + const bearerToken = event.data.token; + + if (bearerToken) { + if ( + !( + window.location.hostname === 'localhost' || + window.location.hostname === 'supermemory.ai' || + window.location.hostname === 'app.supermemory.ai' + ) + ) { + console.log( + 'Bearer token is only allowed to be used on localhost or supermemory.ai' + ); + return; + } + + chrome.storage.local.set({ bearerToken }, () => {}); + } + }); + }, +}); diff --git a/apps/browser-extension/entrypoints/popup/App.css b/apps/browser-extension/entrypoints/popup/App.css new file mode 100644 index 00000000..a6dcc809 --- /dev/null +++ b/apps/browser-extension/entrypoints/popup/App.css @@ -0,0 +1,83 @@ +.popup-container { + width: 320px; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #ffffff; + border-radius: 8px; +} + +.header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + border-bottom: 1px solid #e5e7eb; +} + +.header .logo { + width: 32px; + height: 32px; + flex-shrink: 0; +} + +.header h1 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #000000; +} + +.content { + padding: 16px; +} + +.status { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + font-size: 14px; + color: #000000; +} + +.status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.status-indicator.signed-in { + background-color: #000000; +} + +.status-indicator.signed-out { + background-color: #666666; +} + +.sign-out-btn { + width: 100%; + padding: 8px 16px; + background-color: #000000; + color: white; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; +} + +.sign-out-btn:hover { + background-color: #333333; +} + +.instruction { + margin: 0; + font-size: 13px; + color: #666666; + line-height: 1.4; +} + +.authenticated, .unauthenticated { + text-align: left; +} diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx new file mode 100644 index 00000000..40e108af --- /dev/null +++ b/apps/browser-extension/entrypoints/popup/App.tsx @@ -0,0 +1,89 @@ +import React, { useState, useEffect } from 'react'; +import './App.css'; + +function App() { + const [userSignedIn, setUserSignedIn] = useState(false); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const checkAuthStatus = async () => { + try { + const result = await chrome.storage.local.get(['bearerToken']); + setUserSignedIn(!!result.bearerToken); + } catch (error) { + console.error('Error checking auth status:', error); + setUserSignedIn(false); + } finally { + setLoading(false); + } + }; + + checkAuthStatus(); + }, []); + + const handleSignOut = async () => { + try { + await chrome.storage.local.remove(['bearerToken']); + setUserSignedIn(false); + } catch (error) { + console.error('Error signing out:', error); + } + }; + + if (loading) { + return ( +
+
+ Supermemory +

Supermemory

+
+
+
Loading...
+
+
+ ); + } + + return ( +
+
+ Supermemory +

Supermemory

+
+
+ {userSignedIn ? ( +
+
+ + Signed in +
+ +
+ ) : ( + + )} +
+
+ ); +} + +export default App; diff --git a/apps/browser-extension/entrypoints/popup/index.html b/apps/browser-extension/entrypoints/popup/index.html new file mode 100644 index 00000000..ed4cb949 --- /dev/null +++ b/apps/browser-extension/entrypoints/popup/index.html @@ -0,0 +1,13 @@ + + + + + + Default Popup Title + + + +
+ + + diff --git a/apps/browser-extension/entrypoints/popup/main.tsx b/apps/browser-extension/entrypoints/popup/main.tsx new file mode 100644 index 00000000..cde361f0 --- /dev/null +++ b/apps/browser-extension/entrypoints/popup/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.js'; +import './style.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/apps/browser-extension/entrypoints/popup/style.css b/apps/browser-extension/entrypoints/popup/style.css new file mode 100644 index 00000000..2c3fac68 --- /dev/null +++ b/apps/browser-extension/entrypoints/popup/style.css @@ -0,0 +1,69 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json new file mode 100644 index 00000000..0888df7d --- /dev/null +++ b/apps/browser-extension/package.json @@ -0,0 +1,29 @@ +{ + "name": "Supermemory", + "description": "supermemory", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "wxt --port 3001", + "dev:firefox": "wxt -b firefox", + "build": "wxt build", + "build:firefox": "wxt build -b firefox", + "zip": "wxt zip", + "zip:firefox": "wxt zip -b firefox", + "compile": "tsc --noEmit", + "postinstall": "wxt prepare" + }, + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/chrome": "^0.1.4", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.3", + "@wxt-dev/module-react": "^1.1.3", + "typescript": "^5.8.3", + "wxt": "^0.20.6" + } +} diff --git a/apps/browser-extension/public/icon-128.png b/apps/browser-extension/public/icon-128.png new file mode 100644 index 00000000..55a25aa8 Binary files /dev/null and b/apps/browser-extension/public/icon-128.png differ diff --git a/apps/browser-extension/public/icon-16.png b/apps/browser-extension/public/icon-16.png new file mode 100644 index 00000000..55a25aa8 Binary files /dev/null and b/apps/browser-extension/public/icon-16.png differ diff --git a/apps/browser-extension/public/icon-48.png b/apps/browser-extension/public/icon-48.png new file mode 100644 index 00000000..55a25aa8 Binary files /dev/null and b/apps/browser-extension/public/icon-48.png differ diff --git a/apps/browser-extension/tsconfig.json b/apps/browser-extension/tsconfig.json new file mode 100644 index 00000000..4217f189 --- /dev/null +++ b/apps/browser-extension/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./.wxt/tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "types": ["chrome"] + } +} diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts new file mode 100644 index 00000000..b4370f3c --- /dev/null +++ b/apps/browser-extension/wxt.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'wxt'; + +// See https://wxt.dev/api/config.html +export default defineConfig({ + modules: ['@wxt-dev/module-react'], + manifest: { + permissions: ['contextMenus', 'storage', 'scripting', 'activeTab'], + web_accessible_resources: [ + { + resources: ['icon-16.png'], + matches: [''] + } + ], + }, + webExt: { + chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'], + }, +}); diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index d6edc122..680d208a 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,393 +1,394 @@ -"use client"; +'use client'; -import { useIsMobile } from "@hooks/use-mobile"; -import { useAuth } from "@lib/auth-context"; -import { $fetch } from "@repo/lib/api"; -import { MemoryGraph } from "@repo/ui/memory-graph"; -import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"; -import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; -import { Logo, LogoFull } from "@ui/assets/Logo"; -import { Button } from "@ui/components/button"; -import { GlassMenuEffect } from "@ui/other/glass-effect"; +import { useIsMobile } from '@hooks/use-mobile'; +import { useAuth } from '@lib/auth-context'; +import { $fetch } from '@repo/lib/api'; +import { MemoryGraph } from '@repo/ui/memory-graph'; +import type { DocumentsWithMemoriesResponseSchema } from '@repo/validation/api'; +import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; +import { Logo, LogoFull } from '@ui/assets/Logo'; +import { GlassMenuEffect } from '@ui/other/glass-effect'; +import { Button } from '@ui/components/button'; import { - Gift, - LayoutGrid, - List, - LoaderIcon, - MessageSquare, - Unplug, -} from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import type { z } from "zod"; -import { ConnectAIModal } from "@/components/connect-ai-modal"; -import { InstallPrompt } from "@/components/install-prompt"; -import { MemoryListView } from "@/components/memory-list-view"; -import Menu from "@/components/menu"; -import { ProjectSelector } from "@/components/project-selector"; -import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal"; -import type { TourStep } from "@/components/tour"; -import { TourAlertDialog, useTour } from "@/components/tour"; -import { AddMemoryView } from "@/components/views/add-memory"; -import { ChatRewrite } from "@/components/views/chat"; -import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants"; -import { useViewMode } from "@/lib/view-mode-context"; -import { useChatOpen, useProject } from "@/stores"; -import { useGraphHighlights } from "@/stores/highlights"; + Gift, + LayoutGrid, + List, + LoaderIcon, + MessageSquare, + Unplug, +} from 'lucide-react'; +import { AnimatePresence, motion } from 'motion/react'; +import Link from 'next/link'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import type { z } from 'zod'; +import { MemoryListView } from '@/components/memory-list-view'; +import Menu from '@/components/menu'; +import type { TourStep } from '@/components/tour'; +import { TourAlertDialog, useTour } from '@/components/tour'; +import { useProject } from '@/stores'; +import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from '@/lib/tour-constants'; +import { useViewMode } from '@/lib/view-mode-context'; +import { useChatOpen } from '@/stores'; +import { ChatRewrite } from '@/components/views/chat'; +import { useGraphHighlights } from '@/stores/highlights'; +import { ProjectSelector } from '@/components/project-selector'; +import { AddMemoryView } from '@/components/views/add-memory'; +import { ReferralUpgradeModal } from '@/components/referral-upgrade-modal'; +import { ConnectAIModal } from '@/components/connect-ai-modal'; +import { InstallPrompt } from '@/components/install-prompt'; type DocumentsResponse = z.infer; -type DocumentWithMemories = DocumentsResponse["documents"][0]; +type DocumentWithMemories = DocumentsResponse['documents'][0]; const MemoryGraphPage = () => { - const { documentIds: allHighlightDocumentIds } = useGraphHighlights(); - const isMobile = useIsMobile(); - const { viewMode, setViewMode, isInitialized } = useViewMode(); - const { selectedProject } = useProject(); - const { setSteps, isTourCompleted } = useTour(); - const { isOpen, setIsOpen } = useChatOpen(); - const [injectedDocs, setInjectedDocs] = useState([]); - const [showAddMemoryView, setShowAddMemoryView] = useState(false); - const [showReferralModal, setShowReferralModal] = useState(false); + const { documentIds: allHighlightDocumentIds } = useGraphHighlights(); + const isMobile = useIsMobile(); + const { viewMode, setViewMode, isInitialized } = useViewMode(); + const { selectedProject } = useProject(); + const { setSteps, isTourCompleted } = useTour(); + const { isOpen, setIsOpen } = useChatOpen(); + const [injectedDocs, setInjectedDocs] = useState([]); + const [showAddMemoryView, setShowAddMemoryView] = useState(false); + const [showReferralModal, setShowReferralModal] = useState(false); - // Fetch projects meta to detect experimental flag - const { data: projectsMeta = [] } = useQuery({ - queryKey: ["projects"], - queryFn: async () => { - const response = await $fetch("@get/projects"); - return response.data?.projects ?? []; - }, - staleTime: 5 * 60 * 1000, - }); + // Fetch projects meta to detect experimental flag + const { data: projectsMeta = [] } = useQuery({ + queryKey: ['projects'], + queryFn: async () => { + const response = await $fetch('@get/projects'); + return response.data?.projects ?? []; + }, + staleTime: 5 * 60 * 1000, + }); - const isCurrentProjectExperimental = !!projectsMeta.find( - (p: any) => p.containerTag === selectedProject, - )?.isExperimental; + const isCurrentProjectExperimental = !!projectsMeta.find( + (p: any) => p.containerTag === selectedProject + )?.isExperimental; - // Tour state - const [showTourDialog, setShowTourDialog] = useState(false); + // Tour state + const [showTourDialog, setShowTourDialog] = useState(false); - // Define tour steps with useMemo to prevent recreation - const tourSteps: TourStep[] = useMemo(() => { - return [ - { - content: ( -
-

- Memories Overview -

-

- This is your memory graph. Each node represents a memory, and - connections show relationships between them. -

-
- ), - selectorId: TOUR_STEP_IDS.MEMORY_GRAPH, - position: "center", - }, - { - content: ( -
-

- Add Memories -

-

- Click here to add new memories to your knowledge base. You can add - text, links, or connect external sources. -

-
- ), - selectorId: TOUR_STEP_IDS.MENU_ADD_MEMORY, - position: "right", - }, - { - content: ( -
-

- Connections -

-

- Connect your external accounts like Google Drive, Notion, or - OneDrive to automatically sync and organize your content. -

-
- ), - selectorId: TOUR_STEP_IDS.MENU_CONNECTIONS, - position: "right", - }, - { - content: ( -
-

Projects

-

- Organize your memories into projects. Switch between different - contexts easily. -

-
- ), - selectorId: TOUR_STEP_IDS.MENU_PROJECTS, - position: "right", - }, - { - content: ( -
-

- MCP Servers -

-

- Access Model Context Protocol servers to give AI tools access to - your memories securely. -

-
- ), - selectorId: TOUR_STEP_IDS.MENU_MCP, - position: "right", - }, - { - content: ( -
-

Billing

-

- Manage your subscription and billing information. -

-
- ), - selectorId: TOUR_STEP_IDS.MENU_BILLING, - position: "right", - }, - { - content: ( -
-

- View Toggle -

-

- Switch between graph view and list view to see your memories in - different ways. -

-
- ), - selectorId: TOUR_STEP_IDS.VIEW_TOGGLE, - position: "left", - }, - { - content: ( -
-

Legend

-

- Understand the different types of nodes and connections in your - memory graph. -

-
- ), - selectorId: TOUR_STEP_IDS.LEGEND, - position: "left", - }, - { - content: ( -
-

- Chat Assistant -

-

- Ask questions or add new memories using our AI-powered chat - interface. -

-
- ), - selectorId: TOUR_STEP_IDS.FLOATING_CHAT, - position: "left", - }, - ]; - }, []); + // Define tour steps with useMemo to prevent recreation + const tourSteps: TourStep[] = useMemo(() => { + return [ + { + content: ( +
+

+ Memories Overview +

+

+ This is your memory graph. Each node represents a memory, and + connections show relationships between them. +

+
+ ), + selectorId: TOUR_STEP_IDS.MEMORY_GRAPH, + position: 'center', + }, + { + content: ( +
+

+ Add Memories +

+

+ Click here to add new memories to your knowledge base. You can add + text, links, or connect external sources. +

+
+ ), + selectorId: TOUR_STEP_IDS.MENU_ADD_MEMORY, + position: 'right', + }, + { + content: ( +
+

+ Connections +

+

+ Connect your external accounts like Google Drive, Notion, or + OneDrive to automatically sync and organize your content. +

+
+ ), + selectorId: TOUR_STEP_IDS.MENU_CONNECTIONS, + position: 'right', + }, + { + content: ( +
+

Projects

+

+ Organize your memories into projects. Switch between different + contexts easily. +

+
+ ), + selectorId: TOUR_STEP_IDS.MENU_PROJECTS, + position: 'right', + }, + { + content: ( +
+

+ MCP Servers +

+

+ Access Model Context Protocol servers to give AI tools access to + your memories securely. +

+
+ ), + selectorId: TOUR_STEP_IDS.MENU_MCP, + position: 'right', + }, + { + content: ( +
+

Billing

+

+ Manage your subscription and billing information. +

+
+ ), + selectorId: TOUR_STEP_IDS.MENU_BILLING, + position: 'right', + }, + { + content: ( +
+

+ View Toggle +

+

+ Switch between graph view and list view to see your memories in + different ways. +

+
+ ), + selectorId: TOUR_STEP_IDS.VIEW_TOGGLE, + position: 'left', + }, + { + content: ( +
+

Legend

+

+ Understand the different types of nodes and connections in your + memory graph. +

+
+ ), + selectorId: TOUR_STEP_IDS.LEGEND, + position: 'left', + }, + { + content: ( +
+

+ Chat Assistant +

+

+ Ask questions or add new memories using our AI-powered chat + interface. +

+
+ ), + selectorId: TOUR_STEP_IDS.FLOATING_CHAT, + position: 'left', + }, + ]; + }, []); - // Check if tour has been completed before - useEffect(() => { - const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true"; - if (!hasCompletedTour && !isTourCompleted) { - const timer = setTimeout(() => { - setShowTourDialog(true); - }, 1000); // Show after 1 second - return () => clearTimeout(timer); - } - }, [isTourCompleted]); + // Check if tour has been completed before + useEffect(() => { + const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === 'true'; + if (!hasCompletedTour && !isTourCompleted) { + const timer = setTimeout(() => { + setShowTourDialog(true); + }, 1000); // Show after 1 second + return () => clearTimeout(timer); + } + }, [isTourCompleted]); - // Set up tour steps - useEffect(() => { - setSteps(tourSteps); - }, [setSteps, tourSteps]); + // Set up tour steps + useEffect(() => { + setSteps(tourSteps); + }, [setSteps, tourSteps]); - // Save tour completion to localStorage - useEffect(() => { - if (isTourCompleted) { - localStorage.setItem(TOUR_STORAGE_KEY, "true"); - } - }, [isTourCompleted]); + // Save tour completion to localStorage + useEffect(() => { + if (isTourCompleted) { + localStorage.setItem(TOUR_STORAGE_KEY, 'true'); + } + }, [isTourCompleted]); - // Progressive loading via useInfiniteQuery - const IS_DEV = process.env.NODE_ENV === "development"; - const PAGE_SIZE = IS_DEV ? 3 : 100; - const MAX_TOTAL = 1000; + // Progressive loading via useInfiniteQuery + const IS_DEV = process.env.NODE_ENV === 'development'; + const PAGE_SIZE = IS_DEV ? 3 : 100; + const MAX_TOTAL = 1000; - const { - data, - error, - isPending, - isFetchingNextPage, - hasNextPage, - fetchNextPage, - } = useInfiniteQuery({ - queryKey: ["documents-with-memories", selectedProject], - initialPageParam: 1, - queryFn: async ({ pageParam }) => { - const response = await $fetch("@post/memories/documents", { - body: { - page: pageParam as number, - limit: (pageParam as number) === 1 ? (IS_DEV ? 3 : 500) : PAGE_SIZE, - sort: "createdAt", - order: "desc", - containerTags: selectedProject ? [selectedProject] : undefined, - }, - disableValidation: true, - }); + const { + data, + error, + isPending, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + } = useInfiniteQuery({ + queryKey: ['documents-with-memories', selectedProject], + initialPageParam: 1, + queryFn: async ({ pageParam }) => { + const response = await $fetch('@post/memories/documents', { + body: { + page: pageParam as number, + limit: (pageParam as number) === 1 ? (IS_DEV ? 3 : 500) : PAGE_SIZE, + sort: 'createdAt', + order: 'desc', + containerTags: selectedProject ? [selectedProject] : undefined, + }, + disableValidation: true, + }); - if (response.error) { - throw new Error(response.error?.message || "Failed to fetch documents"); - } + if (response.error) { + throw new Error(response.error?.message || 'Failed to fetch documents'); + } - return response.data; - }, - getNextPageParam: (lastPage, allPages) => { - const loaded = allPages.reduce( - (acc, p) => acc + (p.documents?.length ?? 0), - 0, - ); - if (loaded >= MAX_TOTAL) return undefined; + return response.data; + }, + getNextPageParam: (lastPage, allPages) => { + const loaded = allPages.reduce( + (acc, p) => acc + (p.documents?.length ?? 0), + 0 + ); + if (loaded >= MAX_TOTAL) return undefined; - const { currentPage, totalPages } = lastPage.pagination; - if (currentPage < totalPages) { - return currentPage + 1; - } - return undefined; - }, - staleTime: 5 * 60 * 1000, - }); + const { currentPage, totalPages } = lastPage.pagination; + if (currentPage < totalPages) { + return currentPage + 1; + } + return undefined; + }, + staleTime: 5 * 60 * 1000, + }); - const baseDocuments = useMemo(() => { - return ( - data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? [] - ); - }, [data]); + const baseDocuments = useMemo(() => { + return ( + data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? [] + ); + }, [data]); - const allDocuments = useMemo(() => { - if (injectedDocs.length === 0) return baseDocuments; - const byId = new Map(); - for (const d of injectedDocs) byId.set(d.id, d); - for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d); - return Array.from(byId.values()); - }, [baseDocuments, injectedDocs]); + const allDocuments = useMemo(() => { + if (injectedDocs.length === 0) return baseDocuments; + const byId = new Map(); + for (const d of injectedDocs) byId.set(d.id, d); + for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d); + return Array.from(byId.values()); + }, [baseDocuments, injectedDocs]); - const totalLoaded = allDocuments.length; - const hasMore = hasNextPage; - const isLoadingMore = isFetchingNextPage; + const totalLoaded = allDocuments.length; + const hasMore = hasNextPage; + const isLoadingMore = isFetchingNextPage; - const loadMoreDocuments = useCallback(async (): Promise => { - if (hasNextPage && !isFetchingNextPage) { - await fetchNextPage(); - return; - } - return; - }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + const loadMoreDocuments = useCallback(async (): Promise => { + if (hasNextPage && !isFetchingNextPage) { + await fetchNextPage(); + return; + } + return; + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); - // Reset injected docs when project changes - useEffect(() => { - setInjectedDocs([]); - }, [selectedProject]); + // Reset injected docs when project changes + useEffect(() => { + setInjectedDocs([]); + }, [selectedProject]); - // Surgical fetch of missing highlighted documents (customId-based IDs from search) - useEffect(() => { - if (!isOpen) return; - if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0) - return; - const present = new Set(); - for (const d of [...baseDocuments, ...injectedDocs]) { - if (d.id) present.add(d.id); - if ((d as any).customId) present.add((d as any).customId as string); - } - const missing = allHighlightDocumentIds.filter( - (id: string) => !present.has(id), - ); - if (missing.length === 0) return; - let cancelled = false; - const run = async () => { - try { - const resp = await $fetch("@post/memories/documents/by-ids", { - body: { - ids: missing, - by: "customId", - containerTags: selectedProject ? [selectedProject] : undefined, - }, - disableValidation: true, - }); - if (cancelled || (resp as any)?.error) return; - const extraDocs = (resp as any)?.data?.documents as - | DocumentWithMemories[] - | undefined; - if (!extraDocs || extraDocs.length === 0) return; - setInjectedDocs((prev) => { - const seen = new Set([ - ...prev.map((d) => d.id), - ...baseDocuments.map((d) => d.id), - ]); - const merged = [...prev]; - for (const doc of extraDocs) { - if (!seen.has(doc.id)) { - merged.push(doc); - seen.add(doc.id); - } - } - return merged; - }); - } catch {} - }; - void run(); - return () => { - cancelled = true; - }; - }, [ - isOpen, - allHighlightDocumentIds.join("|"), - baseDocuments, - injectedDocs, - selectedProject, - $fetch, - ]); + // Surgical fetch of missing highlighted documents (customId-based IDs from search) + useEffect(() => { + if (!isOpen) return; + if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0) + return; + const present = new Set(); + for (const d of [...baseDocuments, ...injectedDocs]) { + if (d.id) present.add(d.id); + if ((d as any).customId) present.add((d as any).customId as string); + } + const missing = allHighlightDocumentIds.filter( + (id: string) => !present.has(id) + ); + if (missing.length === 0) return; + let cancelled = false; + const run = async () => { + try { + const resp = await $fetch('@post/memories/documents/by-ids', { + body: { + ids: missing, + by: 'customId', + containerTags: selectedProject ? [selectedProject] : undefined, + }, + disableValidation: true, + }); + if (cancelled || (resp as any)?.error) return; + const extraDocs = (resp as any)?.data?.documents as + | DocumentWithMemories[] + | undefined; + if (!extraDocs || extraDocs.length === 0) return; + setInjectedDocs((prev) => { + const seen = new Set([ + ...prev.map((d) => d.id), + ...baseDocuments.map((d) => d.id), + ]); + const merged = [...prev]; + for (const doc of extraDocs) { + if (!seen.has(doc.id)) { + merged.push(doc); + seen.add(doc.id); + } + } + return merged; + }); + } catch {} + }; + void run(); + return () => { + cancelled = true; + }; + }, [ + isOpen, + allHighlightDocumentIds.join('|'), + baseDocuments, + injectedDocs, + selectedProject, + $fetch, + ]); - // Handle view mode change - const handleViewModeChange = useCallback( - (mode: "graph" | "list") => { - setViewMode(mode); - }, - [setViewMode], - ); + // Handle view mode change + const handleViewModeChange = useCallback( + (mode: 'graph' | 'list') => { + setViewMode(mode); + }, + [setViewMode] + ); - // Prevent body scrolling - useEffect(() => { - document.body.style.overflow = "hidden"; - document.body.style.height = "100vh"; - document.documentElement.style.overflow = "hidden"; - document.documentElement.style.height = "100vh"; + // Prevent body scrolling + useEffect(() => { + document.body.style.overflow = 'hidden'; + document.body.style.height = '100vh'; + document.documentElement.style.overflow = 'hidden'; + document.documentElement.style.height = '100vh'; - return () => { - document.body.style.overflow = ""; - document.body.style.height = ""; - document.documentElement.style.overflow = ""; - document.documentElement.style.height = ""; - }; - }, []); + return () => { + document.body.style.overflow = ''; + document.body.style.height = ''; + document.documentElement.style.overflow = ''; + document.documentElement.style.height = ''; + }; + }, []); - return ( + return (
{/* Main content area */} {
+ handleViewModeChange('list')} + transition={{ duration: 0.2 }} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + {viewMode === 'list' && ( + + )} + + + List + + + + + {/* Animated content switching */} + + {viewMode === 'graph' ? ( + + +
+
+
+

+ No Memories to Visualize +

+ +
+
+
+
+
+ ) : ( + + +
+
+
+

+ No Memories to Visualize +

+ +
+
+
+
+
+ )} +
{/* Animated content switching */} {viewMode === 'graph' ? ( @@ -560,6 +683,21 @@ const MemoryGraphPage = () => { )} + {/* Top Bar */} +
+
+ + + + {/* Top Bar */}
@@ -576,6 +714,9 @@ const MemoryGraphPage = () => { +
+ +
@@ -594,11 +735,29 @@ const MemoryGraphPage = () => {
+ + + +
+
+ +
+
{/* Floating Open Chat Button */} {!isOpen && !isMobile && ( @@ -613,7 +772,7 @@ const MemoryGraphPage = () => { }} >