diff options
Diffstat (limited to 'packages/memory-graph')
| -rw-r--r-- | packages/memory-graph/README.md | 264 | ||||
| -rw-r--r-- | packages/memory-graph/package.json | 6 | ||||
| -rw-r--r-- | packages/memory-graph/src/components/memory-graph-wrapper.tsx | 198 | ||||
| -rw-r--r-- | packages/memory-graph/src/components/memory-graph.tsx | 27 | ||||
| -rw-r--r-- | packages/memory-graph/src/hooks/use-documents-query.ts | 113 | ||||
| -rw-r--r-- | packages/memory-graph/src/index.tsx | 31 | ||||
| -rw-r--r-- | packages/memory-graph/src/lib/api-client.ts | 213 | ||||
| -rw-r--r-- | packages/memory-graph/src/lib/inject-styles.ts | 36 | ||||
| -rw-r--r-- | packages/memory-graph/src/types.ts | 42 | ||||
| -rw-r--r-- | packages/memory-graph/vite.config.ts | 42 |
10 files changed, 272 insertions, 700 deletions
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') { |