diff options
Diffstat (limited to 'packages')
25 files changed, 272 insertions, 1140 deletions
diff --git a/packages/memory-graph-playground/.gitignore b/packages/memory-graph-playground/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/packages/memory-graph-playground/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/packages/memory-graph-playground/README.md b/packages/memory-graph-playground/README.md deleted file mode 100644 index 0480a389..00000000 --- a/packages/memory-graph-playground/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Memory Graph Playground - -A local development playground for testing the `@supermemory/memory-graph` package. - -## Getting Started - -1. Make sure the memory-graph package is built: - ```bash - cd ../memory-graph - bun run build - ``` - -2. Start the dev server: - ```bash - cd ../memory-graph-playground - bun run dev - ``` - -3. Open your browser to the URL shown (usually http://localhost:5173) - -4. Enter your Supermemory API key and click "Load Graph" - -## Features to Test - -- ✅ API key authentication -- ✅ Data fetching with React Query -- ✅ Graph rendering with PixiJS -- ✅ Interactive pan and zoom -- ✅ Node selection and details -- ✅ Space filtering -- ✅ Legend with statistics -- ✅ CSS auto-injection -- ✅ Error handling - -## Development - -When making changes to the memory-graph package: - -1. Make your changes in `packages/memory-graph/src` -2. Rebuild: `cd packages/memory-graph && bun run build` -3. The playground will hot-reload automatically - -## Notes - -- This playground uses the workspace version of `@supermemory/memory-graph` -- CSS is automatically injected from the package -- All dependencies are bundled except React/ReactDOM diff --git a/packages/memory-graph-playground/eslint.config.js b/packages/memory-graph-playground/eslint.config.js deleted file mode 100644 index b19330b1..00000000 --- a/packages/memory-graph-playground/eslint.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs['recommended-latest'], - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - }, -]) diff --git a/packages/memory-graph-playground/index.html b/packages/memory-graph-playground/index.html deleted file mode 100644 index 7542b7d5..00000000 --- a/packages/memory-graph-playground/index.html +++ /dev/null @@ -1,13 +0,0 @@ -<!doctype html> -<html lang="en"> - <head> - <meta charset="UTF-8" /> - <link rel="icon" type="image/svg+xml" href="/vite.svg" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>memory-graph-playground</title> - </head> - <body> - <div id="root"></div> - <script type="module" src="/src/main.tsx"></script> - </body> -</html> diff --git a/packages/memory-graph-playground/package.json b/packages/memory-graph-playground/package.json deleted file mode 100644 index 203b066d..00000000 --- a/packages/memory-graph-playground/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "memory-graph-playground", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "@supermemory/memory-graph": "workspace:*", - "react": "^19.1.1", - "react-dom": "^19.1.1" - }, - "devDependencies": { - "@eslint/js": "^9.36.0", - "@types/node": "^24.6.0", - "@types/react": "^19.1.16", - "@types/react-dom": "^19.1.9", - "@vitejs/plugin-react": "^5.0.4", - "eslint": "^9.36.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.22", - "globals": "^16.4.0", - "typescript": "~5.9.3", - "typescript-eslint": "^8.45.0", - "vite": "^7.1.7" - } -} diff --git a/packages/memory-graph-playground/public/vite.svg b/packages/memory-graph-playground/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/packages/memory-graph-playground/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file diff --git a/packages/memory-graph-playground/src/App.css b/packages/memory-graph-playground/src/App.css deleted file mode 100644 index 9e07ed68..00000000 --- a/packages/memory-graph-playground/src/App.css +++ /dev/null @@ -1,119 +0,0 @@ -.app { - width: 100vw; - height: 100vh; - display: flex; - flex-direction: column; - background: #0f1419; - color: #e2e8f0; -} - -.header { - padding: 1.5rem; - background: #1a1f29; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); -} - -.header h1 { - margin: 0; - font-size: 2rem; - color: #fff; -} - -.header p { - margin: 0.5rem 0 0 0; - color: #94a3b8; -} - -.controls { - padding: 1rem 1.5rem; - display: flex; - gap: 1rem; - background: #1a1f29; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); -} - -.api-key-input { - flex: 1; - padding: 0.75rem 1rem; - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 0.5rem; - background: rgba(255, 255, 255, 0.05); - color: #e2e8f0; - font-size: 1rem; -} - -.api-key-input::placeholder { - color: #64748b; -} - -.api-key-input:focus { - outline: none; - border-color: #60a5fa; -} - -.load-button { - padding: 0.75rem 2rem; - border: none; - border-radius: 0.5rem; - background: #3b82f6; - color: white; - font-size: 1rem; - font-weight: 600; - cursor: pointer; - transition: background 0.2s; -} - -.load-button:hover:not(:disabled) { - background: #2563eb; -} - -.load-button:disabled { - background: #334155; - cursor: not-allowed; - opacity: 0.5; -} - -.graph-container { - flex: 1; - position: relative; - overflow: hidden; - padding: 1rem; -} - -.instructions { - flex: 1; - padding: 3rem; - max-width: 800px; - margin: 0 auto; -} - -.instructions h2 { - color: #fff; - margin-bottom: 1.5rem; -} - -.instructions h3 { - color: #e2e8f0; - margin-top: 2rem; - margin-bottom: 1rem; -} - -.instructions ol, -.instructions ul { - text-align: left; - line-height: 2; -} - -.instructions li { - margin-bottom: 0.5rem; - color: #cbd5e1; -} - -.instructions a { - color: #60a5fa; - text-decoration: none; -} - -.instructions a:hover { - text-decoration: underline; -} diff --git a/packages/memory-graph-playground/src/App.tsx b/packages/memory-graph-playground/src/App.tsx deleted file mode 100644 index 3ab8c82c..00000000 --- a/packages/memory-graph-playground/src/App.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useState } from 'react' -import { MemoryGraph } from '@supermemory/memory-graph' -import './App.css' - -function App() { - const [apiKey, setApiKey] = useState('') - const [showGraph, setShowGraph] = useState(false) - - return ( - <div className="app"> - <div className="header"> - <h1>Memory Graph Playground</h1> - <p>Test the @supermemory/memory-graph package</p> - </div> - - <div className="controls"> - <input - type="password" - placeholder="Enter your Supermemory API key" - value={apiKey} - onChange={(e) => setApiKey(e.target.value)} - className="api-key-input" - /> - <button - onClick={() => setShowGraph(true)} - disabled={!apiKey} - className="load-button" - > - Load Graph - </button> - </div> - - {showGraph && apiKey && ( - <div className="graph-container"> - <MemoryGraph - apiKey={apiKey} - variant="console" - onError={(error) => { - console.error('Graph error:', error) - alert(`Error: ${error.message}`) - }} - onSuccess={(total) => { - console.log(`Loaded ${total} documents`) - }} - /> - </div> - )} - - {!showGraph && ( - <div className="instructions"> - <h2>Instructions</h2> - <ol> - <li>Get your API key from <a href="https://supermemory.ai" target="_blank" rel="noopener noreferrer">supermemory.ai</a></li> - <li>Enter your API key above</li> - <li>Click "Load Graph" to visualize your memories</li> - </ol> - <h3>Features to test:</h3> - <ul> - <li>Pan and zoom the graph</li> - <li>Click on nodes to see details</li> - <li>Drag nodes around</li> - <li>Use the space selector to filter by space</li> - <li>Check the legend for statistics</li> - </ul> - </div> - )} - </div> - ) -} - -export default App diff --git a/packages/memory-graph-playground/src/assets/react.svg b/packages/memory-graph-playground/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/packages/memory-graph-playground/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
\ No newline at end of file diff --git a/packages/memory-graph-playground/src/index.css b/packages/memory-graph-playground/src/index.css deleted file mode 100644 index b2876d8a..00000000 --- a/packages/memory-graph-playground/src/index.css +++ /dev/null @@ -1,31 +0,0 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -:root { - font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: dark; - color: rgba(255, 255, 255, 0.87); - background-color: #0f1419; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - margin: 0; - min-width: 320px; - min-height: 100vh; -} - -#root { - width: 100%; - height: 100vh; -} diff --git a/packages/memory-graph-playground/src/main.tsx b/packages/memory-graph-playground/src/main.tsx deleted file mode 100644 index cd61d6d1..00000000 --- a/packages/memory-graph-playground/src/main.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' - -createRoot(document.getElementById('root')!).render( - <StrictMode> - <App /> - {/*<div>hi</div>*/} - </StrictMode>, -) diff --git a/packages/memory-graph-playground/tsconfig.app.json b/packages/memory-graph-playground/tsconfig.app.json deleted file mode 100644 index a9b5a59c..00000000 --- a/packages/memory-graph-playground/tsconfig.app.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2022", - "useDefineForClassFields": true, - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "ESNext", - "types": ["vite/client"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["src"] -} diff --git a/packages/memory-graph-playground/tsconfig.json b/packages/memory-graph-playground/tsconfig.json deleted file mode 100644 index 1ffef600..00000000 --- a/packages/memory-graph-playground/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} diff --git a/packages/memory-graph-playground/tsconfig.node.json b/packages/memory-graph-playground/tsconfig.node.json deleted file mode 100644 index 8a67f62f..00000000 --- a/packages/memory-graph-playground/tsconfig.node.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2023", - "lib": ["ES2023"], - "module": "ESNext", - "types": ["node"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/packages/memory-graph-playground/vite.config.ts b/packages/memory-graph-playground/vite.config.ts deleted file mode 100644 index 8b0f57b9..00000000 --- a/packages/memory-graph-playground/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], -}) diff --git a/packages/memory-graph/README.md b/packages/memory-graph/README.md index 3dd6be60..049613bb 100644 --- a/packages/memory-graph/README.md +++ b/packages/memory-graph/README.md @@ -7,13 +7,13 @@ ## Features -- 🎨 **WebGL-powered rendering** - Smooth performance with hundreds of nodes using PixiJS -- 🔍 **Interactive exploration** - Pan, zoom, drag nodes, and explore connections -- 🧠 **Semantic connections** - Visualizes relationships based on content similarity -- 📱 **Responsive design** - Works seamlessly on mobile and desktop -- 🎯 **Zero configuration** - Works out of the box with automatic CSS injection -- 📦 **Lightweight** - Tree-shakeable and optimized bundle -- 🎭 **TypeScript** - Full TypeScript support with exported types +- **WebGL-powered rendering** - Smooth performance with hundreds of nodes +- **Interactive exploration** - Pan, zoom, drag nodes, and explore connections +- **Semantic connections** - Visualizes relationships based on content similarity +- **Responsive design** - Works seamlessly on mobile and desktop +- **Zero configuration** - Works out of the box with automatic CSS injection +- **Lightweight** - Tree-shakeable and optimized bundle +- **TypeScript** - Full TypeScript support with exported types ## Installation @@ -29,52 +29,75 @@ bun add @supermemory/memory-graph ## Quick Start +The component accepts document data directly - you fetch the data from your backend, which proxies requests to the Supermemory API with proper authentication. + ```tsx import { MemoryGraph } from '@supermemory/memory-graph' +import type { DocumentWithMemories } from '@supermemory/memory-graph' function App() { + const [documents, setDocuments] = useState<DocumentWithMemories[]>([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState<Error | null>(null) + + useEffect(() => { + // Fetch from YOUR backend (which proxies to Supermemory API) + fetch('/api/supermemory-graph') + .then(res => res.json()) + .then(data => { + setDocuments(data.documents) + setIsLoading(false) + }) + .catch(err => { + setError(err) + setIsLoading(false) + }) + }, []) + return ( <MemoryGraph - apiKey="your-api-key" - id="optional-document-id" + documents={documents} + isLoading={isLoading} + error={error} /> ) } ``` -That's it! The CSS is automatically injected, no manual imports needed. - -## Usage - -### Basic Usage - -```tsx -import { MemoryGraph } from '@supermemory/memory-graph' - -<MemoryGraph - apiKey="your-supermemory-api-key" - variant="console" -/> -``` - -### Advanced Usage - -```tsx -import { MemoryGraph } from '@supermemory/memory-graph' - -<MemoryGraph - apiKey="your-api-key" - id="document-123" - baseUrl="https://api.supermemory.ai" - variant="consumer" - showSpacesSelector={true} - onError={(error) => { - console.error('Failed to load graph:', error) - }} - onSuccess={(data) => { - console.log('Graph loaded:', data) - }} -/> +## Backend Proxy Example + +Create an API route in your backend that authenticates and proxies requests to Supermemory: + +### Next.js API Route + +```typescript +// app/api/supermemory-graph/route.ts +import { NextResponse } from 'next/server' + +export async function GET(request: Request) { + // Add your own auth check here + const user = await getAuthenticatedUser(request) + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const response = await fetch('https://api.supermemory.ai/v3/documents/documents', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.SUPERMEMORY_API_KEY}`, + }, + body: JSON.stringify({ + page: 1, + limit: 500, + sort: 'createdAt', + order: 'desc', + }), + }) + + const data = await response.json() + return NextResponse.json(data) +} ``` ## API Reference @@ -83,84 +106,108 @@ import { MemoryGraph } from '@supermemory/memory-graph' | Prop | Type | Default | Description | |------|------|---------|-------------| -| `apiKey` | `string` | **required** | Your Supermemory API key | -| `id` | `string` | `undefined` | Optional document ID to filter the graph | -| `baseUrl` | `string` | `"https://api.supermemory.ai"` | API base URL | -| `variant` | `"console" \| "consumer"` | `"console"` | Visual variant - console for full view, consumer for embedded | -| `showSpacesSelector` | `boolean` | `true` | Show/hide the spaces filter dropdown | -| `onError` | `(error: Error) => void` | `undefined` | Callback when data fetching fails | -| `onSuccess` | `(data: any) => void` | `undefined` | Callback when data is successfully loaded | - -## Framework Integration +| `documents` | `DocumentWithMemories[]` | **required** | Array of documents to display | +| `isLoading` | `boolean` | `false` | Whether data is currently loading | +| `error` | `Error \| null` | `null` | Error that occurred during fetching | +| `variant` | `"console" \| "consumer"` | `"console"` | Visual variant | +| `showSpacesSelector` | `boolean` | Based on variant | Show/hide the spaces filter | +| `children` | `ReactNode` | - | Content to show when no documents | +| `highlightDocumentIds` | `string[]` | `[]` | Document IDs to highlight | +| `highlightsVisible` | `boolean` | `true` | Whether highlights are visible | -### Next.js +### Pagination Props (Optional) -```tsx -// app/graph/page.tsx -'use client' - -import { MemoryGraph } from '@supermemory/memory-graph' +For large datasets, you can implement pagination: -export default function GraphPage() { - return ( - <div className="w-full h-screen"> - <MemoryGraph apiKey={process.env.NEXT_PUBLIC_SUPERMEMORY_API_KEY!} /> - </div> - ) -} +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `isLoadingMore` | `boolean` | `false` | Whether loading more data | +| `hasMore` | `boolean` | `false` | Whether more data is available | +| `totalLoaded` | `number` | `documents.length` | Total documents loaded | +| `loadMoreDocuments` | `() => Promise<void>` | - | Callback to load more | + +### Types + +```typescript +import type { + DocumentWithMemories, + MemoryEntry, + DocumentsResponse, + MemoryGraphProps +} from '@supermemory/memory-graph' ``` -### Vite/React +## Advanced Usage + +### With Pagination ```tsx -// src/App.tsx import { MemoryGraph } from '@supermemory/memory-graph' -function App() { +function GraphWithPagination() { + const [documents, setDocuments] = useState([]) + const [page, setPage] = useState(1) + const [hasMore, setHasMore] = useState(true) + const [isLoadingMore, setIsLoadingMore] = useState(false) + + const loadMore = async () => { + setIsLoadingMore(true) + const res = await fetch(`/api/supermemory-graph?page=${page + 1}`) + const data = await res.json() + setDocuments(prev => [...prev, ...data.documents]) + setHasMore(data.pagination.currentPage < data.pagination.totalPages) + setPage(p => p + 1) + setIsLoadingMore(false) + } + return ( - <div style={{ width: '100vw', height: '100vh' }}> - <MemoryGraph apiKey={import.meta.env.VITE_SUPERMEMORY_API_KEY} /> - </div> + <MemoryGraph + documents={documents} + isLoading={isLoading} + isLoadingMore={isLoadingMore} + hasMore={hasMore} + loadMoreDocuments={loadMore} + totalLoaded={documents.length} + /> ) } ``` -### Create React App +### Custom Empty State ```tsx -// src/App.tsx -import { MemoryGraph } from '@supermemory/memory-graph' - -function App() { - return ( - <div style={{ width: '100vw', height: '100vh' }}> - <MemoryGraph apiKey={process.env.REACT_APP_SUPERMEMORY_API_KEY} /> - </div> - ) -} +<MemoryGraph documents={[]} isLoading={false}> + <div className="empty-state"> + <h2>No memories yet</h2> + <p>Start adding content to see your knowledge graph</p> + </div> +</MemoryGraph> ``` -## Getting an API Key - -1. Visit [supermemory.ai](https://supermemory.ai) -2. Sign up or log in to your account -3. Navigate to Settings > API Keys -4. Generate a new API key -5. Copy and use it in your application - -⚠️ **Security Note**: Never commit API keys to version control. Use environment variables. - -## Features in Detail +### Variants -### WebGL Rendering +The `variant` prop controls the visual layout and initial viewport settings: -The graph uses PixiJS for hardware-accelerated WebGL rendering, enabling smooth interaction with hundreds of nodes and connections. +| Variant | Initial Zoom | Spaces Selector | Legend Position | Use Case | +|---------|-------------|-----------------|-----------------|----------| +| `console` | 0.8 | Shown | Bottom-right | Full-page dashboard views | +| `consumer` | 0.5 | Hidden | Top-right | Embedded/widget views | -### Semantic Similarity +```tsx +// Full dashboard view +<MemoryGraph + documents={documents} + variant="console" +/> -Connections between memories are visualized based on semantic similarity, with stronger connections appearing more prominent. +// Embedded widget +<MemoryGraph + documents={documents} + variant="consumer" +/> +``` -### Interactive Controls +## Interactive Controls - **Pan**: Click and drag the background - **Zoom**: Mouse wheel or pinch on mobile @@ -168,10 +215,6 @@ Connections between memories are visualized based on semantic similarity, with s - **Drag Nodes**: Click and drag individual nodes - **Fit to View**: Auto-fit button to center all content -### Touch Support - -Full support for touch gestures including pinch-to-zoom and touch-drag for mobile devices. - ## Browser Support - Chrome/Edge (latest) @@ -202,23 +245,8 @@ bun run check-types ## License -MIT © [Supermemory](https://supermemory.ai) +MIT ## Support -- 📧 Email: [email protected] -- 🐛 Issues: [GitHub Issues](https://github.com/supermemoryai/supermemory/issues) -- 💬 Discord: [Join our community](https://discord.gg/supermemory) - -## Roadmap - -- [ ] Custom theme support -- [ ] Export graph as image -- [ ] Advanced filtering options -- [ ] Graph animation presets -- [ ] Accessibility improvements -- [ ] Collaboration features - ---- - -Made with ❤️ by the Supermemory team +- Issues: [GitHub Issues](https://github.com/supermemoryai/supermemory/issues) diff --git a/packages/memory-graph/package.json b/packages/memory-graph/package.json index 6ffd7317..198810cd 100644 --- a/packages/memory-graph/package.json +++ b/packages/memory-graph/package.json @@ -1,6 +1,6 @@ { "name": "@supermemory/memory-graph", - "version": "0.1.0", + "version": "0.1.1", "description": "Interactive graph visualization component for Supermemory - visualize and explore your memory connections", "type": "module", "main": "./dist/memory-graph.cjs", @@ -62,7 +62,6 @@ "@emotion/is-prop-valid": "^1.4.0", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-slot": "^1.2.4", - "@tanstack/react-query": "^5.90.7", "@vanilla-extract/css": "^1.17.4", "@vanilla-extract/recipes": "^0.5.7", "@vanilla-extract/sprinkles": "^1.6.5", @@ -75,7 +74,6 @@ "@vanilla-extract/vite-plugin": "^5.1.1", "@vitejs/plugin-react": "^5.1.0", "typescript": "^5.9.3", - "vite": "^7.2.1", - "vite-plugin-lib-inject-css": "^2.2.2" + "vite": "^7.2.1" } } diff --git a/packages/memory-graph/src/components/memory-graph-wrapper.tsx b/packages/memory-graph/src/components/memory-graph-wrapper.tsx deleted file mode 100644 index cfc8e148..00000000 --- a/packages/memory-graph/src/components/memory-graph-wrapper.tsx +++ /dev/null @@ -1,198 +0,0 @@ -"use client"; - -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { useEffect, useMemo, useRef } from "react"; -import { - flattenDocuments, - getLoadedCount, - getTotalDocuments, - useInfiniteDocumentsQuery, -} from "@/hooks/use-documents-query"; -import { MemoryGraph } from "./memory-graph"; -import { defaultTheme } from "@/styles/theme.css"; -import type { ApiClientError } from "@/lib/api-client"; - -export interface MemoryGraphWrapperProps { - /** API key for authentication */ - apiKey: string; - /** Optional base URL for the API (defaults to https://api.supermemory.ai) */ - baseUrl?: string; - /** Optional document ID to filter by */ - id?: string; - /** Visual variant - console for full view, consumer for embedded */ - variant?: "console" | "consumer"; - /** Show/hide the spaces filter dropdown */ - showSpacesSelector?: boolean; - /** Optional container tags to filter documents */ - containerTags?: string[]; - /** Callback when data fetching fails */ - onError?: (error: ApiClientError) => void; - /** Callback when data is successfully loaded */ - onSuccess?: (totalDocuments: number) => void; - /** Empty state content */ - children?: React.ReactNode; - /** Documents to highlight */ - highlightDocumentIds?: string[]; - /** Whether highlights are visible */ - highlightsVisible?: boolean; - /** Pixels occluded on the right side of the viewport */ - occludedRightPx?: number; -} - -/** - * Internal component that uses the query hooks - */ -function MemoryGraphWithQuery(props: MemoryGraphWrapperProps) { - const { - apiKey, - baseUrl, - containerTags, - variant = "console", - showSpacesSelector, - onError, - onSuccess, - children, - highlightDocumentIds, - highlightsVisible, - occludedRightPx, - } = props; - - // Derive showSpacesSelector from variant if not explicitly provided - // console variant shows spaces selector, consumer variant hides it - const finalShowSpacesSelector = showSpacesSelector ?? (variant === "console"); - - // Use infinite query for automatic pagination - const { - data, - isLoading, - isFetchingNextPage, - hasNextPage, - fetchNextPage, - error, - } = useInfiniteDocumentsQuery({ - apiKey, - baseUrl, - containerTags, - enabled: !!apiKey, - }); - - // Flatten documents from all pages - const documents = useMemo(() => flattenDocuments(data), [data]); - const totalLoaded = useMemo(() => getLoadedCount(data), [data]); - const totalDocuments = useMemo(() => getTotalDocuments(data), [data]); - - // Eagerly load all pages to ensure complete graph data - const isLoadingAllPages = useRef(false); - - useEffect(() => { - // Only start loading once, when initial data is loaded - if (isLoading || isLoadingAllPages.current || !data?.pages?.[0]) return; - - const abortController = new AbortController(); - - // Start recursive page loading - const loadAllPages = async () => { - isLoadingAllPages.current = true; - - try { - // Keep fetching until no more pages or aborted - let shouldContinue = hasNextPage; - - while (shouldContinue && !abortController.signal.aborted) { - const result = await fetchNextPage(); - shouldContinue = result.hasNextPage ?? false; - - // Throttle requests to avoid overwhelming server (50ms delay like console app) - if (shouldContinue && !abortController.signal.aborted) { - await new Promise(resolve => setTimeout(resolve, 50)); - } - } - } catch (error) { - if (!abortController.signal.aborted) { - console.error('[MemoryGraph] Error loading pages:', error); - } - } - }; - - if (hasNextPage) { - loadAllPages(); - } - - // Cleanup on unmount - return () => { - abortController.abort(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); // Only run once on mount - - // Call callbacks - if (error && onError) { - onError(error as ApiClientError); - } - - if (data && onSuccess && totalDocuments > 0) { - onSuccess(totalDocuments); - } - - // Load more function - const loadMoreDocuments = async () => { - if (hasNextPage && !isFetchingNextPage) { - await fetchNextPage(); - } - }; - - return ( - <MemoryGraph - documents={documents} - isLoading={isLoading} - isLoadingMore={isFetchingNextPage} - error={error as Error | null} - totalLoaded={totalLoaded} - hasMore={hasNextPage ?? false} - loadMoreDocuments={loadMoreDocuments} - variant={variant} - showSpacesSelector={finalShowSpacesSelector} - highlightDocumentIds={highlightDocumentIds} - highlightsVisible={highlightsVisible} - occludedRightPx={occludedRightPx} - autoLoadOnViewport={true} - themeClassName={defaultTheme} - > - {children} - </MemoryGraph> - ); -} - -// Create a default query client for the wrapper -const defaultQueryClient = new QueryClient({ - defaultOptions: { - queries: { - refetchOnWindowFocus: false, - refetchOnMount: false, - retry: 2, - }, - }, -}); - -/** - * MemoryGraph component with built-in data fetching - * - * This component handles all data fetching internally using the provided API key. - * Simply pass your API key and it will fetch and render the graph automatically. - * - * @example - * ```tsx - * <MemoryGraphWrapper - * apiKey="your-api-key" - * variant="console" - * onError={(error) => console.error(error)} - * /> - * ``` - */ -export function MemoryGraphWrapper(props: MemoryGraphWrapperProps) { - return ( - <QueryClientProvider client={defaultQueryClient}> - <MemoryGraphWithQuery {...props} /> - </QueryClientProvider> - ); -} diff --git a/packages/memory-graph/src/components/memory-graph.tsx b/packages/memory-graph/src/components/memory-graph.tsx index 3eeed37b..21d4a08f 100644 --- a/packages/memory-graph/src/components/memory-graph.tsx +++ b/packages/memory-graph/src/components/memory-graph.tsx @@ -6,23 +6,25 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { GraphCanvas } from "./graph-canvas"; import { useGraphData } from "@/hooks/use-graph-data"; import { useGraphInteractions } from "@/hooks/use-graph-interactions"; +import { injectStyles } from "@/lib/inject-styles"; import { Legend } from "./legend"; import { LoadingIndicator } from "./loading-indicator"; import { NavigationControls } from "./navigation-controls"; import { NodeDetailPanel } from "./node-detail-panel"; import { SpacesDropdown } from "./spaces-dropdown"; import * as styles from "./memory-graph.css"; +import { defaultTheme } from "@/styles/theme.css"; import type { MemoryGraphProps } from "@/types"; export const MemoryGraph = ({ children, documents, - isLoading, - isLoadingMore, - error, + isLoading = false, + isLoadingMore = false, + error = null, totalLoaded, - hasMore, + hasMore = false, loadMoreDocuments, showSpacesSelector, variant = "console", @@ -33,6 +35,15 @@ export const MemoryGraph = ({ autoLoadOnViewport = true, themeClassName, }: MemoryGraphProps) => { + // Inject styles on first render (client-side only) + useEffect(() => { + injectStyles(); + }, []); + + // Derive totalLoaded from documents if not provided + const effectiveTotalLoaded = totalLoaded ?? documents.length; + // No-op for loadMoreDocuments if not provided + const effectiveLoadMoreDocuments = loadMoreDocuments ?? (async () => {}); // Derive showSpacesSelector from variant if not explicitly provided // console variant shows spaces selector, consumer variant hides it const finalShowSpacesSelector = showSpacesSelector ?? (variant === "console"); @@ -293,7 +304,7 @@ export const MemoryGraph = ({ // If 80% or more of documents are visible, load more const visibilityRatio = visibleDocuments.length / data.documents.length; if (visibilityRatio >= 0.8) { - loadMoreDocuments(); + effectiveLoadMoreDocuments(); } }, [ isLoadingMore, @@ -305,7 +316,7 @@ export const MemoryGraph = ({ containerSize.width, containerSize.height, nodes, - loadMoreDocuments, + effectiveLoadMoreDocuments, ]); // Throttled version to avoid excessive checks @@ -352,7 +363,7 @@ export const MemoryGraph = ({ } return ( - <div className={themeClassName ? `${themeClassName} ${styles.mainContainer}` : styles.mainContainer}> + <div className={`${themeClassName ?? defaultTheme} ${styles.mainContainer}`}> {/* Spaces selector - only shown for console */} {finalShowSpacesSelector && availableSpaces.length > 0 && ( <div className={styles.spacesSelectorContainer}> @@ -369,7 +380,7 @@ export const MemoryGraph = ({ <LoadingIndicator isLoading={isLoading} isLoadingMore={isLoadingMore} - totalLoaded={totalLoaded} + totalLoaded={effectiveTotalLoaded} variant={variant} /> diff --git a/packages/memory-graph/src/hooks/use-documents-query.ts b/packages/memory-graph/src/hooks/use-documents-query.ts deleted file mode 100644 index eb9ab892..00000000 --- a/packages/memory-graph/src/hooks/use-documents-query.ts +++ /dev/null @@ -1,113 +0,0 @@ -"use client"; - -import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; -import { fetchDocuments, type FetchDocumentsOptions } from "@/lib/api-client"; -import type { DocumentsResponse } from "@/api-types"; - -export interface UseDocumentsQueryOptions { - apiKey: string; - baseUrl?: string; - id?: string; // Optional document ID to filter by - containerTags?: string[]; - limit?: number; - sort?: "createdAt" | "updatedAt"; - order?: "asc" | "desc"; - enabled?: boolean; // Whether to enable the query -} - -/** - * Hook for fetching a single page of documents - * Useful when you don't need pagination - */ -export function useDocumentsQuery(options: UseDocumentsQueryOptions) { - const { - apiKey, - baseUrl, - containerTags, - limit = 50, - sort = "createdAt", - order = "desc", - enabled = true, - } = options; - - return useQuery({ - queryKey: ["documents", { apiKey, baseUrl, containerTags, limit, sort, order }], - queryFn: async () => { - return fetchDocuments({ - apiKey, - baseUrl, - page: 1, - limit, - sort, - order, - containerTags, - }); - }, - enabled: enabled && !!apiKey, - staleTime: 1000 * 60 * 5, // 5 minutes - retry: 2, - }); -} - -/** - * Hook for fetching documents with infinite scroll/pagination support - * Automatically handles loading more pages - */ -export function useInfiniteDocumentsQuery(options: UseDocumentsQueryOptions) { - const { - apiKey, - baseUrl, - containerTags, - limit = 500, - sort = "createdAt", - order = "desc", - enabled = true, - } = options; - - return useInfiniteQuery({ - queryKey: ["documents", "infinite", { apiKey, baseUrl, containerTags, limit, sort, order }], - queryFn: async ({ pageParam = 1 }) => { - return fetchDocuments({ - apiKey, - baseUrl, - page: pageParam, - limit, - sort, - order, - containerTags, - }); - }, - initialPageParam: 1, - getNextPageParam: (lastPage: DocumentsResponse) => { - const { currentPage, totalPages } = lastPage.pagination; - return currentPage < totalPages ? currentPage + 1 : undefined; - }, - enabled: enabled && !!apiKey, - staleTime: 1000 * 60 * 5, // 5 minutes - retry: 2, - }); -} - -/** - * Helper to flatten infinite query results into a single documents array - */ -export function flattenDocuments(data: { pages: DocumentsResponse[] } | undefined) { - if (!data?.pages) return []; - return data.pages.flatMap((page) => page.documents); -} - -/** - * Helper to get total documents count from infinite query - */ -export function getTotalDocuments(data: { pages: DocumentsResponse[] } | undefined) { - if (!data?.pages?.[0]) return 0; - return data.pages[0].pagination.totalItems; -} - -/** - * Helper to get current loaded count from infinite query - */ -export function getLoadedCount(data: { pages: DocumentsResponse[] } | undefined) { - if (!data?.pages) return 0; - return data.pages.reduce((sum, page) => sum + page.documents.length, 0); -} diff --git a/packages/memory-graph/src/index.tsx b/packages/memory-graph/src/index.tsx index 1e413e00..6e5c882f 100644 --- a/packages/memory-graph/src/index.tsx +++ b/packages/memory-graph/src/index.tsx @@ -1,13 +1,11 @@ -// Auto-inject global styles (side effect import) -import "./styles"; - // Export the main component -export { MemoryGraphWrapper as MemoryGraph } from "./components/memory-graph-wrapper"; +export { MemoryGraph } from "./components/memory-graph"; + +// Export style injector for manual use if needed +export { injectStyles } from "./lib/inject-styles"; // Export types for consumers -export type { - MemoryGraphWrapperProps as MemoryGraphProps, -} from "./components/memory-graph-wrapper"; +export type { MemoryGraphProps } from "./types"; export type { DocumentWithMemories, @@ -21,25 +19,6 @@ export type { MemoryRelation, } from "./types"; -// Export API client for advanced usage -export { - fetchDocuments, - fetchDocumentsPage, - validateApiKey, - type FetchDocumentsOptions, - type ApiClientError, -} from "./lib/api-client"; - -// Export hooks for advanced usage (if users want to bring their own QueryClient) -export { - useDocumentsQuery, - useInfiniteDocumentsQuery, - flattenDocuments, - getTotalDocuments, - getLoadedCount, - type UseDocumentsQueryOptions, -} from "./hooks/use-documents-query"; - // Export theme system for custom theming export { themeContract, defaultTheme } from "./styles/theme.css"; export { sprinkles } from "./styles/sprinkles.css"; diff --git a/packages/memory-graph/src/lib/api-client.ts b/packages/memory-graph/src/lib/api-client.ts deleted file mode 100644 index faef4d06..00000000 --- a/packages/memory-graph/src/lib/api-client.ts +++ /dev/null @@ -1,213 +0,0 @@ -import type { DocumentsResponse } from "@/api-types"; - -export interface FetchDocumentsOptions { - apiKey: string; - baseUrl?: string; - page?: number; - limit?: number; - sort?: "createdAt" | "updatedAt"; - order?: "asc" | "desc"; - containerTags?: string[]; - signal?: AbortSignal; -} - -export interface ApiClientError extends Error { - status?: number; - statusText?: string; - response?: unknown; -} - -/** - * Creates an API client error with additional context - */ -function createApiError( - message: string, - status?: number, - statusText?: string, - response?: unknown, -): ApiClientError { - const error = new Error(message) as ApiClientError; - error.name = "ApiClientError"; - error.status = status; - error.statusText = statusText; - error.response = response; - return error; -} - -/** - * Fetches documents with their memory entries from the Supermemory API - * - * @param options - Configuration options for the API request - * @returns Promise resolving to the documents response - * @throws ApiClientError if the request fails - */ -export async function fetchDocuments( - options: FetchDocumentsOptions, -): Promise<DocumentsResponse> { - const { - apiKey, - baseUrl = "https://api.supermemory.ai", - page = 1, - limit = 50, - sort = "createdAt", - order = "desc", - containerTags, - signal, - } = options; - - // Validate required parameters - if (!apiKey) { - throw createApiError("API key is required"); - } - - // Construct the full URL - const url = `${baseUrl}/v3/documents/documents`; - - // Build request body - const body: { - page: number; - limit: number; - sort: string; - order: string; - containerTags?: string[]; - } = { - page, - limit, - sort, - order, - }; - - if (containerTags && containerTags.length > 0) { - body.containerTags = containerTags; - } - - try { - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - body: JSON.stringify(body), - signal, - }); - - // Handle non-OK responses - if (!response.ok) { - let errorMessage = `Failed to fetch documents: ${response.status} ${response.statusText}`; - let errorResponse: unknown; - - try { - errorResponse = await response.json(); - if ( - errorResponse && - typeof errorResponse === "object" && - "message" in errorResponse - ) { - errorMessage = `API Error: ${(errorResponse as { message: string }).message}`; - } - } catch { - // If response is not JSON, use default error message - } - - throw createApiError( - errorMessage, - response.status, - response.statusText, - errorResponse, - ); - } - - // Parse and validate response - const data = await response.json(); - - // Basic validation of response structure - if (!data || typeof data !== "object") { - throw createApiError("Invalid response format: expected an object"); - } - - if (!("documents" in data) || !Array.isArray(data.documents)) { - throw createApiError( - "Invalid response format: missing documents array", - ); - } - - if (!("pagination" in data) || typeof data.pagination !== "object") { - throw createApiError( - "Invalid response format: missing pagination object", - ); - } - - return data as DocumentsResponse; - } catch (error) { - // Re-throw ApiClientError as-is - if ((error as ApiClientError).name === "ApiClientError") { - throw error; - } - - // Handle network errors - if (error instanceof TypeError && error.message.includes("fetch")) { - throw createApiError( - `Network error: Unable to connect to ${baseUrl}. Please check your internet connection.`, - ); - } - - // Handle abort errors - if (error instanceof Error && error.name === "AbortError") { - throw createApiError("Request was aborted"); - } - - // Handle other errors - throw createApiError( - `Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } -} - -/** - * Fetches a single page of documents (convenience wrapper) - */ -export async function fetchDocumentsPage( - apiKey: string, - page: number, - baseUrl?: string, - signal?: AbortSignal, -): Promise<DocumentsResponse> { - return fetchDocuments({ - apiKey, - baseUrl, - page, - limit: 50, - signal, - }); -} - -/** - * Validates an API key by making a test request - * - * @param apiKey - The API key to validate - * @param baseUrl - Optional base URL for the API - * @returns Promise resolving to true if valid, false otherwise - */ -export async function validateApiKey( - apiKey: string, - baseUrl?: string, -): Promise<boolean> { - try { - await fetchDocuments({ - apiKey, - baseUrl, - page: 1, - limit: 1, - }); - return true; - } catch (error) { - // Check if it's an authentication error - if ((error as ApiClientError).status === 401) { - return false; - } - // Other errors might indicate valid key but other issues - // We'll return true in those cases to not block the user - return true; - } -} diff --git a/packages/memory-graph/src/lib/inject-styles.ts b/packages/memory-graph/src/lib/inject-styles.ts new file mode 100644 index 00000000..1a6bf4eb --- /dev/null +++ b/packages/memory-graph/src/lib/inject-styles.ts @@ -0,0 +1,36 @@ +/** + * Runtime CSS injection for universal bundler support + * The CSS content is injected by the build plugin + */ + +// This will be replaced by the build plugin with the actual CSS content +declare const __MEMORY_GRAPH_CSS__: string; + +// Track injection state +let injected = false; + +/** + * Inject memory-graph styles into the document head. + * Safe to call multiple times - will only inject once. + */ +export function injectStyles(): void { + // Only run in browser + if (typeof document === "undefined") return; + + // Only inject once + if (injected) return; + + // Check if already injected (e.g., by another instance) + if (document.querySelector('style[data-memory-graph]')) { + injected = true; + return; + } + + injected = true; + + // Create and inject style element + const style = document.createElement("style"); + style.setAttribute("data-memory-graph", ""); + style.textContent = __MEMORY_GRAPH_CSS__; + document.head.appendChild(style); +} diff --git a/packages/memory-graph/src/types.ts b/packages/memory-graph/src/types.ts index 796c8d01..0a80df22 100644 --- a/packages/memory-graph/src/types.ts +++ b/packages/memory-graph/src/types.ts @@ -73,27 +73,37 @@ export interface GraphCanvasProps { } export interface MemoryGraphProps { - children?: React.ReactNode; + /** The documents to display in the graph */ documents: DocumentWithMemories[]; - isLoading: boolean; - isLoadingMore: boolean; - error: Error | null; - totalLoaded: number; - hasMore: boolean; - loadMoreDocuments: () => Promise<void>; - // App-specific props - showSpacesSelector?: boolean; // true for console, false for consumer - variant?: "console" | "consumer"; // for different positioning and styling - legendId?: string; // Optional ID for the legend component - // Optional document highlight list (document custom IDs) + /** Whether the initial data is loading */ + isLoading?: boolean; + /** Error that occurred during data fetching */ + error?: Error | null; + /** Optional children to render when no documents exist */ + children?: React.ReactNode; + /** Whether more data is being loaded (for pagination) */ + isLoadingMore?: boolean; + /** Total number of documents loaded */ + totalLoaded?: number; + /** Whether there are more documents to load */ + hasMore?: boolean; + /** Callback to load more documents (for pagination) */ + loadMoreDocuments?: () => Promise<void>; + /** Show/hide the spaces filter dropdown */ + showSpacesSelector?: boolean; + /** Visual variant - "console" for full view, "consumer" for embedded */ + variant?: "console" | "consumer"; + /** Optional ID for the legend component */ + legendId?: string; + /** Document IDs to highlight in the graph */ highlightDocumentIds?: string[]; - // Whether highlights are currently visible (e.g., chat open) + /** Whether highlights are currently visible */ highlightsVisible?: boolean; - // Pixels occluded on the right side of the viewport (e.g., chat panel) + /** Pixels occluded on the right side of the viewport */ occludedRightPx?: number; - // Whether to auto-load more documents based on viewport visibility + /** Whether to auto-load more documents based on viewport visibility */ autoLoadOnViewport?: boolean; - // Theme class name to apply + /** Theme class name to apply */ themeClassName?: string; } diff --git a/packages/memory-graph/vite.config.ts b/packages/memory-graph/vite.config.ts index 3098067f..f6055602 100644 --- a/packages/memory-graph/vite.config.ts +++ b/packages/memory-graph/vite.config.ts @@ -1,12 +1,46 @@ -import { defineConfig } from 'vite' +import { defineConfig, type Plugin } from 'vite' import react from '@vitejs/plugin-react' import { resolve } from 'path' +import { readFileSync } from 'fs' import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; -import { libInjectCss } from 'vite-plugin-lib-inject-css'; + +/** + * Custom plugin to embed CSS content into the JS bundle for runtime injection. + * This allows the package to work with any bundler (Vite, webpack, Next.js, etc.) + */ +function injectCssPlugin(): Plugin { + let cssContent = ''; + + return { + name: 'inject-css-content', + enforce: 'post', + generateBundle(_, bundle) { + // Find the generated CSS file + for (const [fileName, chunk] of Object.entries(bundle)) { + if (fileName.endsWith('.css') && chunk.type === 'asset') { + cssContent = chunk.source as string; + break; + } + } + + // Replace placeholder in JS files with actual CSS content + for (const [fileName, chunk] of Object.entries(bundle)) { + if ((fileName.endsWith('.js') || fileName.endsWith('.cjs')) && chunk.type === 'chunk') { + // Escape the CSS for embedding in JS string + const escapedCss = JSON.stringify(cssContent); + chunk.code = chunk.code.replace( + /__MEMORY_GRAPH_CSS__/g, + escapedCss + ); + } + } + } + }; +} // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react(), vanillaExtractPlugin(), libInjectCss()], + plugins: [react(), vanillaExtractPlugin(), injectCssPlugin()], build: { lib: { entry: resolve(__dirname, 'src/index.tsx'), @@ -28,7 +62,7 @@ export default defineConfig({ 'react-dom': 'ReactDOM', 'react/jsx-runtime': 'react/jsx-runtime' }, - // Preserve CSS as separate file + // Preserve CSS as separate file (for manual import fallback) assetFileNames: (assetInfo) => { // Vanilla-extract generates index.css, rename to memory-graph.css if (assetInfo.name === 'index.css' || assetInfo.name === 'style.css') { |