@jeanmemory/react
Version:
React SDK for Jean Memory - Build personalized AI chatbots in 5 lines of code
225 lines (224 loc) • 8.17 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 JEAN_API_BASE_OLD = 'https://jean-memory-api-virginia.onrender.com';
const JeanContext = (0, react_1.createContext)(null);
// Generate code verifier and challenge for PKCE
function generatePKCE() {
const verifier = generateRandomString(128);
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
return crypto.subtle.digest('SHA-256', data).then(digest => {
const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
return { verifier, challenge };
});
}
function generateRandomString(length) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
const values = crypto.getRandomValues(new Uint8Array(length));
return Array.from(values)
.map(x => charset[x % charset.length])
.join('');
}
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);
// Validate API key on mount
(0, react_1.useEffect)(() => {
if (!apiKey) {
setRawError('API key is required');
return;
}
if (!apiKey.startsWith('jean_sk_')) {
setRawError('Invalid API key format');
return;
}
// API key will be validated on first request
console.log('✅ Jean Memory SDK initialized');
}, [apiKey]);
// Load user from localStorage on mount
(0, react_1.useEffect)(() => {
const storedUser = localStorage.getItem('jean_user');
if (storedUser) {
try {
setUser(JSON.parse(storedUser));
}
catch (e) {
localStorage.removeItem('jean_user');
}
}
}, []);
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 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;
}
};
const signIn = async () => {
setIsLoading(true);
try {
// Generate PKCE parameters
const { verifier, challenge } = await generatePKCE();
const state = generateRandomString(32);
// Store for callback
sessionStorage.setItem('jean_oauth_state', state);
sessionStorage.setItem('jean_oauth_verifier', verifier);
// Build OAuth URL
const params = new URLSearchParams({
response_type: 'code',
client_id: apiKey || 'default_client',
redirect_uri: window.location.origin + window.location.pathname,
state,
code_challenge: challenge,
code_challenge_method: 'S256',
scope: 'read write'
});
// Redirect to OAuth provider
window.location.href = `${config_1.JEAN_API_BASE}/oauth/authorize?${params.toString()}`;
}
catch (error) {
setIsLoading(false);
const errorMessage = error instanceof Error ? error.message : 'Sign in failed';
setRawError(errorMessage);
}
};
const handleSetUser = (newUser) => {
setUser(newUser);
localStorage.setItem('jean_user', JSON.stringify(newUser));
};
const signOut = () => {
setUser(null);
setMessages([]);
localStorage.removeItem('jean_user');
};
const contextValue = {
// Essential state
isAuthenticated: !!user,
isLoading,
user,
messages,
error: rawError,
// Essential methods
signIn,
signOut,
sendMessage,
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;
}