UNPKG

@jeanmemory/react

Version:

React SDK for Jean Memory - Build personalized AI chatbots in 5 lines of code

333 lines (332 loc) โ€ข 13.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JeanProvider = JeanProvider; exports.useJean = useJean; const jsx_runtime_1 = require("react/jsx-runtime"); /** * Jean Memory React SDK - Provider Component * Manages authentication state and API client */ const react_1 = require("react"); const config_1 = require("./config"); const mcp_1 = require("./mcp"); const oauth_1 = require("./oauth"); const JeanContext = (0, react_1.createContext)(null); function JeanProvider({ apiKey, children }) { const [user, setUser] = (0, react_1.useState)(null); const [messages, setMessages] = (0, react_1.useState)([]); const [isLoading, setIsLoading] = (0, react_1.useState)(false); const [rawError, setRawError] = (0, react_1.useState)(null); const [isInitialized, setIsInitialized] = (0, react_1.useState)(false); const oAuthHandled = (0, react_1.useRef)(false); // Initialize OAuth and check for existing session (0, react_1.useEffect)(() => { const initAuth = async () => { setIsLoading(true); try { // Check for OAuth callback parameters - handle only once per page load const params = new URLSearchParams(window.location.search); if (params.has('code') && params.has('state') && !oAuthHandled.current) { // Mark as handled immediately to prevent re-entry oAuthHandled.current = true; console.log('๐Ÿ”„ Jean OAuth: Processing callback in Provider...'); const callbackUser = await (0, oauth_1.handleOAuthCallback)(); if (callbackUser) { setUser(callbackUser); console.log('โœ… OAuth authentication completed'); // Clean up URL parameters while preserving other query params const url = new URL(window.location.href); url.searchParams.delete('code'); url.searchParams.delete('state'); url.searchParams.delete('error'); url.searchParams.delete('error_description'); window.history.replaceState({}, document.title, url.toString()); setIsInitialized(true); setIsLoading(false); return; } } // Check for existing session const existingSession = (0, oauth_1.getUserSession)(); if (existingSession) { setUser(existingSession); console.log('โœ… Restored existing session'); } setIsInitialized(true); } catch (error) { console.error('Auth initialization error:', error); setRawError(error instanceof Error ? error.message : 'Authentication failed'); oAuthHandled.current = false; // Reset on error for potential retry } finally { setIsLoading(false); } }; initAuth(); }, []); // Handle OAuth completion and API key validation (0, react_1.useEffect)(() => { if (!apiKey) { setRawError('API key is required'); return; } if (!apiKey.startsWith('jean_sk_')) { setRawError('Invalid API key format'); return; } console.log('โœ… Jean Memory SDK initialized'); // Check if this is a test API key and auto-initialize test user if (apiKey.includes('test')) { console.log('๐Ÿงช Test API key detected - initializing test user mode'); initializeTestUser(); return; } // Validate API key on mount if (!apiKey || !apiKey.startsWith('jean_sk_')) { setRawError('Invalid or missing API key'); } }, [apiKey]); const sendMessage = async (message, options = {}) => { if (!user) { throw new Error('User not authenticated'); } setIsLoading(true); setRawError(null); try { // Add user message to conversation const userMessage = { id: Date.now().toString(), role: 'user', content: message, timestamp: new Date() }; setMessages(prev => [...prev, userMessage]); const response = await (0, mcp_1.makeMCPRequest)(user, apiKey, options.tool || 'jean_memory', { user_message: message, is_new_conversation: messages.length <= 1, needs_context: true }); if (response.error) { throw new Error(response.error.message); } const assistantMessage = { id: (Date.now() + 1).toString(), role: 'assistant', content: response.result?.content?.[0]?.text || 'I understood and saved that information.', timestamp: new Date() }; setMessages(prev => [...prev, assistantMessage]); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to send message'; setRawError(errorMessage); throw err; } finally { setIsLoading(false); } }; const getContext = async (query, options = { mode: 'balanced' }) => { if (!user) { throw new Error('User not authenticated'); } const response = await (0, mcp_1.makeMCPRequest)(user, apiKey, 'jean_memory', // Always use the jean_memory tool { user_message: query, is_new_conversation: false, // getContext is for ongoing context retrieval needs_context: true, speed: options.mode === 'deep' ? 'comprehensive' : options.mode, // Map 'deep' to 'comprehensive' for backward compat }); if (response.error) { throw new Error(response.error.message); } // Extract text content from response for consistency const content = response.result?.content?.[0]?.text || response.result; // Add to messages if content was retrieved (for UI consistency) if (content && typeof content === 'string') { const assistantMessage = { id: (Date.now() + 1).toString(), role: 'assistant', content: content, timestamp: new Date() }; setMessages(prev => [...prev, assistantMessage]); } return response.result; }; const storeDocument = async (title, content) => { if (!user) { throw new Error("User not authenticated"); } const response = await (0, mcp_1.makeMCPRequest)(user, apiKey, 'store_document', { title, content, document_type: 'markdown' }); if (response.error) { throw new Error(response.error.message); } }; const connect = (service) => { if (!user) { throw new Error('User not authenticated'); } // Open OAuth flow for external service integration const integrationUrl = `${config_1.JEAN_API_BASE}/api/v1/integrations/${service}/connect?user_token=${user.access_token}`; // Open in new window for better UX const popup = window.open(integrationUrl, `connect-${service}`, 'width=600,height=700,scrollbars=yes,resizable=yes'); // Listen for completion const checkClosed = setInterval(() => { if (popup?.closed) { clearInterval(checkClosed); console.log(`${service} integration window closed`); // Could refresh integrations list here } }, 1000); }; const clearConversation = () => { setMessages([]); }; // Direct memory tools const tools = { add_memory: async (content) => { if (!user) { throw new Error('User not authenticated'); } const response = await (0, mcp_1.makeMCPRequest)(user, apiKey, 'add_memory', { content }); if (response.error) { throw new Error(response.error.message); } return response.result; }, search_memory: async (query) => { if (!user) { throw new Error('User not authenticated'); } const response = await (0, mcp_1.makeMCPRequest)(user, apiKey, 'search_memory', { query }); if (response.error) { throw new Error(response.error.message); } return response.result; }, deep_memory_query: async (query) => { if (!user) { throw new Error('User not authenticated'); } const response = await (0, mcp_1.makeMCPRequest)(user, apiKey, 'deep_memory_query', { search_query: query }); if (response.error) { throw new Error(response.error.message); } return response.result; }, store_document: async (title, content, document_type = 'markdown') => { if (!user) { throw new Error('User not authenticated'); } const response = await (0, mcp_1.makeMCPRequest)(user, apiKey, 'store_document', { title, content, document_type }); if (response.error) { throw new Error(response.error.message); } return response.result; }, getContext: getContext }; // Initialize test user for development/testing const initializeTestUser = async () => { if (!apiKey.includes('test')) { setRawError('Test user initialization only available with test API keys'); return; } try { setIsLoading(true); // Generate consistent test user ID from API key const encoder = new TextEncoder(); const data = encoder.encode(apiKey); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map(b => { const hex = b.toString(16); return hex.length === 1 ? '0' + hex : hex; }).join('').substring(0, 8); const testUserId = `test_user_${hashHex}`; // Create test user object const testUser = { email: 'test@example.com', name: 'Test User', access_token: `test_token_${hashHex}` }; console.log('๐Ÿงช Test user initialized:', testUser); handleSetUser(testUser); } catch (error) { setRawError('Failed to initialize test user'); console.error('Test user initialization error:', error); } finally { setIsLoading(false); } }; const signIn = async () => { if (!isInitialized) { setRawError('Authentication system not ready. Please try again.'); return; } setIsLoading(true); setRawError(null); try { // Use Universal OAuth 2.1 PKCE flow await (0, oauth_1.initiateOAuth)({ apiKey, redirectUri: window.location.origin + window.location.pathname }); // User will be redirected to Google OAuth } catch (error) { setIsLoading(false); const errorMessage = error instanceof Error ? error.message : 'Sign in failed'; setRawError(errorMessage); } }; const handleSetUser = (newUser) => { setUser(newUser); (0, oauth_1.storeUserSession)(newUser); setRawError(null); // Clear any auth errors }; const signOut = async () => { // Clear all session data (0, oauth_1.clearUserSession)(); setUser(null); setMessages([]); setRawError(null); console.log('๐Ÿ‘‹ User signed out'); }; const contextValue = { // Essential state isAuthenticated: !!user, isLoading, user, messages, error: rawError, apiKey, // Essential methods signIn, signOut, sendMessage, getContext, storeDocument, connect, clearConversation, setUser: handleSetUser, // Tools tools }; return ((0, jsx_runtime_1.jsx)(JeanContext.Provider, { value: contextValue, children: children })); } function useJean() { const context = (0, react_1.useContext)(JeanContext); if (!context) { throw new Error('useJean must be used within a JeanProvider'); } return context; }