@jeanmemory/react
Version:
React SDK for Jean Memory - Build personalized AI chatbots in 5 lines of code
846 lines (845 loc) • 34.5 kB
JavaScript
;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const jsxRuntime = require("react/jsx-runtime");
const react = require("react");
const JEAN_API_BASE = "https://jean-memory-api-virginia.onrender.com";
const JEAN_OAUTH_BASE = "https://jeanmemory.com";
let requestId = 0;
async function makeMCPRequest(user, apiKey, toolName, arguments_, clientName = "react-sdk") {
const id = ++requestId;
const mcpRequest = {
jsonrpc: "2.0",
id,
method: "tools/call",
params: {
name: toolName,
arguments: arguments_
}
};
const response = await fetch(`${JEAN_API_BASE}/mcp/${clientName}/messages/${user.user_id}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-User-Id": user.user_id,
"X-Client-Name": clientName,
"X-API-Key": apiKey
},
body: JSON.stringify(mcpRequest)
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`MCP request failed: ${response.statusText} - ${errorBody}`);
}
return response.json();
}
const JeanContext = react.createContext(null);
function generatePKCE$1() {
const verifier = generateRandomString$1(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$1(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] = react.useState(null);
const [messages, setMessages] = react.useState([]);
const [isLoading, setIsLoading] = react.useState(false);
const [rawError, setRawError] = react.useState(null);
react.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");
}, [apiKey]);
react.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 {
const userMessage = {
id: Date.now().toString(),
role: "user",
content: message,
timestamp: /* @__PURE__ */ new Date()
};
setMessages((prev) => [...prev, userMessage]);
const response = await 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: /* @__PURE__ */ 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 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");
}
const integrationUrl = `${JEAN_API_BASE}/api/v1/integrations/${service}/connect?user_token=${user.access_token}`;
const popup = window.open(
integrationUrl,
`connect-${service}`,
"width=600,height=700,scrollbars=yes,resizable=yes"
);
const checkClosed = setInterval(() => {
if (popup?.closed) {
clearInterval(checkClosed);
console.log(`${service} integration window closed`);
}
}, 1e3);
};
const signIn = async () => {
setIsLoading(true);
try {
const { verifier, challenge } = await generatePKCE$1();
const state = generateRandomString$1(32);
sessionStorage.setItem("jean_oauth_state", state);
sessionStorage.setItem("jean_oauth_verifier", verifier);
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"
});
window.location.href = `${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 clearConversation = () => {
setMessages([]);
};
const tools = {
add_memory: async (content) => {
if (!user) {
throw new Error("User not authenticated");
}
const response = await 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 makeMCPRequest(
user,
apiKey,
"search_memory",
{ query }
);
if (response.error) {
throw new Error(response.error.message);
}
return response.result;
}
};
const contextValue = {
// Essential state
isAuthenticated: !!user,
isLoading,
user,
messages,
error: rawError,
// Essential methods
signIn,
signOut,
sendMessage,
storeDocument,
connect,
clearConversation,
setUser: handleSetUser,
// Tools
tools
};
return /* @__PURE__ */ jsxRuntime.jsx(JeanContext.Provider, { value: contextValue, children });
}
function useJean() {
const context = react.useContext(JeanContext);
if (!context) {
throw new Error("useJean must be used within a JeanProvider");
}
return context;
}
/**
* @license lucide-react v0.395.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
const mergeClasses = (...classes) => classes.filter((className, index, array) => {
return Boolean(className) && array.indexOf(className) === index;
}).join(" ");
/**
* @license lucide-react v0.395.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
var defaultAttributes = {
xmlns: "http://www.w3.org/2000/svg",
width: 24,
height: 24,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 2,
strokeLinecap: "round",
strokeLinejoin: "round"
};
/**
* @license lucide-react v0.395.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Icon = react.forwardRef(
({
color = "currentColor",
size = 24,
strokeWidth = 2,
absoluteStrokeWidth,
className = "",
children,
iconNode,
...rest
}, ref) => {
return react.createElement(
"svg",
{
ref,
...defaultAttributes,
width: size,
height: size,
stroke: color,
strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth,
className: mergeClasses("lucide", className),
...rest
},
[
...iconNode.map(([tag, attrs]) => react.createElement(tag, attrs)),
...Array.isArray(children) ? children : [children]
]
);
}
);
/**
* @license lucide-react v0.395.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const createLucideIcon = (iconName, iconNode) => {
const Component = react.forwardRef(
({ className, ...props }, ref) => react.createElement(Icon, {
ref,
iconNode,
className: mergeClasses(`lucide-${toKebabCase(iconName)}`, className),
...props
})
);
Component.displayName = `${iconName}`;
return Component;
};
/**
* @license lucide-react v0.395.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Bot = createLucideIcon("Bot", [
["path", { d: "M12 8V4H8", key: "hb8ula" }],
["rect", { width: "16", height: "12", x: "4", y: "8", rx: "2", key: "enze0r" }],
["path", { d: "M2 14h2", key: "vft8re" }],
["path", { d: "M20 14h2", key: "4cs60a" }],
["path", { d: "M15 13v2", key: "1xurst" }],
["path", { d: "M9 13v2", key: "rq6x2g" }]
]);
/**
* @license lucide-react v0.395.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const LogOut = createLucideIcon("LogOut", [
["path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4", key: "1uf3rs" }],
["polyline", { points: "16 17 21 12 16 7", key: "1gabdz" }],
["line", { x1: "21", x2: "9", y1: "12", y2: "12", key: "1uyos4" }]
]);
/**
* @license lucide-react v0.395.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Moon = createLucideIcon("Moon", [
["path", { d: "M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z", key: "a7tn18" }]
]);
/**
* @license lucide-react v0.395.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Send = createLucideIcon("Send", [
["path", { d: "m22 2-7 20-4-9-9-4Z", key: "1q3vgg" }],
["path", { d: "M22 2 11 13", key: "nzbqef" }]
]);
/**
* @license lucide-react v0.395.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Sun = createLucideIcon("Sun", [
["circle", { cx: "12", cy: "12", r: "4", key: "4exip2" }],
["path", { d: "M12 2v2", key: "tus03m" }],
["path", { d: "M12 20v2", key: "1lh1kg" }],
["path", { d: "m4.93 4.93 1.41 1.41", key: "149t6j" }],
["path", { d: "m17.66 17.66 1.41 1.41", key: "ptbguv" }],
["path", { d: "M2 12h2", key: "1t8f8n" }],
["path", { d: "M20 12h2", key: "1q8mjw" }],
["path", { d: "m6.34 17.66-1.41 1.41", key: "1m8zz5" }],
["path", { d: "m19.07 4.93-1.41 1.41", key: "1shlcs" }]
]);
/**
* @license lucide-react v0.395.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Trash2 = createLucideIcon("Trash2", [
["path", { d: "M3 6h18", key: "d0wm0j" }],
["path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6", key: "4alrt4" }],
["path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2", key: "v07s0e" }],
["line", { x1: "10", x2: "10", y1: "11", y2: "17", key: "1uufr5" }],
["line", { x1: "14", x2: "14", y1: "11", y2: "17", key: "xtxkd" }]
]);
/**
* @license lucide-react v0.395.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const User = createLucideIcon("User", [
["path", { d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2", key: "975kel" }],
["circle", { cx: "12", cy: "7", r: "4", key: "17ys0d" }]
]);
/**
* @license lucide-react v0.395.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const X = createLucideIcon("X", [
["path", { d: "M18 6 6 18", key: "1bl5f8" }],
["path", { d: "m6 6 12 12", key: "d8bk6v" }]
]);
function JeanChat({
className = "",
showHeader = true,
placeholder = "Type your message..."
}) {
const agent = useJean();
const [input, setInput] = react.useState("");
const messagesEndRef = react.useRef(null);
const [isDark, setIsDark] = react.useState(false);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
react.useEffect(() => {
scrollToBottom();
}, [agent.messages]);
react.useEffect(() => {
if (typeof window !== "undefined") {
const storedTheme = localStorage.getItem("jean-chat-theme");
if (storedTheme === "dark") {
setIsDark(true);
}
}
}, []);
react.useEffect(() => {
if (isDark) {
document.documentElement.classList.add("dark");
localStorage.setItem("jean-chat-theme", "dark");
} else {
document.documentElement.classList.remove("dark");
localStorage.setItem("jean-chat-theme", "light");
}
}, [isDark]);
const handleSubmit = async (e) => {
e.preventDefault();
if (!input.trim() || agent.isLoading) return;
const message = input.trim();
setInput("");
try {
await agent.sendMessage(message);
} catch (err) {
console.error("Failed to send message:", err);
}
};
if (!agent.isAuthenticated) {
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `flex items-center justify-center h-full ${className} bg-gray-50 dark:bg-gray-900`, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center p-8 max-w-md w-full", children: [
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-16 h-16 mx-auto mb-6 rounded-2xl bg-white dark:bg-gray-800 flex items-center justify-center border border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
"path",
{
d: "M12 2L13.09 8.26L20 9L13.09 15.74L12 22L10.91 15.74L4 9L10.91 8.26L12 2Z",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
className: "text-gray-600 dark:text-gray-400"
}
) }) }),
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xl font-semibold text-gray-900 dark:text-white mb-2", children: "Connect with Jean Memory" }),
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 dark:text-gray-400 text-sm mb-6", children: "Access your personalized AI assistant with persistent memory across all your applications." }),
/* @__PURE__ */ jsxRuntime.jsxs(
"button",
{
onClick: agent.signIn,
className: "inline-flex items-center justify-center gap-2.5 px-5 py-2.5 bg-black text-white font-medium text-sm border border-transparent rounded-lg hover:bg-gray-800 transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-200 dark:focus:ring-gray-300 shadow-md hover:shadow-lg disabled:opacity-60 disabled:cursor-not-allowed",
children: [
/* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
"path",
{
d: "M12 2L13.09 8.26L20 9L13.09 15.74L12 22L10.91 15.74L4 9L10.91 8.26L12 2Z",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round"
}
) }),
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Sign In with Jean" })
]
}
)
] }) });
}
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col h-full bg-white dark:bg-gray-900 text-gray-900 dark:text-white ${className}`, children: [
showHeader && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between items-center px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800", children: [
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center space-x-3", children: [
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-10 h-10 rounded-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(Bot, { size: 20, className: "text-white" }) }),
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "font-semibold", children: "Jean Memory Assistant" }),
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: [
"Connected as ",
agent.user?.email
] })
] })
] }),
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center space-x-2", children: [
/* @__PURE__ */ jsxRuntime.jsx(
"button",
{
onClick: () => setIsDark(!isDark),
className: "p-2 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full transition-colors",
children: isDark ? /* @__PURE__ */ jsxRuntime.jsx(Sun, { size: 16 }) : /* @__PURE__ */ jsxRuntime.jsx(Moon, { size: 16 })
}
),
/* @__PURE__ */ jsxRuntime.jsx(
"button",
{
onClick: () => window.location.reload(),
disabled: agent.messages.length === 0,
className: "p-2 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full disabled:opacity-50 disabled:cursor-not-allowed transition-colors",
title: "Clear Conversation",
children: /* @__PURE__ */ jsxRuntime.jsx(Trash2, { size: 16 })
}
),
/* @__PURE__ */ jsxRuntime.jsx(
"button",
{
onClick: agent.signOut,
className: "p-2 text-red-500 hover:bg-red-100 dark:hover:bg-red-900/50 rounded-full transition-colors",
title: "Sign Out",
children: /* @__PURE__ */ jsxRuntime.jsx(LogOut, { size: 16 })
}
)
] })
] }),
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-y-auto p-6 space-y-6", children: agent.messages.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center h-full text-center text-gray-500 dark:text-gray-400", children: [
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-20 h-20 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center mb-4", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "40", height: "40", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", className: "text-gray-400 dark:text-gray-500", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 2L13.09 8.26L20 9L13.09 15.74L12 22L10.91 15.74L4 9L10.91 8.26L12 2Z" }) }) }),
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-medium text-gray-800 dark:text-gray-200 mb-1", children: "Start a conversation" }),
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm max-w-sm", children: "Ask me anything! I have access to your personal context and can help with a wide range of topics." })
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
agent.messages.map((message) => /* @__PURE__ */ jsxRuntime.jsxs(
"div",
{
className: `flex items-start gap-3 ${message.role === "user" ? "justify-end" : "justify-start"}`,
children: [
message.role !== "user" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-8 h-8 rounded-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(Bot, { size: 18, className: "text-white" }) }),
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: `max-w-[80%]`, children: [
/* @__PURE__ */ jsxRuntime.jsx(
"div",
{
className: `px-4 py-2.5 rounded-2xl ${message.role === "user" ? "bg-blue-600 text-white rounded-br-lg" : "bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-bl-lg"}`,
children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm whitespace-pre-wrap leading-relaxed", children: message.content })
}
),
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `mt-1.5 px-1 ${message.role === "user" ? "text-right" : "text-left"}`, children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 dark:text-gray-500", children: message.timestamp.toLocaleTimeString([], {
hour: "numeric",
minute: "2-digit"
}) }) })
] }),
message.role === "user" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(User, { size: 18, className: "text-gray-600 dark:text-gray-300" }) })
]
},
message.id
)),
agent.isLoading && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3 justify-start", children: [
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-8 h-8 rounded-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(Bot, { size: 18, className: "text-white" }) }),
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 px-4 py-2.5 rounded-2xl rounded-bl-lg", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center space-x-2", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex space-x-1", children: [
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2 h-2 bg-gray-400 rounded-full animate-bounce" }),
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2 h-2 bg-gray-400 rounded-full animate-bounce", style: { animationDelay: "0.1s" } }),
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2 h-2 bg-gray-400 rounded-full animate-bounce", style: { animationDelay: "0.2s" } })
] }) }) })
] }),
/* @__PURE__ */ jsxRuntime.jsx("div", { ref: messagesEndRef })
] }) }),
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-t border-gray-200 dark:border-gray-700 p-4 bg-gray-50 dark:bg-gray-800", children: [
agent.error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-3 p-3 bg-red-100 dark:bg-red-900/30 border border-red-200 dark:border-red-800/50 rounded-lg flex items-start space-x-3", children: [
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-red-600 dark:text-red-400 mt-0.5", children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10" }),
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
] }) }),
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-red-700 dark:text-red-300 text-sm font-medium", children: "Error" }),
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-red-600 dark:text-red-400 text-sm", children: agent.error })
] }),
/* @__PURE__ */ jsxRuntime.jsx(
"button",
{
onClick: () => window.location.reload(),
className: "p-1 text-red-600 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-900/50 rounded-full",
children: /* @__PURE__ */ jsxRuntime.jsx(X, { size: 16 })
}
)
] }),
/* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className: "relative", children: [
/* @__PURE__ */ jsxRuntime.jsx(
"textarea",
{
value: input,
onChange: (e) => setInput(e.target.value),
onKeyDown: (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit(e);
}
},
placeholder,
disabled: agent.isLoading,
className: "w-full px-4 py-3 pr-12 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-2xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 resize-none text-sm",
rows: 1
}
),
/* @__PURE__ */ jsxRuntime.jsx(
"button",
{
type: "submit",
disabled: agent.isLoading || !input.trim(),
className: "absolute right-3 top-1/2 -translate-y-1/2 w-8 h-8 bg-blue-600 text-white rounded-full hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center transition-colors",
children: agent.isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(Send, { size: 16 })
}
)
] })
] })
] });
}
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 SignInWithJean({
onSuccess,
onError,
apiKey,
className = "",
children
}) {
const [isLoading, setIsLoading] = react.useState(false);
react.useEffect(() => {
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
const state = params.get("state");
if (code && state) {
handleOAuthCallback(code, state);
}
}, []);
const handleOAuthCallback = async (code, state) => {
try {
const storedState = sessionStorage.getItem("jean_oauth_state");
const verifier = sessionStorage.getItem("jean_oauth_verifier");
if (state !== storedState) {
throw new Error("State mismatch - possible CSRF attack");
}
if (!verifier) {
throw new Error("Missing PKCE verifier");
}
const response = await fetch(`${JEAN_API_BASE}/oauth/token`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
grant_type: "authorization_code",
code,
redirect_uri: window.location.origin + window.location.pathname,
code_verifier: verifier,
client_id: apiKey || "default_client"
})
});
if (!response.ok) {
throw new Error("Failed to exchange code for token");
}
const data = await response.json();
const userResponse = await fetch(`${JEAN_API_BASE}/api/v1/user/me`, {
headers: {
"Authorization": `Bearer ${data.access_token}`
}
});
if (!userResponse.ok) {
throw new Error("Failed to get user info");
}
const user = await userResponse.json();
user.access_token = data.access_token;
sessionStorage.removeItem("jean_oauth_state");
sessionStorage.removeItem("jean_oauth_verifier");
const url = new URL(window.location.href);
url.searchParams.delete("code");
url.searchParams.delete("state");
window.history.replaceState({}, "", url.toString());
onSuccess(user);
} catch (error) {
console.error("OAuth callback error:", error);
if (onError) {
onError(error instanceof Error ? error : new Error("OAuth callback failed"));
}
}
};
const handleSignIn = async () => {
setIsLoading(true);
try {
const { verifier, challenge } = await generatePKCE();
const state = generateRandomString(32);
sessionStorage.setItem("jean_oauth_state", state);
sessionStorage.setItem("jean_oauth_verifier", verifier);
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"
});
window.location.href = `${JEAN_OAUTH_BASE}/oauth/authorize?${params.toString()}`;
} catch (error) {
setIsLoading(false);
console.error("Sign in error:", error);
if (onError) {
onError(error instanceof Error ? error : new Error("Sign in failed"));
}
}
};
return /* @__PURE__ */ jsxRuntime.jsx(
"button",
{
onClick: handleSignIn,
disabled: isLoading,
className: `inline-flex items-center justify-center gap-2.5 px-5 py-2.5 bg-black text-white font-medium text-sm border border-transparent rounded-lg hover:bg-gray-800 transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-200 dark:focus:ring-gray-300 shadow-md hover:shadow-lg disabled:opacity-60 disabled:cursor-not-allowed ${className}`,
children: isLoading ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-4 h-4 border-2 border-white dark:border-black border-t-transparent rounded-full animate-spin" }),
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Signing in..." })
] }) : children || /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
/* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
"path",
{
d: "M12 2L13.09 8.26L20 9L13.09 15.74L12 22L10.91 15.74L4 9L10.91 8.26L12 2Z",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round"
}
) }),
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Sign In with Jean" })
] })
}
);
}
function useJeanMCP({ apiKey, clientName = "react-app" }) {
const callJeanMemory = react.useCallback(
async (user, message, isNewConversation = false) => {
const response = await makeMCPRequest(
user,
apiKey,
"jean_memory",
{
user_message: message,
is_new_conversation: isNewConversation,
needs_context: true
},
clientName
);
if (response.error) {
throw new Error(`MCP Error: ${response.error.message}`);
}
return response.result?.content?.[0]?.text || "No response from tool";
},
[apiKey, clientName]
);
const addMemory = react.useCallback(
async (user, content) => {
const response = await makeMCPRequest(
user,
apiKey,
"add_memories",
{ text: content },
clientName
);
if (response.error) {
throw new Error(`MCP Error: ${response.error.message}`);
}
return response.result?.content?.[0]?.text || "No response from tool";
},
[apiKey, clientName]
);
const searchMemory = react.useCallback(
async (user, query) => {
const response = await makeMCPRequest(
user,
apiKey,
"search_memory",
{ query },
clientName
);
if (response.error) {
throw new Error(`MCP Error: ${response.error.message}`);
}
return response.result?.content?.[0]?.text || "No response from tool";
},
[apiKey, clientName]
);
const storeDocument = react.useCallback(
async (user, title, content, type = "markdown") => {
const response = await makeMCPRequest(
user,
apiKey,
"store_document",
{
title,
content,
document_type: type
},
clientName
);
if (response.error) {
throw new Error(`MCP Error: ${response.error.message}`);
}
return response.result?.content?.[0]?.text || "No response from tool";
},
[apiKey, clientName]
);
return {
callJeanMemory,
addMemory,
searchMemory,
storeDocument
};
}
exports.JeanChat = JeanChat;
exports.JeanProvider = JeanProvider;
exports.SignInWithJean = SignInWithJean;
exports.useJean = useJean;
exports.useJeanMCP = useJeanMCP;
//# sourceMappingURL=index.cjs.map