@jeanmemory/react
Version:
React SDK for Jean Memory - Build personalized AI chatbots in 5 lines of code
333 lines (332 loc) โข 13.1 kB
JavaScript
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;
}
;