@lastbrain/ai-ui-react 1.0.38 → 1.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -279,7 +279,6 @@ function AiPromptPanelInternal({ isOpen, onClose, onSubmit, uiMode: _uiMode = "m
279
279
  position: "sticky",
280
280
  top: 0,
281
281
  zIndex: 5,
282
- background: "var(--ai-bg-primary, #1f2937)",
283
282
  borderBottom: "1px solid var(--ai-border-primary, #374151)",
284
283
  backdropFilter: "blur(8px)",
285
284
  }, children: [_jsx("h2", { style: aiStyles.modalTitle, children: showPromptLibrary ? "Select a Prompt" : "AI Prompt Configuration" }), _jsx("button", { style: {
@@ -530,7 +529,6 @@ function AiPromptPanelInternal({ isOpen, onClose, onSubmit, uiMode: _uiMode = "m
530
529
  position: "sticky",
531
530
  bottom: 0,
532
531
  zIndex: 5,
533
- background: "var(--ai-bg-primary, #1f2937)",
534
532
  borderTop: "1px solid var(--ai-border-primary, #374151)",
535
533
  backdropFilter: "blur(8px)",
536
534
  }, children: [_jsx("button", { onClick: handleClose, onMouseEnter: () => setIsCancelHovered(true), onMouseLeave: () => setIsCancelHovered(false), style: {
@@ -561,7 +559,6 @@ function AiPromptPanelInternal({ isOpen, onClose, onSubmit, uiMode: _uiMode = "m
561
559
  overflow: "hidden",
562
560
  display: "flex",
563
561
  flexDirection: "column",
564
- background: "var(--ai-bg-primary, #1f2937)",
565
562
  boxShadow: "0 20px 40px rgba(0, 0, 0, 0.5)",
566
563
  }, children: [_jsxs("div", { style: aiStyles.modalHeader, children: [_jsx("h2", { style: aiStyles.modalTitle, children: "Gestion des mod\u00E8les IA" }), _jsx("button", { style: aiStyles.modalCloseButton, onClick: () => {
567
564
  setIsModelManagementOpen(false);
@@ -662,9 +659,7 @@ function AiPromptPanelInternal({ isOpen, onClose, onSubmit, uiMode: _uiMode = "m
662
659
  }, children: [_jsx("span", { style: {
663
660
  fontWeight: "600",
664
661
  fontSize: "15px",
665
- color: isActive
666
- ? "#10b981"
667
- : "var(--ai-text-primary, #f3f4f6)",
662
+ color: "var(--ai-text-secondary, #6b7280)",
668
663
  letterSpacing: "-0.01em",
669
664
  }, children: modelData.name }), modelData.isPro && (_jsx("span", { style: {
670
665
  padding: "3px 10px",
@@ -0,0 +1,20 @@
1
+ interface LBConnectButtonProps {
2
+ /** Texte du bouton */
3
+ label?: string;
4
+ /** Classe CSS personnalisée */
5
+ className?: string;
6
+ /** Callback après connexion réussie */
7
+ onConnected?: () => void;
8
+ /** Callback à l'ouverture de la modal */
9
+ onOpenModal?: () => void;
10
+ }
11
+ export declare function LBConnectButton({ label, className, onConnected, onOpenModal, }: LBConnectButtonProps): import("react/jsx-runtime").JSX.Element;
12
+ /**
13
+ * Modal d'authentification LastBrain
14
+ */
15
+ interface LBAuthModalProps {
16
+ onClose: (success: boolean) => void;
17
+ }
18
+ declare function LBAuthModal({ onClose }: LBAuthModalProps): import("react/jsx-runtime").JSX.Element;
19
+ export { LBAuthModal };
20
+ //# sourceMappingURL=LBConnectButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LBConnectButton.d.ts","sourceRoot":"","sources":["../../src/components/LBConnectButton.tsx"],"names":[],"mappings":"AAUA,UAAU,oBAAoB;IAC5B,sBAAsB;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B;AAED,wBAAgB,eAAe,CAAC,EAC9B,KAAkC,EAClC,SAAc,EACd,WAAW,EACX,WAAW,GACZ,EAAE,oBAAoB,2CA0CtB;AAED;;GAEG;AACH,UAAU,gBAAgB;IACxB,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACrC;AAED,iBAAS,WAAW,CAAC,EAAE,OAAO,EAAE,EAAE,gBAAgB,2CAsJjD;AAED,OAAO,EAAE,WAAW,EAAE,CAAC"}
@@ -0,0 +1,87 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * Bouton de connexion LastBrain
5
+ * Ouvre la modal d'authentification
6
+ */
7
+ import React from "react";
8
+ import { useLB } from "../context/LBAuthProvider";
9
+ export function LBConnectButton({ label = "Se connecter à LastBrain", className = "", onConnected, onOpenModal, }) {
10
+ const { status, user, logout } = useLB();
11
+ const [showModal, setShowModal] = React.useState(false);
12
+ const handleClick = () => {
13
+ if (status === "ready" && user) {
14
+ // Déjà connecté, proposer de se déconnecter
15
+ logout();
16
+ }
17
+ else {
18
+ // Pas connecté, ouvrir la modal
19
+ setShowModal(true);
20
+ onOpenModal?.();
21
+ }
22
+ };
23
+ const handleModalClose = (success) => {
24
+ setShowModal(false);
25
+ if (success) {
26
+ onConnected?.();
27
+ }
28
+ };
29
+ return (_jsxs(_Fragment, { children: [_jsx("button", { onClick: handleClick, className: className ||
30
+ "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700", disabled: status === "loading", children: status === "loading"
31
+ ? "Chargement..."
32
+ : status === "ready" && user
33
+ ? `Connecté (${user.email})`
34
+ : label }), showModal && _jsx(LBAuthModal, { onClose: handleModalClose })] }));
35
+ }
36
+ function LBAuthModal({ onClose }) {
37
+ const { login, fetchApiKeys, selectApiKey, status } = useLB();
38
+ const [step, setStep] = React.useState("login");
39
+ const [email, setEmail] = React.useState("");
40
+ const [password, setPassword] = React.useState("");
41
+ const [error, setError] = React.useState("");
42
+ const [loading, setLoading] = React.useState(false);
43
+ const [accessToken, setAccessToken] = React.useState("");
44
+ const [apiKeys, setApiKeys] = React.useState([]);
45
+ const handleLogin = async (e) => {
46
+ e.preventDefault();
47
+ setError("");
48
+ setLoading(true);
49
+ try {
50
+ const result = await login(email, password);
51
+ setAccessToken(result.accessToken);
52
+ // Récupérer les clés API
53
+ const keys = await fetchApiKeys(result.accessToken);
54
+ setApiKeys(keys);
55
+ if (keys.length === 0) {
56
+ setError("Aucune clé API active trouvée. Créez-en une dans votre dashboard LastBrain.");
57
+ return;
58
+ }
59
+ // Passer à l'étape de sélection
60
+ setStep("select-key");
61
+ }
62
+ catch (err) {
63
+ setError(err instanceof Error ? err.message : "Échec de la connexion");
64
+ }
65
+ finally {
66
+ setLoading(false);
67
+ }
68
+ };
69
+ const handleSelectKey = async (apiKeyId) => {
70
+ setError("");
71
+ setLoading(true);
72
+ try {
73
+ await selectApiKey(accessToken, apiKeyId);
74
+ onClose(true); // Succès
75
+ }
76
+ catch (err) {
77
+ setError(err instanceof Error ? err.message : "Échec de la sélection");
78
+ }
79
+ finally {
80
+ setLoading(false);
81
+ }
82
+ };
83
+ return (_jsx("div", { className: "fixed inset-0 bg-black/50 flex items-center justify-center z-50", children: _jsxs("div", { className: "bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4", children: [_jsxs("div", { className: "flex justify-between items-center mb-4", children: [_jsx("h2", { className: "text-xl font-bold", children: step === "login"
84
+ ? "Connexion LastBrain"
85
+ : "Sélectionner une clé API" }), _jsx("button", { onClick: () => onClose(false), className: "text-gray-500 hover:text-gray-700", children: "\u2715" })] }), step === "login" ? (_jsxs("form", { onSubmit: handleLogin, className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-sm font-medium mb-1", children: "Email" }), _jsx("input", { type: "email", value: email, onChange: (e) => setEmail(e.target.value), className: "w-full px-3 py-2 border rounded dark:bg-gray-700", required: true, autoFocus: true })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-sm font-medium mb-1", children: "Mot de passe" }), _jsx("input", { type: "password", value: password, onChange: (e) => setPassword(e.target.value), className: "w-full px-3 py-2 border rounded dark:bg-gray-700", required: true })] }), error && _jsx("div", { className: "text-red-600 text-sm", children: error }), _jsx("button", { type: "submit", disabled: loading, className: "w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50", children: loading ? "Connexion..." : "Se connecter" }), _jsxs("p", { className: "text-sm text-gray-600 dark:text-gray-400 text-center", children: ["Pas encore de compte ?", " ", _jsx("a", { href: "https://lastbrain.io/signup", target: "_blank", rel: "noopener noreferrer", className: "text-blue-600 hover:underline", children: "Cr\u00E9er un compte" })] })] })) : (_jsxs("div", { className: "space-y-4", children: [_jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400", children: "S\u00E9lectionnez une cl\u00E9 API pour cr\u00E9er une session de 72h" }), _jsx("div", { className: "space-y-2", children: apiKeys.map((key) => (_jsxs("button", { onClick: () => handleSelectKey(key.id), disabled: !key.isActive || loading, className: "w-full text-left px-4 py-3 border rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed", children: [_jsx("div", { className: "font-medium", children: key.name }), _jsxs("div", { className: "text-sm text-gray-500", children: [key.keyPrefix, "..."] }), !key.isActive && (_jsx("div", { className: "text-xs text-red-600", children: "Inactive" }))] }, key.id))) }), error && _jsx("div", { className: "text-red-600 text-sm", children: error })] }))] }) }));
86
+ }
87
+ export { LBAuthModal };
@@ -0,0 +1,10 @@
1
+ import type { LBApiKey } from "@lastbrain/ai-ui-core";
2
+ interface LBKeyPickerProps {
3
+ /** Classe CSS personnalisée */
4
+ className?: string;
5
+ /** Callback après changement de clé */
6
+ onKeyChanged?: (key: LBApiKey) => void;
7
+ }
8
+ export declare function LBKeyPicker({ className, onKeyChanged, }: LBKeyPickerProps): import("react/jsx-runtime").JSX.Element | null;
9
+ export {};
10
+ //# sourceMappingURL=LBKeyPicker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LBKeyPicker.d.ts","sourceRoot":"","sources":["../../src/components/LBKeyPicker.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEtD,UAAU,gBAAgB;IACxB,+BAA+B;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,IAAI,CAAC;CACxC;AAED,wBAAgB,WAAW,CAAC,EAC1B,SAAc,EACd,YAAY,GACb,EAAE,gBAAgB,kDA2HlB"}
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * Composant LBKeyPicker
5
+ * Permet de changer de clé API sans se reconnecter
6
+ */
7
+ import { useEffect, useState } from "react";
8
+ import { useLB } from "../hooks/useLB";
9
+ export function LBKeyPicker({ className = "", onKeyChanged, }) {
10
+ const { status, selectedKey, fetchApiKeys, selectApiKey, accessToken } = useLB();
11
+ const [apiKeys, setApiKeys] = useState([]);
12
+ const [loading, setLoading] = useState(false);
13
+ const [error, setError] = useState("");
14
+ const [showDropdown, setShowDropdown] = useState(false);
15
+ useEffect(() => {
16
+ if (status === "ready" && accessToken) {
17
+ loadKeys();
18
+ }
19
+ }, [status, accessToken]);
20
+ const loadKeys = async () => {
21
+ if (!accessToken)
22
+ return;
23
+ try {
24
+ setLoading(true);
25
+ const keys = await fetchApiKeys(accessToken);
26
+ setApiKeys(keys);
27
+ }
28
+ catch (err) {
29
+ setError("Impossible de charger les clés API");
30
+ }
31
+ finally {
32
+ setLoading(false);
33
+ }
34
+ };
35
+ const handleSelectKey = async (keyId) => {
36
+ if (!accessToken)
37
+ return;
38
+ try {
39
+ setLoading(true);
40
+ setError("");
41
+ await selectApiKey(accessToken, keyId);
42
+ const selectedApiKey = apiKeys.find((k) => k.id === keyId);
43
+ if (selectedApiKey) {
44
+ onKeyChanged?.(selectedApiKey);
45
+ }
46
+ setShowDropdown(false);
47
+ }
48
+ catch (err) {
49
+ setError(err instanceof Error ? err.message : "Échec du changement de clé");
50
+ }
51
+ finally {
52
+ setLoading(false);
53
+ }
54
+ };
55
+ if (status !== "ready" || !selectedKey) {
56
+ return null;
57
+ }
58
+ return (_jsxs("div", { className: `relative ${className}`, children: [_jsxs("button", { onClick: () => setShowDropdown(!showDropdown), disabled: loading, className: "px-4 py-2 border rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 flex items-center gap-2", children: [_jsxs("span", { className: "font-mono text-sm", children: [selectedKey.keyPrefix, "..."] }), _jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }) })] }), showDropdown && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-10", onClick: () => setShowDropdown(false) }), _jsxs("div", { className: "absolute right-0 mt-2 w-64 bg-white dark:bg-gray-800 border rounded-lg shadow-lg z-20", children: [_jsxs("div", { className: "p-2", children: [_jsx("div", { className: "text-sm font-medium text-gray-700 dark:text-gray-300 px-3 py-2", children: "Changer de cl\u00E9 API" }), _jsx("div", { className: "space-y-1", children: apiKeys.map((key) => (_jsxs("button", { onClick: () => handleSelectKey(key.id), disabled: !key.isActive || loading || key.id === selectedKey.id, className: `w-full text-left px-3 py-2 rounded text-sm ${key.id === selectedKey.id
59
+ ? "bg-blue-100 dark:bg-blue-900"
60
+ : "hover:bg-gray-100 dark:hover:bg-gray-700"} disabled:opacity-50 disabled:cursor-not-allowed`, children: [_jsx("div", { className: "font-medium", children: key.name }), _jsxs("div", { className: "text-xs text-gray-500 font-mono", children: [key.keyPrefix, "..."] }), !key.isActive && (_jsx("div", { className: "text-xs text-red-600", children: "Inactive" }))] }, key.id))) })] }), error && (_jsx("div", { className: "px-3 py-2 text-xs text-red-600 border-t", children: error }))] })] }))] }));
61
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Provider d'authentification LastBrain
3
+ * Gère l'état de connexion, la session et les appels IA
4
+ */
5
+ import { type ReactNode } from "react";
6
+ import type { LBAuthState, LBApiKey, LBLoginResult } from "@lastbrain/ai-ui-core";
7
+ interface LBProviderProps {
8
+ children: ReactNode;
9
+ /** URL de l'API LastBrain (ex: https://api.lastbrain.io) */
10
+ baseUrl?: string;
11
+ /** URL du proxy local (ex: /api/lb) */
12
+ proxyUrl?: string;
13
+ /** Fonction appelée lors des changements d'état */
14
+ onStatusChange?: (status: LBAuthState["status"]) => void;
15
+ }
16
+ interface LBContextValue extends LBAuthState {
17
+ /** Fonction de connexion */
18
+ login: (email: string, password: string) => Promise<LBLoginResult>;
19
+ /** Fonction de déconnexion */
20
+ logout: () => Promise<void>;
21
+ /** Récupère les clés API de l'utilisateur */
22
+ fetchApiKeys: (accessToken: string) => Promise<LBApiKey[]>;
23
+ /** Sélectionne une clé API et crée une session */
24
+ selectApiKey: (accessToken: string, apiKeyId: string) => Promise<void>;
25
+ /** Recharge l'état de la session */
26
+ refreshSession: () => Promise<void>;
27
+ /** Clés API disponibles */
28
+ apiKeys: LBApiKey[];
29
+ /** Access token temporaire (après login) */
30
+ accessToken?: string;
31
+ }
32
+ export declare function LBProvider({ children, baseUrl: _baseUrl, proxyUrl, onStatusChange, }: LBProviderProps): import("react/jsx-runtime").JSX.Element;
33
+ /**
34
+ * Hook pour accéder au contexte LastBrain
35
+ */
36
+ export declare function useLB(): LBContextValue;
37
+ export {};
38
+ //# sourceMappingURL=LBAuthProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LBAuthProvider.d.ts","sourceRoot":"","sources":["../../src/context/LBAuthProvider.tsx"],"names":[],"mappings":"AAEA;;;GAGG;AAEH,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EAER,aAAa,EAEd,MAAM,uBAAuB,CAAC;AAE/B,UAAU,eAAe;IACvB,QAAQ,EAAE,SAAS,CAAC;IACpB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;CAC1D;AAED,UAAU,cAAe,SAAQ,WAAW;IAC1C,4BAA4B;IAC5B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IACnE,8BAA8B;IAC9B,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,6CAA6C;IAC7C,YAAY,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC3D,kDAAkD;IAClD,YAAY,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,oCAAoC;IACpC,cAAc,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,2BAA2B;IAC3B,OAAO,EAAE,QAAQ,EAAE,CAAC;IACpB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,OAAO,EAAE,QAAoB,EAC7B,QAAoB,EACpB,cAAc,GACf,EAAE,eAAe,2CAwMjB;AAED;;GAEG;AACH,wBAAgB,KAAK,IAAI,cAAc,CAMtC"}
@@ -0,0 +1,194 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /**
4
+ * Provider d'authentification LastBrain
5
+ * Gère l'état de connexion, la session et les appels IA
6
+ */
7
+ import { createContext, useContext, useEffect, useCallback, useState, } from "react";
8
+ const LBContext = createContext(undefined);
9
+ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lb", proxyUrl = "/api/lb", onStatusChange, }) {
10
+ const [state, setState] = useState({
11
+ status: "loading",
12
+ });
13
+ const [apiKeys, setApiKeys] = useState([]);
14
+ const [accessToken, setAccessToken] = useState();
15
+ /**
16
+ * Vérifie si une session existe au chargement
17
+ */
18
+ const checkSession = useCallback(async () => {
19
+ try {
20
+ const response = await fetch(`${proxyUrl}/auth/session/verify`, {
21
+ credentials: "include",
22
+ });
23
+ if (response.ok) {
24
+ const session = await response.json();
25
+ setState({
26
+ status: "ready",
27
+ session,
28
+ user: {
29
+ id: session.userId,
30
+ email: "", // Sera rempli par une autre requête si nécessaire
31
+ },
32
+ });
33
+ onStatusChange?.("ready");
34
+ }
35
+ else {
36
+ setState({ status: "needs_auth" });
37
+ onStatusChange?.("needs_auth");
38
+ }
39
+ }
40
+ catch (error) {
41
+ console.error("[LBProvider] Session check failed:", error);
42
+ setState({ status: "needs_auth" });
43
+ onStatusChange?.("needs_auth");
44
+ }
45
+ }, [proxyUrl, onStatusChange]);
46
+ useEffect(() => {
47
+ checkSession();
48
+ }, [checkSession]);
49
+ /**
50
+ * Connexion utilisateur
51
+ */
52
+ const login = useCallback(async (email, password) => {
53
+ try {
54
+ setState((prev) => ({ ...prev, status: "loading" }));
55
+ const response = await fetch(`${proxyUrl}/auth/login`, {
56
+ method: "POST",
57
+ headers: { "Content-Type": "application/json" },
58
+ body: JSON.stringify({ email, password }),
59
+ credentials: "include",
60
+ });
61
+ if (!response.ok) {
62
+ const error = await response.json();
63
+ throw new Error(error.message || "Login failed");
64
+ }
65
+ const result = await response.json();
66
+ setAccessToken(result.accessToken);
67
+ setState({
68
+ status: "ready",
69
+ user: result.user,
70
+ });
71
+ return result;
72
+ }
73
+ catch (error) {
74
+ const message = error instanceof Error ? error.message : "Login failed";
75
+ setState({
76
+ status: "error",
77
+ error: message,
78
+ });
79
+ throw error;
80
+ }
81
+ }, [proxyUrl]);
82
+ /**
83
+ * Récupère les clés API de l'utilisateur
84
+ */
85
+ const fetchApiKeys = useCallback(async (token) => {
86
+ try {
87
+ const response = await fetch(`${proxyUrl}/user/api-keys`, {
88
+ headers: {
89
+ Authorization: `Bearer ${token}`,
90
+ },
91
+ credentials: "include",
92
+ });
93
+ if (!response.ok) {
94
+ throw new Error("Failed to fetch API keys");
95
+ }
96
+ const keys = await response.json();
97
+ setApiKeys(keys);
98
+ return keys;
99
+ }
100
+ catch (error) {
101
+ console.error("[LBProvider] Failed to fetch API keys:", error);
102
+ throw error;
103
+ }
104
+ }, [proxyUrl]);
105
+ /**
106
+ * Sélectionne une clé API et crée une session
107
+ */
108
+ const selectApiKey = useCallback(async (token, apiKeyId) => {
109
+ try {
110
+ setState((prev) => ({ ...prev, status: "loading" }));
111
+ const response = await fetch(`${proxyUrl}/auth/session`, {
112
+ method: "POST",
113
+ headers: {
114
+ "Content-Type": "application/json",
115
+ Authorization: `Bearer ${token}`,
116
+ },
117
+ body: JSON.stringify({ api_key_id: apiKeyId }),
118
+ credentials: "include",
119
+ });
120
+ if (!response.ok) {
121
+ throw new Error("Failed to create session");
122
+ }
123
+ const sessionResult = await response.json();
124
+ setState({
125
+ status: "ready",
126
+ user: state.user,
127
+ selectedKey: sessionResult.apiKey,
128
+ session: {
129
+ sessionToken: sessionResult.sessionToken,
130
+ userId: state.user?.id || "",
131
+ apiKeyId,
132
+ expiresAt: Date.now() + sessionResult.expiresIn * 1000,
133
+ },
134
+ });
135
+ setAccessToken(undefined); // Nettoyer l'access token temporaire
136
+ onStatusChange?.("ready");
137
+ }
138
+ catch (error) {
139
+ const message = error instanceof Error ? error.message : "Failed to select API key";
140
+ setState({
141
+ status: "error",
142
+ error: message,
143
+ });
144
+ throw error;
145
+ }
146
+ }, [proxyUrl, state.user, onStatusChange]);
147
+ /**
148
+ * Déconnexion
149
+ */
150
+ const logout = useCallback(async () => {
151
+ try {
152
+ await fetch(`${proxyUrl}/auth/session/logout`, {
153
+ method: "POST",
154
+ credentials: "include",
155
+ });
156
+ }
157
+ catch (error) {
158
+ console.error("[LBProvider] Logout failed:", error);
159
+ }
160
+ finally {
161
+ setState({ status: "needs_auth" });
162
+ setApiKeys([]);
163
+ setAccessToken(undefined);
164
+ onStatusChange?.("needs_auth");
165
+ }
166
+ }, [proxyUrl, onStatusChange]);
167
+ /**
168
+ * Recharge la session
169
+ */
170
+ const refreshSession = useCallback(async () => {
171
+ await checkSession();
172
+ }, [checkSession]);
173
+ const value = {
174
+ ...state,
175
+ login,
176
+ logout,
177
+ fetchApiKeys,
178
+ selectApiKey,
179
+ refreshSession,
180
+ apiKeys,
181
+ accessToken,
182
+ };
183
+ return _jsx(LBContext.Provider, { value: value, children: children });
184
+ }
185
+ /**
186
+ * Hook pour accéder au contexte LastBrain
187
+ */
188
+ export function useLB() {
189
+ const context = useContext(LBContext);
190
+ if (!context) {
191
+ throw new Error("useLB must be used within LBProvider");
192
+ }
193
+ return context;
194
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Hook useLB - Export du hook depuis LBAuthProvider
3
+ */
4
+ export { useLB } from "../context/LBAuthProvider";
5
+ //# sourceMappingURL=useLB.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLB.d.ts","sourceRoot":"","sources":["../../src/hooks/useLB.ts"],"names":[],"mappings":"AAEA;;GAEG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC"}
@@ -0,0 +1,5 @@
1
+ "use client";
2
+ /**
3
+ * Hook useLB - Export du hook depuis LBAuthProvider
4
+ */
5
+ export { useLB } from "../context/LBAuthProvider";
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./types";
2
2
  export * from "./context/AiProvider";
3
+ export * from "./context/LBAuthProvider";
3
4
  export * from "./hooks/useAiClient";
4
5
  export * from "./hooks/useAiModels";
5
6
  export * from "./hooks/useAiStatus";
@@ -7,6 +8,7 @@ export * from "./hooks/useAiCallText";
7
8
  export * from "./hooks/useAiCallImage";
8
9
  export * from "./hooks/usePrompts";
9
10
  export * from "./hooks/useModelManagement";
11
+ export * from "./hooks/useLB";
10
12
  export * from "./components/AiPromptPanel";
11
13
  export * from "./components/AiModelSelect";
12
14
  export * from "./components/AiInput";
@@ -17,6 +19,8 @@ export * from "./components/AiImageButton";
17
19
  export * from "./components/AiContextButton";
18
20
  export * from "./components/AiSettingsButton";
19
21
  export * from "./components/AiStatusButton";
22
+ export * from "./components/LBConnectButton";
23
+ export * from "./components/LBKeyPicker";
20
24
  export * from "./components/ErrorToast";
21
25
  export * from "./components/UsageToast";
22
26
  export * from "./utils/modelManagement";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,SAAS,CAAC;AAGxB,cAAc,sBAAsB,CAAC;AAGrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,4BAA4B,CAAC;AAG3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAG5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AAGxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,eAAe,CAAC;AAG9B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,kCAAkC,CAAC;AACjD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,SAAS,CAAC;AAGxB,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AAGzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,eAAe,CAAC;AAG9B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,0BAA0B,CAAC;AAGzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AAGxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,eAAe,CAAC;AAG9B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,kCAAkC,CAAC;AACjD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC"}
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  export * from "./types";
3
3
  // Context
4
4
  export * from "./context/AiProvider";
5
+ export * from "./context/LBAuthProvider";
5
6
  // Hooks
6
7
  export * from "./hooks/useAiClient";
7
8
  export * from "./hooks/useAiModels";
@@ -10,6 +11,7 @@ export * from "./hooks/useAiCallText";
10
11
  export * from "./hooks/useAiCallImage";
11
12
  export * from "./hooks/usePrompts";
12
13
  export * from "./hooks/useModelManagement";
14
+ export * from "./hooks/useLB";
13
15
  // Components
14
16
  export * from "./components/AiPromptPanel";
15
17
  export * from "./components/AiModelSelect";
@@ -21,6 +23,8 @@ export * from "./components/AiImageButton";
21
23
  export * from "./components/AiContextButton";
22
24
  export * from "./components/AiSettingsButton";
23
25
  export * from "./components/AiStatusButton";
26
+ export * from "./components/LBConnectButton";
27
+ export * from "./components/LBKeyPicker";
24
28
  // Toast system
25
29
  export * from "./components/ErrorToast";
26
30
  export * from "./components/UsageToast";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastbrain/ai-ui-react",
3
- "version": "1.0.38",
3
+ "version": "1.0.39",
4
4
  "description": "Headless React components for LastBrain AI UI Kit",
5
5
  "private": false,
6
6
  "type": "module",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "lucide-react": "^0.257.0",
51
- "@lastbrain/ai-ui-core": "1.0.28"
51
+ "@lastbrain/ai-ui-core": "1.0.29"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/react": "^19.2.0",
@@ -424,7 +424,7 @@ function AiPromptPanelInternal({
424
424
  position: "sticky",
425
425
  top: 0,
426
426
  zIndex: 5,
427
- background: "var(--ai-bg-primary, #1f2937)",
427
+
428
428
  borderBottom: "1px solid var(--ai-border-primary, #374151)",
429
429
  backdropFilter: "blur(8px)",
430
430
  }}
@@ -1003,7 +1003,7 @@ function AiPromptPanelInternal({
1003
1003
  position: "sticky",
1004
1004
  bottom: 0,
1005
1005
  zIndex: 5,
1006
- background: "var(--ai-bg-primary, #1f2937)",
1006
+
1007
1007
  borderTop: "1px solid var(--ai-border-primary, #374151)",
1008
1008
  backdropFilter: "blur(8px)",
1009
1009
  }}
@@ -1066,7 +1066,7 @@ function AiPromptPanelInternal({
1066
1066
  overflow: "hidden",
1067
1067
  display: "flex",
1068
1068
  flexDirection: "column",
1069
- background: "var(--ai-bg-primary, #1f2937)",
1069
+
1070
1070
  boxShadow: "0 20px 40px rgba(0, 0, 0, 0.5)",
1071
1071
  }}
1072
1072
  >
@@ -1250,9 +1250,8 @@ function AiPromptPanelInternal({
1250
1250
  style={{
1251
1251
  fontWeight: "600",
1252
1252
  fontSize: "15px",
1253
- color: isActive
1254
- ? "#10b981"
1255
- : "var(--ai-text-primary, #f3f4f6)",
1253
+ color: "var(--ai-text-secondary, #6b7280)",
1254
+
1256
1255
  letterSpacing: "-0.01em",
1257
1256
  }}
1258
1257
  >
@@ -0,0 +1,230 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Bouton de connexion LastBrain
5
+ * Ouvre la modal d'authentification
6
+ */
7
+
8
+ import React from "react";
9
+ import { useLB } from "../context/LBAuthProvider";
10
+
11
+ interface LBConnectButtonProps {
12
+ /** Texte du bouton */
13
+ label?: string;
14
+ /** Classe CSS personnalisée */
15
+ className?: string;
16
+ /** Callback après connexion réussie */
17
+ onConnected?: () => void;
18
+ /** Callback à l'ouverture de la modal */
19
+ onOpenModal?: () => void;
20
+ }
21
+
22
+ export function LBConnectButton({
23
+ label = "Se connecter à LastBrain",
24
+ className = "",
25
+ onConnected,
26
+ onOpenModal,
27
+ }: LBConnectButtonProps) {
28
+ const { status, user, logout } = useLB();
29
+ const [showModal, setShowModal] = React.useState(false);
30
+
31
+ const handleClick = () => {
32
+ if (status === "ready" && user) {
33
+ // Déjà connecté, proposer de se déconnecter
34
+ logout();
35
+ } else {
36
+ // Pas connecté, ouvrir la modal
37
+ setShowModal(true);
38
+ onOpenModal?.();
39
+ }
40
+ };
41
+
42
+ const handleModalClose = (success: boolean) => {
43
+ setShowModal(false);
44
+ if (success) {
45
+ onConnected?.();
46
+ }
47
+ };
48
+
49
+ return (
50
+ <>
51
+ <button
52
+ onClick={handleClick}
53
+ className={
54
+ className ||
55
+ "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
56
+ }
57
+ disabled={status === "loading"}
58
+ >
59
+ {status === "loading"
60
+ ? "Chargement..."
61
+ : status === "ready" && user
62
+ ? `Connecté (${user.email})`
63
+ : label}
64
+ </button>
65
+
66
+ {showModal && <LBAuthModal onClose={handleModalClose} />}
67
+ </>
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Modal d'authentification LastBrain
73
+ */
74
+ interface LBAuthModalProps {
75
+ onClose: (success: boolean) => void;
76
+ }
77
+
78
+ function LBAuthModal({ onClose }: LBAuthModalProps) {
79
+ const { login, fetchApiKeys, selectApiKey, status } = useLB();
80
+ const [step, setStep] = React.useState<"login" | "select-key">("login");
81
+ const [email, setEmail] = React.useState("");
82
+ const [password, setPassword] = React.useState("");
83
+ const [error, setError] = React.useState("");
84
+ const [loading, setLoading] = React.useState(false);
85
+ const [accessToken, setAccessToken] = React.useState("");
86
+ const [apiKeys, setApiKeys] = React.useState<any[]>([]);
87
+
88
+ const handleLogin = async (e: React.FormEvent) => {
89
+ e.preventDefault();
90
+ setError("");
91
+ setLoading(true);
92
+
93
+ try {
94
+ const result = await login(email, password);
95
+ setAccessToken(result.accessToken);
96
+
97
+ // Récupérer les clés API
98
+ const keys = await fetchApiKeys(result.accessToken);
99
+ setApiKeys(keys);
100
+
101
+ if (keys.length === 0) {
102
+ setError(
103
+ "Aucune clé API active trouvée. Créez-en une dans votre dashboard LastBrain."
104
+ );
105
+ return;
106
+ }
107
+
108
+ // Passer à l'étape de sélection
109
+ setStep("select-key");
110
+ } catch (err) {
111
+ setError(err instanceof Error ? err.message : "Échec de la connexion");
112
+ } finally {
113
+ setLoading(false);
114
+ }
115
+ };
116
+
117
+ const handleSelectKey = async (apiKeyId: string) => {
118
+ setError("");
119
+ setLoading(true);
120
+
121
+ try {
122
+ await selectApiKey(accessToken, apiKeyId);
123
+ onClose(true); // Succès
124
+ } catch (err) {
125
+ setError(err instanceof Error ? err.message : "Échec de la sélection");
126
+ } finally {
127
+ setLoading(false);
128
+ }
129
+ };
130
+
131
+ return (
132
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
133
+ <div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
134
+ <div className="flex justify-between items-center mb-4">
135
+ <h2 className="text-xl font-bold">
136
+ {step === "login"
137
+ ? "Connexion LastBrain"
138
+ : "Sélectionner une clé API"}
139
+ </h2>
140
+ <button
141
+ onClick={() => onClose(false)}
142
+ className="text-gray-500 hover:text-gray-700"
143
+ >
144
+
145
+ </button>
146
+ </div>
147
+
148
+ {step === "login" ? (
149
+ <form onSubmit={handleLogin} className="space-y-4">
150
+ <div>
151
+ <label className="block text-sm font-medium mb-1">Email</label>
152
+ <input
153
+ type="email"
154
+ value={email}
155
+ onChange={(e) => setEmail(e.target.value)}
156
+ className="w-full px-3 py-2 border rounded dark:bg-gray-700"
157
+ required
158
+ autoFocus
159
+ />
160
+ </div>
161
+
162
+ <div>
163
+ <label className="block text-sm font-medium mb-1">
164
+ Mot de passe
165
+ </label>
166
+ <input
167
+ type="password"
168
+ value={password}
169
+ onChange={(e) => setPassword(e.target.value)}
170
+ className="w-full px-3 py-2 border rounded dark:bg-gray-700"
171
+ required
172
+ />
173
+ </div>
174
+
175
+ {error && <div className="text-red-600 text-sm">{error}</div>}
176
+
177
+ <button
178
+ type="submit"
179
+ disabled={loading}
180
+ className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
181
+ >
182
+ {loading ? "Connexion..." : "Se connecter"}
183
+ </button>
184
+
185
+ <p className="text-sm text-gray-600 dark:text-gray-400 text-center">
186
+ Pas encore de compte ?{" "}
187
+ <a
188
+ href="https://lastbrain.io/signup"
189
+ target="_blank"
190
+ rel="noopener noreferrer"
191
+ className="text-blue-600 hover:underline"
192
+ >
193
+ Créer un compte
194
+ </a>
195
+ </p>
196
+ </form>
197
+ ) : (
198
+ <div className="space-y-4">
199
+ <p className="text-sm text-gray-600 dark:text-gray-400">
200
+ Sélectionnez une clé API pour créer une session de 72h
201
+ </p>
202
+
203
+ <div className="space-y-2">
204
+ {apiKeys.map((key) => (
205
+ <button
206
+ key={key.id}
207
+ onClick={() => handleSelectKey(key.id)}
208
+ disabled={!key.isActive || loading}
209
+ className="w-full text-left px-4 py-3 border rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
210
+ >
211
+ <div className="font-medium">{key.name}</div>
212
+ <div className="text-sm text-gray-500">
213
+ {key.keyPrefix}...
214
+ </div>
215
+ {!key.isActive && (
216
+ <div className="text-xs text-red-600">Inactive</div>
217
+ )}
218
+ </button>
219
+ ))}
220
+ </div>
221
+
222
+ {error && <div className="text-red-600 text-sm">{error}</div>}
223
+ </div>
224
+ )}
225
+ </div>
226
+ </div>
227
+ );
228
+ }
229
+
230
+ export { LBAuthModal };
@@ -0,0 +1,145 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Composant LBKeyPicker
5
+ * Permet de changer de clé API sans se reconnecter
6
+ */
7
+
8
+ import { useEffect, useState } from "react";
9
+ import { useLB } from "../hooks/useLB";
10
+ import type { LBApiKey } from "@lastbrain/ai-ui-core";
11
+
12
+ interface LBKeyPickerProps {
13
+ /** Classe CSS personnalisée */
14
+ className?: string;
15
+ /** Callback après changement de clé */
16
+ onKeyChanged?: (key: LBApiKey) => void;
17
+ }
18
+
19
+ export function LBKeyPicker({
20
+ className = "",
21
+ onKeyChanged,
22
+ }: LBKeyPickerProps) {
23
+ const { status, selectedKey, fetchApiKeys, selectApiKey, accessToken } =
24
+ useLB();
25
+ const [apiKeys, setApiKeys] = useState<LBApiKey[]>([]);
26
+ const [loading, setLoading] = useState(false);
27
+ const [error, setError] = useState("");
28
+ const [showDropdown, setShowDropdown] = useState(false);
29
+
30
+ useEffect(() => {
31
+ if (status === "ready" && accessToken) {
32
+ loadKeys();
33
+ }
34
+ }, [status, accessToken]);
35
+
36
+ const loadKeys = async () => {
37
+ if (!accessToken) return;
38
+
39
+ try {
40
+ setLoading(true);
41
+ const keys = await fetchApiKeys(accessToken);
42
+ setApiKeys(keys);
43
+ } catch (err) {
44
+ setError("Impossible de charger les clés API");
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ };
49
+
50
+ const handleSelectKey = async (keyId: string) => {
51
+ if (!accessToken) return;
52
+
53
+ try {
54
+ setLoading(true);
55
+ setError("");
56
+ await selectApiKey(accessToken, keyId);
57
+ const selectedApiKey = apiKeys.find((k) => k.id === keyId);
58
+ if (selectedApiKey) {
59
+ onKeyChanged?.(selectedApiKey);
60
+ }
61
+ setShowDropdown(false);
62
+ } catch (err) {
63
+ setError(
64
+ err instanceof Error ? err.message : "Échec du changement de clé"
65
+ );
66
+ } finally {
67
+ setLoading(false);
68
+ }
69
+ };
70
+
71
+ if (status !== "ready" || !selectedKey) {
72
+ return null;
73
+ }
74
+
75
+ return (
76
+ <div className={`relative ${className}`}>
77
+ <button
78
+ onClick={() => setShowDropdown(!showDropdown)}
79
+ disabled={loading}
80
+ className="px-4 py-2 border rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 flex items-center gap-2"
81
+ >
82
+ <span className="font-mono text-sm">{selectedKey.keyPrefix}...</span>
83
+ <svg
84
+ className="w-4 h-4"
85
+ fill="none"
86
+ stroke="currentColor"
87
+ viewBox="0 0 24 24"
88
+ >
89
+ <path
90
+ strokeLinecap="round"
91
+ strokeLinejoin="round"
92
+ strokeWidth={2}
93
+ d="M19 9l-7 7-7-7"
94
+ />
95
+ </svg>
96
+ </button>
97
+
98
+ {showDropdown && (
99
+ <>
100
+ <div
101
+ className="fixed inset-0 z-10"
102
+ onClick={() => setShowDropdown(false)}
103
+ />
104
+ <div className="absolute right-0 mt-2 w-64 bg-white dark:bg-gray-800 border rounded-lg shadow-lg z-20">
105
+ <div className="p-2">
106
+ <div className="text-sm font-medium text-gray-700 dark:text-gray-300 px-3 py-2">
107
+ Changer de clé API
108
+ </div>
109
+ <div className="space-y-1">
110
+ {apiKeys.map((key) => (
111
+ <button
112
+ key={key.id}
113
+ onClick={() => handleSelectKey(key.id)}
114
+ disabled={
115
+ !key.isActive || loading || key.id === selectedKey.id
116
+ }
117
+ className={`w-full text-left px-3 py-2 rounded text-sm ${
118
+ key.id === selectedKey.id
119
+ ? "bg-blue-100 dark:bg-blue-900"
120
+ : "hover:bg-gray-100 dark:hover:bg-gray-700"
121
+ } disabled:opacity-50 disabled:cursor-not-allowed`}
122
+ >
123
+ <div className="font-medium">{key.name}</div>
124
+ <div className="text-xs text-gray-500 font-mono">
125
+ {key.keyPrefix}...
126
+ </div>
127
+ {!key.isActive && (
128
+ <div className="text-xs text-red-600">Inactive</div>
129
+ )}
130
+ </button>
131
+ ))}
132
+ </div>
133
+ </div>
134
+
135
+ {error && (
136
+ <div className="px-3 py-2 text-xs text-red-600 border-t">
137
+ {error}
138
+ </div>
139
+ )}
140
+ </div>
141
+ </>
142
+ )}
143
+ </div>
144
+ );
145
+ }
@@ -0,0 +1,269 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Provider d'authentification LastBrain
5
+ * Gère l'état de connexion, la session et les appels IA
6
+ */
7
+
8
+ import {
9
+ createContext,
10
+ useContext,
11
+ useEffect,
12
+ useCallback,
13
+ useState,
14
+ type ReactNode,
15
+ } from "react";
16
+ import type {
17
+ LBAuthState,
18
+ LBApiKey,
19
+ LBSession,
20
+ LBLoginResult,
21
+ LBSessionResult,
22
+ } from "@lastbrain/ai-ui-core";
23
+
24
+ interface LBProviderProps {
25
+ children: ReactNode;
26
+ /** URL de l'API LastBrain (ex: https://api.lastbrain.io) */
27
+ baseUrl?: string;
28
+ /** URL du proxy local (ex: /api/lb) */
29
+ proxyUrl?: string;
30
+ /** Fonction appelée lors des changements d'état */
31
+ onStatusChange?: (status: LBAuthState["status"]) => void;
32
+ }
33
+
34
+ interface LBContextValue extends LBAuthState {
35
+ /** Fonction de connexion */
36
+ login: (email: string, password: string) => Promise<LBLoginResult>;
37
+ /** Fonction de déconnexion */
38
+ logout: () => Promise<void>;
39
+ /** Récupère les clés API de l'utilisateur */
40
+ fetchApiKeys: (accessToken: string) => Promise<LBApiKey[]>;
41
+ /** Sélectionne une clé API et crée une session */
42
+ selectApiKey: (accessToken: string, apiKeyId: string) => Promise<void>;
43
+ /** Recharge l'état de la session */
44
+ refreshSession: () => Promise<void>;
45
+ /** Clés API disponibles */
46
+ apiKeys: LBApiKey[];
47
+ /** Access token temporaire (après login) */
48
+ accessToken?: string;
49
+ }
50
+
51
+ const LBContext = createContext<LBContextValue | undefined>(undefined);
52
+
53
+ export function LBProvider({
54
+ children,
55
+ baseUrl: _baseUrl = "/api/lb",
56
+ proxyUrl = "/api/lb",
57
+ onStatusChange,
58
+ }: LBProviderProps) {
59
+ const [state, setState] = useState<LBAuthState>({
60
+ status: "loading",
61
+ });
62
+ const [apiKeys, setApiKeys] = useState<LBApiKey[]>([]);
63
+ const [accessToken, setAccessToken] = useState<string>();
64
+
65
+ /**
66
+ * Vérifie si une session existe au chargement
67
+ */
68
+ const checkSession = useCallback(async () => {
69
+ try {
70
+ const response = await fetch(`${proxyUrl}/auth/session/verify`, {
71
+ credentials: "include",
72
+ });
73
+
74
+ if (response.ok) {
75
+ const session: LBSession = await response.json();
76
+ setState({
77
+ status: "ready",
78
+ session,
79
+ user: {
80
+ id: session.userId,
81
+ email: "", // Sera rempli par une autre requête si nécessaire
82
+ },
83
+ });
84
+ onStatusChange?.("ready");
85
+ } else {
86
+ setState({ status: "needs_auth" });
87
+ onStatusChange?.("needs_auth");
88
+ }
89
+ } catch (error) {
90
+ console.error("[LBProvider] Session check failed:", error);
91
+ setState({ status: "needs_auth" });
92
+ onStatusChange?.("needs_auth");
93
+ }
94
+ }, [proxyUrl, onStatusChange]);
95
+
96
+ useEffect(() => {
97
+ checkSession();
98
+ }, [checkSession]);
99
+
100
+ /**
101
+ * Connexion utilisateur
102
+ */
103
+ const login = useCallback(
104
+ async (email: string, password: string): Promise<LBLoginResult> => {
105
+ try {
106
+ setState((prev: LBAuthState) => ({ ...prev, status: "loading" }));
107
+
108
+ const response = await fetch(`${proxyUrl}/auth/login`, {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json" },
111
+ body: JSON.stringify({ email, password }),
112
+ credentials: "include",
113
+ });
114
+
115
+ if (!response.ok) {
116
+ const error = await response.json();
117
+ throw new Error(error.message || "Login failed");
118
+ }
119
+
120
+ const result: LBLoginResult = await response.json();
121
+ setAccessToken(result.accessToken);
122
+
123
+ setState({
124
+ status: "ready",
125
+ user: result.user,
126
+ });
127
+
128
+ return result;
129
+ } catch (error) {
130
+ const message = error instanceof Error ? error.message : "Login failed";
131
+ setState({
132
+ status: "error",
133
+ error: message,
134
+ });
135
+ throw error;
136
+ }
137
+ },
138
+ [proxyUrl]
139
+ );
140
+
141
+ /**
142
+ * Récupère les clés API de l'utilisateur
143
+ */
144
+ const fetchApiKeys = useCallback(
145
+ async (token: string): Promise<LBApiKey[]> => {
146
+ try {
147
+ const response = await fetch(`${proxyUrl}/user/api-keys`, {
148
+ headers: {
149
+ Authorization: `Bearer ${token}`,
150
+ },
151
+ credentials: "include",
152
+ });
153
+
154
+ if (!response.ok) {
155
+ throw new Error("Failed to fetch API keys");
156
+ }
157
+
158
+ const keys: LBApiKey[] = await response.json();
159
+ setApiKeys(keys);
160
+ return keys;
161
+ } catch (error) {
162
+ console.error("[LBProvider] Failed to fetch API keys:", error);
163
+ throw error;
164
+ }
165
+ },
166
+ [proxyUrl]
167
+ );
168
+
169
+ /**
170
+ * Sélectionne une clé API et crée une session
171
+ */
172
+ const selectApiKey = useCallback(
173
+ async (token: string, apiKeyId: string): Promise<void> => {
174
+ try {
175
+ setState((prev: LBAuthState) => ({ ...prev, status: "loading" }));
176
+
177
+ const response = await fetch(`${proxyUrl}/auth/session`, {
178
+ method: "POST",
179
+ headers: {
180
+ "Content-Type": "application/json",
181
+ Authorization: `Bearer ${token}`,
182
+ },
183
+ body: JSON.stringify({ api_key_id: apiKeyId }),
184
+ credentials: "include",
185
+ });
186
+
187
+ if (!response.ok) {
188
+ throw new Error("Failed to create session");
189
+ }
190
+
191
+ const sessionResult: LBSessionResult = await response.json();
192
+
193
+ setState({
194
+ status: "ready",
195
+ user: state.user,
196
+ selectedKey: sessionResult.apiKey,
197
+ session: {
198
+ sessionToken: sessionResult.sessionToken,
199
+ userId: state.user?.id || "",
200
+ apiKeyId,
201
+ expiresAt: Date.now() + sessionResult.expiresIn * 1000,
202
+ },
203
+ });
204
+
205
+ setAccessToken(undefined); // Nettoyer l'access token temporaire
206
+ onStatusChange?.("ready");
207
+ } catch (error) {
208
+ const message =
209
+ error instanceof Error ? error.message : "Failed to select API key";
210
+ setState({
211
+ status: "error",
212
+ error: message,
213
+ });
214
+ throw error;
215
+ }
216
+ },
217
+ [proxyUrl, state.user, onStatusChange]
218
+ );
219
+
220
+ /**
221
+ * Déconnexion
222
+ */
223
+ const logout = useCallback(async (): Promise<void> => {
224
+ try {
225
+ await fetch(`${proxyUrl}/auth/session/logout`, {
226
+ method: "POST",
227
+ credentials: "include",
228
+ });
229
+ } catch (error) {
230
+ console.error("[LBProvider] Logout failed:", error);
231
+ } finally {
232
+ setState({ status: "needs_auth" });
233
+ setApiKeys([]);
234
+ setAccessToken(undefined);
235
+ onStatusChange?.("needs_auth");
236
+ }
237
+ }, [proxyUrl, onStatusChange]);
238
+
239
+ /**
240
+ * Recharge la session
241
+ */
242
+ const refreshSession = useCallback(async (): Promise<void> => {
243
+ await checkSession();
244
+ }, [checkSession]);
245
+
246
+ const value: LBContextValue = {
247
+ ...state,
248
+ login,
249
+ logout,
250
+ fetchApiKeys,
251
+ selectApiKey,
252
+ refreshSession,
253
+ apiKeys,
254
+ accessToken,
255
+ };
256
+
257
+ return <LBContext.Provider value={value}>{children}</LBContext.Provider>;
258
+ }
259
+
260
+ /**
261
+ * Hook pour accéder au contexte LastBrain
262
+ */
263
+ export function useLB(): LBContextValue {
264
+ const context = useContext(LBContext);
265
+ if (!context) {
266
+ throw new Error("useLB must be used within LBProvider");
267
+ }
268
+ return context;
269
+ }
@@ -0,0 +1,7 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Hook useLB - Export du hook depuis LBAuthProvider
5
+ */
6
+
7
+ export { useLB } from "../context/LBAuthProvider";
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export * from "./types";
3
3
 
4
4
  // Context
5
5
  export * from "./context/AiProvider";
6
+ export * from "./context/LBAuthProvider";
6
7
 
7
8
  // Hooks
8
9
  export * from "./hooks/useAiClient";
@@ -12,6 +13,7 @@ export * from "./hooks/useAiCallText";
12
13
  export * from "./hooks/useAiCallImage";
13
14
  export * from "./hooks/usePrompts";
14
15
  export * from "./hooks/useModelManagement";
16
+ export * from "./hooks/useLB";
15
17
 
16
18
  // Components
17
19
  export * from "./components/AiPromptPanel";
@@ -24,6 +26,8 @@ export * from "./components/AiImageButton";
24
26
  export * from "./components/AiContextButton";
25
27
  export * from "./components/AiSettingsButton";
26
28
  export * from "./components/AiStatusButton";
29
+ export * from "./components/LBConnectButton";
30
+ export * from "./components/LBKeyPicker";
27
31
 
28
32
  // Toast system
29
33
  export * from "./components/ErrorToast";