@lastbrain/ai-ui-react 1.0.43 → 1.0.45

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.
@@ -1 +1 @@
1
- {"version":3,"file":"AiStatusButton.d.ts","sourceRoot":"","sources":["../../src/components/AiStatusButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAetD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,OAAe,EACf,SAAc,GACf,EAAE,mBAAmB,2CA41BrB"}
1
+ {"version":3,"file":"AiStatusButton.d.ts","sourceRoot":"","sources":["../../src/components/AiStatusButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAetD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,OAAe,EACf,SAAc,GACf,EAAE,mBAAmB,2CAk2BrB"}
@@ -194,7 +194,7 @@ export function AiStatusButton({ status, loading = false, className = "", }) {
194
194
  gap: "8px",
195
195
  borderTop: "1px solid var(--ai-border-primary, #374151)",
196
196
  paddingTop: "12px",
197
- }, children: [_jsx("button", { onClick: () => window.open("https://lastbrain.io/metrics", "_blank"), style: {
197
+ }, children: [_jsx("button", { onClick: () => window.open("https://prompt.lastbrain.io/metrics", "_blank"), style: {
198
198
  flex: 1,
199
199
  background: "transparent",
200
200
  border: "none",
@@ -213,7 +213,7 @@ export function AiStatusButton({ status, loading = false, className = "", }) {
213
213
  Object.assign(e.currentTarget.style, {
214
214
  background: "transparent",
215
215
  });
216
- }, title: "View Metrics", children: _jsx(BarChart3, { size: 18 }) }), _jsx("button", { onClick: () => window.open("https://lastbrain.io/settings", "_blank"), style: {
216
+ }, title: "View Metrics", children: _jsx(BarChart3, { size: 18 }) }), _jsx("button", { onClick: () => window.open("https://prompt.lastbrain.io/settings", "_blank"), style: {
217
217
  flex: 1,
218
218
  background: "transparent",
219
219
  border: "none",
@@ -0,0 +1,10 @@
1
+ import type { LBApiKey } from "@lastbrain/ai-ui-core";
2
+ interface LBApiKeySelectorProps {
3
+ apiKeys: LBApiKey[];
4
+ onSelect: (apiKeyId: string) => Promise<void>;
5
+ onCancel: () => void;
6
+ isOpen: boolean;
7
+ }
8
+ export declare function LBApiKeySelector({ apiKeys, onSelect, onCancel, isOpen, }: LBApiKeySelectorProps): import("react/jsx-runtime").JSX.Element | null;
9
+ export {};
10
+ //# sourceMappingURL=LBApiKeySelector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LBApiKeySelector.d.ts","sourceRoot":"","sources":["../../src/components/LBApiKeySelector.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEtD,UAAU,qBAAqB;IAC7B,OAAO,EAAE,QAAQ,EAAE,CAAC;IACpB,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,MAAM,GACP,EAAE,qBAAqB,kDA0TvB"}
@@ -0,0 +1,193 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ export function LBApiKeySelector({ apiKeys, onSelect, onCancel, isOpen, }) {
4
+ const [selectedKeyId, setSelectedKeyId] = useState(apiKeys.find((k) => k.isActive)?.id || apiKeys[0]?.id || "");
5
+ const [loading, setLoading] = useState(false);
6
+ const [error, setError] = useState("");
7
+ if (!isOpen)
8
+ return null;
9
+ const handleSubmit = async (e) => {
10
+ e.preventDefault();
11
+ if (!selectedKeyId) {
12
+ setError("Veuillez sélectionner une clé API");
13
+ return;
14
+ }
15
+ setLoading(true);
16
+ setError("");
17
+ try {
18
+ await onSelect(selectedKeyId);
19
+ }
20
+ catch (err) {
21
+ setError(err instanceof Error ? err.message : "Erreur lors de la sélection");
22
+ setLoading(false);
23
+ }
24
+ };
25
+ return (_jsxs("div", { style: {
26
+ position: "fixed",
27
+ top: 0,
28
+ left: 0,
29
+ right: 0,
30
+ bottom: 0,
31
+ zIndex: 10000,
32
+ display: "flex",
33
+ alignItems: "center",
34
+ justifyContent: "center",
35
+ padding: "16px",
36
+ }, onClick: onCancel, children: [_jsx("div", { style: {
37
+ position: "absolute",
38
+ top: 0,
39
+ left: 0,
40
+ right: 0,
41
+ bottom: 0,
42
+ background: "var(--ai-overlay-bg, rgba(0, 0, 0, 0.75))",
43
+ backdropFilter: "blur(8px)",
44
+ } }), _jsxs("div", { style: {
45
+ position: "relative",
46
+ background: "var(--ai-modal-bg, #1f2937)",
47
+ border: "1px solid var(--ai-border-primary, #374151)",
48
+ borderRadius: "12px",
49
+ padding: "24px",
50
+ maxWidth: "500px",
51
+ width: "100%",
52
+ boxShadow: "0 20px 50px rgba(0, 0, 0, 0.5)",
53
+ }, onClick: (e) => e.stopPropagation(), children: [_jsx("h2", { style: {
54
+ margin: "0 0 8px 0",
55
+ fontSize: "20px",
56
+ fontWeight: 600,
57
+ color: "var(--ai-text-primary, #f9fafb)",
58
+ textAlign: "center",
59
+ }, children: "S\u00E9lectionnez une cl\u00E9 API" }), _jsx("p", { style: {
60
+ margin: "0 0 24px 0",
61
+ fontSize: "14px",
62
+ color: "var(--ai-text-secondary, #9ca3af)",
63
+ textAlign: "center",
64
+ lineHeight: "1.5",
65
+ }, children: "Choisissez la cl\u00E9 API \u00E0 utiliser pour vos requ\u00EAtes IA" }), _jsxs("form", { onSubmit: handleSubmit, children: [_jsx("div", { style: {
66
+ display: "flex",
67
+ flexDirection: "column",
68
+ gap: "8px",
69
+ marginBottom: "24px",
70
+ maxHeight: "300px",
71
+ overflowY: "auto",
72
+ }, children: apiKeys.map((key) => {
73
+ const isSelected = key.id === selectedKeyId;
74
+ const isActive = key.isActive;
75
+ return (_jsxs("label", { style: {
76
+ display: "flex",
77
+ alignItems: "center",
78
+ padding: "12px 16px",
79
+ background: isSelected
80
+ ? "var(--ai-input-bg-focus, #374151)"
81
+ : "var(--ai-input-bg, #111827)",
82
+ border: `2px solid ${isSelected
83
+ ? "var(--ai-accent-primary, #8b5cf6)"
84
+ : "var(--ai-border-primary, #374151)"}`,
85
+ borderRadius: "8px",
86
+ cursor: isActive ? "pointer" : "not-allowed",
87
+ opacity: isActive ? 1 : 0.5,
88
+ transition: "all 0.2s ease",
89
+ }, onMouseEnter: (e) => {
90
+ if (isActive && !isSelected) {
91
+ e.currentTarget.style.borderColor =
92
+ "var(--ai-border-hover, #4b5563)";
93
+ e.currentTarget.style.background =
94
+ "var(--ai-input-bg-focus, #374151)";
95
+ }
96
+ }, onMouseLeave: (e) => {
97
+ if (isActive && !isSelected) {
98
+ e.currentTarget.style.borderColor =
99
+ "var(--ai-border-primary, #374151)";
100
+ e.currentTarget.style.background =
101
+ "var(--ai-input-bg, #111827)";
102
+ }
103
+ }, children: [_jsx("input", { type: "radio", name: "apiKey", value: key.id, checked: isSelected, disabled: !isActive, onChange: (e) => setSelectedKeyId(e.target.value), style: {
104
+ marginRight: "12px",
105
+ accentColor: "var(--ai-accent-primary, #8b5cf6)",
106
+ cursor: isActive ? "pointer" : "not-allowed",
107
+ } }), _jsxs("div", { style: { flex: 1 }, children: [_jsx("div", { style: {
108
+ fontSize: "14px",
109
+ fontWeight: 500,
110
+ color: "var(--ai-text-primary, #f9fafb)",
111
+ marginBottom: "4px",
112
+ }, children: key.name }), _jsx("div", { style: {
113
+ fontSize: "12px",
114
+ color: "var(--ai-text-secondary, #9ca3af)",
115
+ fontFamily: "monospace",
116
+ }, children: key.keyPrefix || key.id.substring(0, 12) + "..." })] }), isActive ? (_jsx("div", { style: {
117
+ fontSize: "11px",
118
+ padding: "4px 8px",
119
+ borderRadius: "4px",
120
+ background: "rgba(16, 185, 129, 0.1)",
121
+ color: "#10b981",
122
+ fontWeight: 600,
123
+ }, children: "Active" })) : (_jsx("div", { style: {
124
+ fontSize: "11px",
125
+ padding: "4px 8px",
126
+ borderRadius: "4px",
127
+ background: "rgba(239, 68, 68, 0.1)",
128
+ color: "#ef4444",
129
+ fontWeight: 600,
130
+ }, children: "Inactive" }))] }, key.id));
131
+ }) }), error && (_jsx("div", { style: {
132
+ padding: "12px",
133
+ background: "rgba(239, 68, 68, 0.1)",
134
+ border: "1px solid rgba(239, 68, 68, 0.3)",
135
+ borderRadius: "6px",
136
+ marginBottom: "16px",
137
+ }, children: _jsx("p", { style: {
138
+ margin: 0,
139
+ fontSize: "13px",
140
+ color: "#ef4444",
141
+ lineHeight: "1.5",
142
+ }, children: error }) })), _jsxs("div", { style: { display: "flex", gap: "12px" }, children: [_jsx("button", { type: "button", onClick: onCancel, disabled: loading, style: {
143
+ flex: 1,
144
+ padding: "12px",
145
+ background: "transparent",
146
+ border: "1px solid var(--ai-border-primary, #374151)",
147
+ borderRadius: "8px",
148
+ color: "var(--ai-text-secondary, #9ca3af)",
149
+ fontSize: "14px",
150
+ fontWeight: 600,
151
+ cursor: loading ? "not-allowed" : "pointer",
152
+ opacity: loading ? 0.5 : 1,
153
+ transition: "all 0.2s ease",
154
+ }, onMouseEnter: (e) => {
155
+ if (!loading) {
156
+ e.currentTarget.style.background =
157
+ "var(--ai-input-bg, #111827)";
158
+ e.currentTarget.style.borderColor =
159
+ "var(--ai-border-hover, #4b5563)";
160
+ }
161
+ }, onMouseLeave: (e) => {
162
+ if (!loading) {
163
+ e.currentTarget.style.background = "transparent";
164
+ e.currentTarget.style.borderColor =
165
+ "var(--ai-border-primary, #374151)";
166
+ }
167
+ }, children: "Annuler" }), _jsx("button", { type: "submit", disabled: loading || !selectedKeyId, style: {
168
+ flex: 1,
169
+ padding: "12px",
170
+ background: loading
171
+ ? "var(--ai-input-bg, #111827)"
172
+ : "linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)",
173
+ border: "none",
174
+ borderRadius: "8px",
175
+ color: "#ffffff",
176
+ fontSize: "14px",
177
+ fontWeight: 600,
178
+ cursor: loading || !selectedKeyId ? "not-allowed" : "pointer",
179
+ opacity: loading || !selectedKeyId ? 0.5 : 1,
180
+ transition: "all 0.2s ease",
181
+ }, onMouseEnter: (e) => {
182
+ if (!loading && selectedKeyId) {
183
+ e.currentTarget.style.transform = "translateY(-1px)";
184
+ e.currentTarget.style.boxShadow =
185
+ "0 8px 20px rgba(139, 92, 246, 0.4)";
186
+ }
187
+ }, onMouseLeave: (e) => {
188
+ if (!loading && selectedKeyId) {
189
+ e.currentTarget.style.transform = "none";
190
+ e.currentTarget.style.boxShadow = "none";
191
+ }
192
+ }, children: loading ? "Connexion..." : "Continuer" })] })] })] })] }));
193
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"LBSigninModal.d.ts","sourceRoot":"","sources":["../../src/components/LBSigninModal.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,kDAoYpE"}
1
+ {"version":3,"file":"LBSigninModal.d.ts","sourceRoot":"","sources":["../../src/components/LBSigninModal.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,kDA8bpE"}
@@ -1,13 +1,20 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useState } from "react";
3
+ import { useState, useEffect } from "react";
4
4
  import { useLB } from "../context/LBAuthProvider";
5
+ import { LBApiKeySelector } from "./LBApiKeySelector";
5
6
  export function LBSigninModal({ isOpen, onClose }) {
6
7
  // Vérifier si LBProvider est disponible
7
8
  let login;
9
+ let selectApiKeyWithToken;
10
+ let apiKeys = [];
11
+ let lbStatus;
8
12
  try {
9
13
  const lbContext = useLB();
10
14
  login = lbContext.login;
15
+ selectApiKeyWithToken = lbContext.selectApiKeyWithToken;
16
+ apiKeys = lbContext.apiKeys || [];
17
+ lbStatus = lbContext.status;
11
18
  }
12
19
  catch {
13
20
  // LBProvider n'est pas disponible, ne pas rendre le modal
@@ -17,6 +24,13 @@ export function LBSigninModal({ isOpen, onClose }) {
17
24
  const [password, setPassword] = useState("");
18
25
  const [loading, setLoading] = useState(false);
19
26
  const [error, setError] = useState("");
27
+ const [showKeySelector, setShowKeySelector] = useState(false);
28
+ // Si le status est "needs_key_selection", afficher le sélecteur
29
+ useEffect(() => {
30
+ if (lbStatus === "needs_key_selection" && apiKeys.length > 0) {
31
+ setShowKeySelector(true);
32
+ }
33
+ }, [lbStatus, apiKeys.length]);
20
34
  if (!isOpen || !login)
21
35
  return null;
22
36
  const handleSubmit = async (e) => {
@@ -26,7 +40,14 @@ export function LBSigninModal({ isOpen, onClose }) {
26
40
  try {
27
41
  const result = await login(email, password);
28
42
  if (result.success) {
29
- onClose();
43
+ if (result.needsKeySelection) {
44
+ // L'utilisateur doit choisir une clé API
45
+ setShowKeySelector(true);
46
+ }
47
+ else {
48
+ // Connexion réussie, fermer le modal
49
+ onClose();
50
+ }
30
51
  }
31
52
  else {
32
53
  setError(result.error || "Échec de la connexion");
@@ -39,6 +60,29 @@ export function LBSigninModal({ isOpen, onClose }) {
39
60
  setLoading(false);
40
61
  }
41
62
  };
63
+ const handleKeySelect = async (apiKeyId) => {
64
+ if (!selectApiKeyWithToken)
65
+ return;
66
+ try {
67
+ await selectApiKeyWithToken(apiKeyId);
68
+ setShowKeySelector(false);
69
+ onClose();
70
+ }
71
+ catch (err) {
72
+ setError(err instanceof Error ? err.message : "Erreur lors de la sélection");
73
+ setShowKeySelector(false);
74
+ }
75
+ };
76
+ const handleCancelKeySelection = () => {
77
+ setShowKeySelector(false);
78
+ setEmail("");
79
+ setPassword("");
80
+ setError("");
81
+ };
82
+ // Si on doit afficher le sélecteur de clés
83
+ if (showKeySelector && apiKeys.length > 0) {
84
+ return (_jsx(LBApiKeySelector, { apiKeys: apiKeys, onSelect: handleKeySelect, onCancel: handleCancelKeySelection, isOpen: true }));
85
+ }
42
86
  const handleKeyDown = (e) => {
43
87
  if (e.key === "Escape") {
44
88
  onClose();
@@ -18,13 +18,16 @@ interface LBContextValue extends LBAuthState {
18
18
  login: (email: string, password: string) => Promise<{
19
19
  success: boolean;
20
20
  error?: string;
21
+ needsKeySelection?: boolean;
21
22
  }>;
22
23
  /** Fonction de déconnexion */
23
24
  logout: () => Promise<void>;
24
25
  /** Récupère les clés API de l'utilisateur */
25
26
  fetchApiKeys: (accessToken: string) => Promise<LBApiKey[]>;
26
- /** Sélectionne une clé API et crée une session */
27
+ /** Sélectionne une clé API et crée une session (après login) */
27
28
  selectApiKey: (accessToken: string, apiKeyId: string) => Promise<void>;
29
+ /** Sélectionne une clé API avec le token stocké */
30
+ selectApiKeyWithToken: (apiKeyId: string) => Promise<void>;
28
31
  /** Recharge l'état de la session */
29
32
  refreshSession: () => Promise<void>;
30
33
  /** Clés API disponibles */
@@ -1 +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,EAIT,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,CACL,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,KACb,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,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,QAA2B,EACpC,QAA2B,EAC3B,cAAc,GACf,EAAE,eAAe,2CA+OjB;AAED;;GAEG;AACH,wBAAgB,KAAK,IAAI,cAAc,CAMtC"}
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,EAIT,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,CACL,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,KACb,OAAO,CAAC;QACX,OAAO,EAAE,OAAO,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC7B,CAAC,CAAC;IACH,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,gEAAgE;IAChE,YAAY,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,mDAAmD;IACnD,qBAAqB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,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,QAA2B,EACpC,QAA2B,EAC3B,cAAc,GACf,EAAE,eAAe,2CA0UjB;AAED;;GAEG;AACH,wBAAgB,KAAK,IAAI,cAAc,CAMtC"}
@@ -51,16 +51,22 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
51
51
  */
52
52
  const fetchApiKeys = useCallback(async (token) => {
53
53
  try {
54
+ console.log("[LBProvider] Fetching API keys with token:", token.substring(0, 20) + "...");
54
55
  const response = await fetch(`${proxyUrl}/public/user/api-keys`, {
55
56
  headers: {
56
57
  Authorization: `Bearer ${token}`,
58
+ "Content-Type": "application/json",
57
59
  },
58
60
  credentials: "include",
59
61
  });
62
+ console.log("[LBProvider] API keys response status:", response.status);
60
63
  if (!response.ok) {
61
- throw new Error("Failed to fetch API keys");
64
+ const errorData = await response.json().catch(() => ({}));
65
+ console.error("[LBProvider] Failed to fetch API keys:", errorData);
66
+ throw new Error(errorData.message || "Failed to fetch API keys");
62
67
  }
63
68
  const data = await response.json();
69
+ console.log("[LBProvider] API keys received:", data);
64
70
  const keys = data.apiKeys || data;
65
71
  setApiKeys(keys);
66
72
  return keys;
@@ -75,6 +81,7 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
75
81
  */
76
82
  const selectApiKey = useCallback(async (token, apiKeyId) => {
77
83
  try {
84
+ console.log("[LBProvider] Selecting API key:", apiKeyId);
78
85
  setState((prev) => ({ ...prev, status: "loading" }));
79
86
  const response = await fetch(`${proxyUrl}/auth/session`, {
80
87
  method: "POST",
@@ -85,10 +92,14 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
85
92
  body: JSON.stringify({ api_key_id: apiKeyId }),
86
93
  credentials: "include",
87
94
  });
95
+ console.log("[LBProvider] Session response status:", response.status);
88
96
  if (!response.ok) {
89
- throw new Error("Failed to create session");
97
+ const errorData = await response.json().catch(() => ({}));
98
+ console.error("[LBProvider] Failed to create session:", errorData);
99
+ throw new Error(errorData.message || "Failed to create session");
90
100
  }
91
101
  const sessionResult = await response.json();
102
+ console.log("[LBProvider] Session created successfully:", sessionResult);
92
103
  setState({
93
104
  status: "ready",
94
105
  user: state.user,
@@ -101,6 +112,7 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
101
112
  },
102
113
  });
103
114
  setAccessToken(undefined); // Nettoyer l'access token temporaire
115
+ setApiKeys([]); // Nettoyer les clés API temporaires
104
116
  onStatusChange?.("ready");
105
117
  }
106
118
  catch (error) {
@@ -113,10 +125,12 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
113
125
  }
114
126
  }, [proxyUrl, state.user, onStatusChange]);
115
127
  /**
116
- * Connexion utilisateur
128
+ * Connexion utilisateur (étape 1 : login)
129
+ * Retourne le token et les clés API sans créer de session
117
130
  */
118
131
  const login = useCallback(async (email, password) => {
119
132
  try {
133
+ console.log("[LBProvider] Login attempt:", email);
120
134
  setState((prev) => ({ ...prev, status: "loading" }));
121
135
  const response = await fetch(`${proxyUrl}/auth/login`, {
122
136
  method: "POST",
@@ -124,9 +138,11 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
124
138
  body: JSON.stringify({ email, password }),
125
139
  credentials: "include",
126
140
  });
141
+ console.log("[LBProvider] Login response status:", response.status);
127
142
  if (!response.ok) {
128
143
  const error = await response.json();
129
144
  const errorMessage = error.message || "Login failed";
145
+ console.error("[LBProvider] Login failed:", errorMessage);
130
146
  setState({
131
147
  status: "needs_auth",
132
148
  error: errorMessage,
@@ -134,41 +150,70 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
134
150
  return { success: false, error: errorMessage };
135
151
  }
136
152
  const result = await response.json();
153
+ console.log("[LBProvider] Login successful:", result.user?.email);
154
+ console.log("[LBProvider] Access token received:", result.accessToken ? "YES" : "NO");
155
+ console.log("[LBProvider] Token length:", result.accessToken?.length || 0);
137
156
  const token = result.accessToken;
157
+ if (!token) {
158
+ console.error("[LBProvider] No access token in login response!");
159
+ setState({
160
+ status: "needs_auth",
161
+ error: "Token manquant dans la réponse",
162
+ });
163
+ return { success: false, error: "Token manquant dans la réponse" };
164
+ }
138
165
  setAccessToken(token);
139
166
  setState({
140
167
  status: "loading",
141
168
  user: result.user,
142
169
  });
143
- // Récupérer les clés API et sélectionner automatiquement la première active
170
+ // Récupérer les clés API
171
+ console.log("[LBProvider] About to fetch API keys...");
144
172
  try {
145
173
  const keys = await fetchApiKeys(token);
146
- const activeKey = keys.find((k) => k.isActive);
147
- if (activeKey) {
148
- // Créer une session avec la première clé active
149
- await selectApiKey(token, activeKey.id);
150
- }
151
- else {
152
- // Aucune clé active trouvée
174
+ console.log("[LBProvider] Fetched keys:", keys.length);
175
+ if (keys.length === 0) {
176
+ console.warn("[LBProvider] No API keys found for user");
153
177
  setState({
154
178
  status: "ready",
155
179
  user: result.user,
180
+ error: "Aucune clé API disponible",
156
181
  });
182
+ return { success: true, needsKeySelection: false };
157
183
  }
158
- return { success: true };
184
+ // Si une seule clé active, la sélectionner automatiquement
185
+ const activeKeys = keys.filter((k) => k.isActive);
186
+ if (activeKeys.length === 1) {
187
+ console.log("[LBProvider] Auto-selecting single active key");
188
+ await selectApiKey(token, activeKeys[0].id);
189
+ return { success: true, needsKeySelection: false };
190
+ }
191
+ // Sinon, laisser l'utilisateur choisir
192
+ console.log("[LBProvider] Multiple keys available, user needs to select");
193
+ setState({
194
+ status: "needs_key_selection",
195
+ user: result.user,
196
+ });
197
+ return { success: true, needsKeySelection: true };
159
198
  }
160
199
  catch (keyError) {
161
- console.error("[LBProvider] Failed to fetch/select API key:", keyError);
162
- // Login réussi mais impossible de récupérer les clés
200
+ console.error("[LBProvider] Failed to fetch API keys:");
201
+ console.error("[LBProvider] Error details:", keyError);
202
+ console.error("[LBProvider] Error message:", keyError instanceof Error ? keyError.message : String(keyError));
203
+ console.error("[LBProvider] Error stack:", keyError instanceof Error ? keyError.stack : "No stack");
163
204
  setState({
164
- status: "ready",
165
- user: result.user,
205
+ status: "needs_auth",
206
+ error: "Impossible de récupérer les clés API",
166
207
  });
167
- return { success: true };
208
+ return {
209
+ success: false,
210
+ error: "Impossible de récupérer les clés API",
211
+ };
168
212
  }
169
213
  }
170
214
  catch (error) {
171
215
  const message = error instanceof Error ? error.message : "Une erreur s'est produite";
216
+ console.error("[LBProvider] Login error:", message);
172
217
  setState({
173
218
  status: "error",
174
219
  error: message,
@@ -176,6 +221,15 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
176
221
  return { success: false, error: message };
177
222
  }
178
223
  }, [proxyUrl, fetchApiKeys, selectApiKey]);
224
+ /**
225
+ * Sélectionne une clé API avec le token déjà stocké (après login)
226
+ */
227
+ const selectApiKeyWithToken = useCallback(async (apiKeyId) => {
228
+ if (!accessToken) {
229
+ throw new Error("No access token available");
230
+ }
231
+ await selectApiKey(accessToken, apiKeyId);
232
+ }, [accessToken, selectApiKey]);
179
233
  /**
180
234
  * Déconnexion
181
235
  */
@@ -208,6 +262,7 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
208
262
  logout,
209
263
  fetchApiKeys,
210
264
  selectApiKey,
265
+ selectApiKeyWithToken,
211
266
  refreshSession,
212
267
  apiKeys,
213
268
  accessToken,
package/dist/index.d.ts CHANGED
@@ -21,6 +21,7 @@ export * from "./components/AiSettingsButton";
21
21
  export * from "./components/AiStatusButton";
22
22
  export * from "./components/LBConnectButton";
23
23
  export * from "./components/LBSigninModal";
24
+ export * from "./components/LBApiKeySelector";
24
25
  export * from "./components/LBKeyPicker";
25
26
  export * from "./components/ErrorToast";
26
27
  export * from "./components/UsageToast";
@@ -1 +1 @@
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,4BAA4B,CAAC;AAC3C,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"}
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,4BAA4B,CAAC;AAC3C,cAAc,+BAA+B,CAAC;AAC9C,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
@@ -25,6 +25,7 @@ export * from "./components/AiSettingsButton";
25
25
  export * from "./components/AiStatusButton";
26
26
  export * from "./components/LBConnectButton";
27
27
  export * from "./components/LBSigninModal";
28
+ export * from "./components/LBApiKeySelector";
28
29
  export * from "./components/LBKeyPicker";
29
30
  // Toast system
30
31
  export * from "./components/ErrorToast";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastbrain/ai-ui-react",
3
- "version": "1.0.43",
3
+ "version": "1.0.45",
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.31"
51
+ "@lastbrain/ai-ui-core": "1.0.33"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/react": "^19.2.0",
@@ -394,7 +394,10 @@ export function AiStatusButton({
394
394
  >
395
395
  <button
396
396
  onClick={() =>
397
- window.open("https://lastbrain.io/metrics", "_blank")
397
+ window.open(
398
+ "https://prompt.lastbrain.io/metrics",
399
+ "_blank"
400
+ )
398
401
  }
399
402
  style={{
400
403
  flex: 1,
@@ -424,7 +427,10 @@ export function AiStatusButton({
424
427
  </button>
425
428
  <button
426
429
  onClick={() =>
427
- window.open("https://lastbrain.io/settings", "_blank")
430
+ window.open(
431
+ "https://prompt.lastbrain.io/settings",
432
+ "_blank"
433
+ )
428
434
  }
429
435
  style={{
430
436
  flex: 1,
@@ -0,0 +1,330 @@
1
+ import React, { useState } from "react";
2
+ import type { LBApiKey } from "@lastbrain/ai-ui-core";
3
+
4
+ interface LBApiKeySelectorProps {
5
+ apiKeys: LBApiKey[];
6
+ onSelect: (apiKeyId: string) => Promise<void>;
7
+ onCancel: () => void;
8
+ isOpen: boolean;
9
+ }
10
+
11
+ export function LBApiKeySelector({
12
+ apiKeys,
13
+ onSelect,
14
+ onCancel,
15
+ isOpen,
16
+ }: LBApiKeySelectorProps) {
17
+ const [selectedKeyId, setSelectedKeyId] = useState<string>(
18
+ apiKeys.find((k) => k.isActive)?.id || apiKeys[0]?.id || ""
19
+ );
20
+ const [loading, setLoading] = useState(false);
21
+ const [error, setError] = useState("");
22
+
23
+ if (!isOpen) return null;
24
+
25
+ const handleSubmit = async (e: React.FormEvent) => {
26
+ e.preventDefault();
27
+ if (!selectedKeyId) {
28
+ setError("Veuillez sélectionner une clé API");
29
+ return;
30
+ }
31
+
32
+ setLoading(true);
33
+ setError("");
34
+
35
+ try {
36
+ await onSelect(selectedKeyId);
37
+ } catch (err) {
38
+ setError(
39
+ err instanceof Error ? err.message : "Erreur lors de la sélection"
40
+ );
41
+ setLoading(false);
42
+ }
43
+ };
44
+
45
+ return (
46
+ <div
47
+ style={{
48
+ position: "fixed",
49
+ top: 0,
50
+ left: 0,
51
+ right: 0,
52
+ bottom: 0,
53
+ zIndex: 10000,
54
+ display: "flex",
55
+ alignItems: "center",
56
+ justifyContent: "center",
57
+ padding: "16px",
58
+ }}
59
+ onClick={onCancel}
60
+ >
61
+ {/* Backdrop */}
62
+ <div
63
+ style={{
64
+ position: "absolute",
65
+ top: 0,
66
+ left: 0,
67
+ right: 0,
68
+ bottom: 0,
69
+ background: "var(--ai-overlay-bg, rgba(0, 0, 0, 0.75))",
70
+ backdropFilter: "blur(8px)",
71
+ }}
72
+ />
73
+
74
+ {/* Modal */}
75
+ <div
76
+ style={{
77
+ position: "relative",
78
+ background: "var(--ai-modal-bg, #1f2937)",
79
+ border: "1px solid var(--ai-border-primary, #374151)",
80
+ borderRadius: "12px",
81
+ padding: "24px",
82
+ maxWidth: "500px",
83
+ width: "100%",
84
+ boxShadow: "0 20px 50px rgba(0, 0, 0, 0.5)",
85
+ }}
86
+ onClick={(e) => e.stopPropagation()}
87
+ >
88
+ <h2
89
+ style={{
90
+ margin: "0 0 8px 0",
91
+ fontSize: "20px",
92
+ fontWeight: 600,
93
+ color: "var(--ai-text-primary, #f9fafb)",
94
+ textAlign: "center",
95
+ }}
96
+ >
97
+ Sélectionnez une clé API
98
+ </h2>
99
+
100
+ <p
101
+ style={{
102
+ margin: "0 0 24px 0",
103
+ fontSize: "14px",
104
+ color: "var(--ai-text-secondary, #9ca3af)",
105
+ textAlign: "center",
106
+ lineHeight: "1.5",
107
+ }}
108
+ >
109
+ Choisissez la clé API à utiliser pour vos requêtes IA
110
+ </p>
111
+
112
+ <form onSubmit={handleSubmit}>
113
+ {/* Liste des clés API */}
114
+ <div
115
+ style={{
116
+ display: "flex",
117
+ flexDirection: "column",
118
+ gap: "8px",
119
+ marginBottom: "24px",
120
+ maxHeight: "300px",
121
+ overflowY: "auto",
122
+ }}
123
+ >
124
+ {apiKeys.map((key) => {
125
+ const isSelected = key.id === selectedKeyId;
126
+ const isActive = key.isActive;
127
+
128
+ return (
129
+ <label
130
+ key={key.id}
131
+ style={{
132
+ display: "flex",
133
+ alignItems: "center",
134
+ padding: "12px 16px",
135
+ background: isSelected
136
+ ? "var(--ai-input-bg-focus, #374151)"
137
+ : "var(--ai-input-bg, #111827)",
138
+ border: `2px solid ${
139
+ isSelected
140
+ ? "var(--ai-accent-primary, #8b5cf6)"
141
+ : "var(--ai-border-primary, #374151)"
142
+ }`,
143
+ borderRadius: "8px",
144
+ cursor: isActive ? "pointer" : "not-allowed",
145
+ opacity: isActive ? 1 : 0.5,
146
+ transition: "all 0.2s ease",
147
+ }}
148
+ onMouseEnter={(e) => {
149
+ if (isActive && !isSelected) {
150
+ e.currentTarget.style.borderColor =
151
+ "var(--ai-border-hover, #4b5563)";
152
+ e.currentTarget.style.background =
153
+ "var(--ai-input-bg-focus, #374151)";
154
+ }
155
+ }}
156
+ onMouseLeave={(e) => {
157
+ if (isActive && !isSelected) {
158
+ e.currentTarget.style.borderColor =
159
+ "var(--ai-border-primary, #374151)";
160
+ e.currentTarget.style.background =
161
+ "var(--ai-input-bg, #111827)";
162
+ }
163
+ }}
164
+ >
165
+ <input
166
+ type="radio"
167
+ name="apiKey"
168
+ value={key.id}
169
+ checked={isSelected}
170
+ disabled={!isActive}
171
+ onChange={(e) => setSelectedKeyId(e.target.value)}
172
+ style={{
173
+ marginRight: "12px",
174
+ accentColor: "var(--ai-accent-primary, #8b5cf6)",
175
+ cursor: isActive ? "pointer" : "not-allowed",
176
+ }}
177
+ />
178
+ <div style={{ flex: 1 }}>
179
+ <div
180
+ style={{
181
+ fontSize: "14px",
182
+ fontWeight: 500,
183
+ color: "var(--ai-text-primary, #f9fafb)",
184
+ marginBottom: "4px",
185
+ }}
186
+ >
187
+ {key.name}
188
+ </div>
189
+ <div
190
+ style={{
191
+ fontSize: "12px",
192
+ color: "var(--ai-text-secondary, #9ca3af)",
193
+ fontFamily: "monospace",
194
+ }}
195
+ >
196
+ {key.keyPrefix || key.id.substring(0, 12) + "..."}
197
+ </div>
198
+ </div>
199
+ {isActive ? (
200
+ <div
201
+ style={{
202
+ fontSize: "11px",
203
+ padding: "4px 8px",
204
+ borderRadius: "4px",
205
+ background: "rgba(16, 185, 129, 0.1)",
206
+ color: "#10b981",
207
+ fontWeight: 600,
208
+ }}
209
+ >
210
+ Active
211
+ </div>
212
+ ) : (
213
+ <div
214
+ style={{
215
+ fontSize: "11px",
216
+ padding: "4px 8px",
217
+ borderRadius: "4px",
218
+ background: "rgba(239, 68, 68, 0.1)",
219
+ color: "#ef4444",
220
+ fontWeight: 600,
221
+ }}
222
+ >
223
+ Inactive
224
+ </div>
225
+ )}
226
+ </label>
227
+ );
228
+ })}
229
+ </div>
230
+
231
+ {error && (
232
+ <div
233
+ style={{
234
+ padding: "12px",
235
+ background: "rgba(239, 68, 68, 0.1)",
236
+ border: "1px solid rgba(239, 68, 68, 0.3)",
237
+ borderRadius: "6px",
238
+ marginBottom: "16px",
239
+ }}
240
+ >
241
+ <p
242
+ style={{
243
+ margin: 0,
244
+ fontSize: "13px",
245
+ color: "#ef4444",
246
+ lineHeight: "1.5",
247
+ }}
248
+ >
249
+ {error}
250
+ </p>
251
+ </div>
252
+ )}
253
+
254
+ {/* Boutons */}
255
+ <div style={{ display: "flex", gap: "12px" }}>
256
+ <button
257
+ type="button"
258
+ onClick={onCancel}
259
+ disabled={loading}
260
+ style={{
261
+ flex: 1,
262
+ padding: "12px",
263
+ background: "transparent",
264
+ border: "1px solid var(--ai-border-primary, #374151)",
265
+ borderRadius: "8px",
266
+ color: "var(--ai-text-secondary, #9ca3af)",
267
+ fontSize: "14px",
268
+ fontWeight: 600,
269
+ cursor: loading ? "not-allowed" : "pointer",
270
+ opacity: loading ? 0.5 : 1,
271
+ transition: "all 0.2s ease",
272
+ }}
273
+ onMouseEnter={(e) => {
274
+ if (!loading) {
275
+ e.currentTarget.style.background =
276
+ "var(--ai-input-bg, #111827)";
277
+ e.currentTarget.style.borderColor =
278
+ "var(--ai-border-hover, #4b5563)";
279
+ }
280
+ }}
281
+ onMouseLeave={(e) => {
282
+ if (!loading) {
283
+ e.currentTarget.style.background = "transparent";
284
+ e.currentTarget.style.borderColor =
285
+ "var(--ai-border-primary, #374151)";
286
+ }
287
+ }}
288
+ >
289
+ Annuler
290
+ </button>
291
+ <button
292
+ type="submit"
293
+ disabled={loading || !selectedKeyId}
294
+ style={{
295
+ flex: 1,
296
+ padding: "12px",
297
+ background: loading
298
+ ? "var(--ai-input-bg, #111827)"
299
+ : "linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)",
300
+ border: "none",
301
+ borderRadius: "8px",
302
+ color: "#ffffff",
303
+ fontSize: "14px",
304
+ fontWeight: 600,
305
+ cursor: loading || !selectedKeyId ? "not-allowed" : "pointer",
306
+ opacity: loading || !selectedKeyId ? 0.5 : 1,
307
+ transition: "all 0.2s ease",
308
+ }}
309
+ onMouseEnter={(e) => {
310
+ if (!loading && selectedKeyId) {
311
+ e.currentTarget.style.transform = "translateY(-1px)";
312
+ e.currentTarget.style.boxShadow =
313
+ "0 8px 20px rgba(139, 92, 246, 0.4)";
314
+ }
315
+ }}
316
+ onMouseLeave={(e) => {
317
+ if (!loading && selectedKeyId) {
318
+ e.currentTarget.style.transform = "none";
319
+ e.currentTarget.style.boxShadow = "none";
320
+ }
321
+ }}
322
+ >
323
+ {loading ? "Connexion..." : "Continuer"}
324
+ </button>
325
+ </div>
326
+ </form>
327
+ </div>
328
+ </div>
329
+ );
330
+ }
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
 
3
- import { useState } from "react";
3
+ import { useState, useEffect } from "react";
4
4
  import { useLB } from "../context/LBAuthProvider";
5
+ import { LBApiKeySelector } from "./LBApiKeySelector";
5
6
 
6
7
  export interface LBSigninModalProps {
7
8
  isOpen: boolean;
@@ -14,12 +15,22 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
14
15
  | ((
15
16
  email: string,
16
17
  password: string
17
- ) => Promise<{ success: boolean; error?: string }>)
18
+ ) => Promise<{
19
+ success: boolean;
20
+ error?: string;
21
+ needsKeySelection?: boolean;
22
+ }>)
18
23
  | undefined;
24
+ let selectApiKeyWithToken: ((apiKeyId: string) => Promise<void>) | undefined;
25
+ let apiKeys: any[] = [];
26
+ let lbStatus: string | undefined;
19
27
 
20
28
  try {
21
29
  const lbContext = useLB();
22
30
  login = lbContext.login;
31
+ selectApiKeyWithToken = lbContext.selectApiKeyWithToken;
32
+ apiKeys = lbContext.apiKeys || [];
33
+ lbStatus = lbContext.status;
23
34
  } catch {
24
35
  // LBProvider n'est pas disponible, ne pas rendre le modal
25
36
  return null;
@@ -29,6 +40,14 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
29
40
  const [password, setPassword] = useState("");
30
41
  const [loading, setLoading] = useState(false);
31
42
  const [error, setError] = useState("");
43
+ const [showKeySelector, setShowKeySelector] = useState(false);
44
+
45
+ // Si le status est "needs_key_selection", afficher le sélecteur
46
+ useEffect(() => {
47
+ if (lbStatus === "needs_key_selection" && apiKeys.length > 0) {
48
+ setShowKeySelector(true);
49
+ }
50
+ }, [lbStatus, apiKeys.length]);
32
51
 
33
52
  if (!isOpen || !login) return null;
34
53
 
@@ -40,7 +59,13 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
40
59
  try {
41
60
  const result = await login(email, password);
42
61
  if (result.success) {
43
- onClose();
62
+ if (result.needsKeySelection) {
63
+ // L'utilisateur doit choisir une clé API
64
+ setShowKeySelector(true);
65
+ } else {
66
+ // Connexion réussie, fermer le modal
67
+ onClose();
68
+ }
44
69
  } else {
45
70
  setError(result.error || "Échec de la connexion");
46
71
  }
@@ -53,6 +78,40 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
53
78
  }
54
79
  };
55
80
 
81
+ const handleKeySelect = async (apiKeyId: string) => {
82
+ if (!selectApiKeyWithToken) return;
83
+
84
+ try {
85
+ await selectApiKeyWithToken(apiKeyId);
86
+ setShowKeySelector(false);
87
+ onClose();
88
+ } catch (err) {
89
+ setError(
90
+ err instanceof Error ? err.message : "Erreur lors de la sélection"
91
+ );
92
+ setShowKeySelector(false);
93
+ }
94
+ };
95
+
96
+ const handleCancelKeySelection = () => {
97
+ setShowKeySelector(false);
98
+ setEmail("");
99
+ setPassword("");
100
+ setError("");
101
+ };
102
+
103
+ // Si on doit afficher le sélecteur de clés
104
+ if (showKeySelector && apiKeys.length > 0) {
105
+ return (
106
+ <LBApiKeySelector
107
+ apiKeys={apiKeys}
108
+ onSelect={handleKeySelect}
109
+ onCancel={handleCancelKeySelection}
110
+ isOpen={true}
111
+ />
112
+ );
113
+ }
114
+
56
115
  const handleKeyDown = (e: React.KeyboardEvent) => {
57
116
  if (e.key === "Escape") {
58
117
  onClose();
@@ -36,13 +36,19 @@ interface LBContextValue extends LBAuthState {
36
36
  login: (
37
37
  email: string,
38
38
  password: string
39
- ) => Promise<{ success: boolean; error?: string }>;
39
+ ) => Promise<{
40
+ success: boolean;
41
+ error?: string;
42
+ needsKeySelection?: boolean;
43
+ }>;
40
44
  /** Fonction de déconnexion */
41
45
  logout: () => Promise<void>;
42
46
  /** Récupère les clés API de l'utilisateur */
43
47
  fetchApiKeys: (accessToken: string) => Promise<LBApiKey[]>;
44
- /** Sélectionne une clé API et crée une session */
48
+ /** Sélectionne une clé API et crée une session (après login) */
45
49
  selectApiKey: (accessToken: string, apiKeyId: string) => Promise<void>;
50
+ /** Sélectionne une clé API avec le token stocké */
51
+ selectApiKeyWithToken: (apiKeyId: string) => Promise<void>;
46
52
  /** Recharge l'état de la session */
47
53
  refreshSession: () => Promise<void>;
48
54
  /** Clés API disponibles */
@@ -106,18 +112,30 @@ export function LBProvider({
106
112
  const fetchApiKeys = useCallback(
107
113
  async (token: string): Promise<LBApiKey[]> => {
108
114
  try {
115
+ console.log(
116
+ "[LBProvider] Fetching API keys with token:",
117
+ token.substring(0, 20) + "..."
118
+ );
119
+
109
120
  const response = await fetch(`${proxyUrl}/public/user/api-keys`, {
110
121
  headers: {
111
122
  Authorization: `Bearer ${token}`,
123
+ "Content-Type": "application/json",
112
124
  },
113
125
  credentials: "include",
114
126
  });
115
127
 
128
+ console.log("[LBProvider] API keys response status:", response.status);
129
+
116
130
  if (!response.ok) {
117
- throw new Error("Failed to fetch API keys");
131
+ const errorData = await response.json().catch(() => ({}));
132
+ console.error("[LBProvider] Failed to fetch API keys:", errorData);
133
+ throw new Error(errorData.message || "Failed to fetch API keys");
118
134
  }
119
135
 
120
136
  const data = await response.json();
137
+ console.log("[LBProvider] API keys received:", data);
138
+
121
139
  const keys: LBApiKey[] = data.apiKeys || data;
122
140
  setApiKeys(keys);
123
141
  return keys;
@@ -135,6 +153,7 @@ export function LBProvider({
135
153
  const selectApiKey = useCallback(
136
154
  async (token: string, apiKeyId: string): Promise<void> => {
137
155
  try {
156
+ console.log("[LBProvider] Selecting API key:", apiKeyId);
138
157
  setState((prev: LBAuthState) => ({ ...prev, status: "loading" }));
139
158
 
140
159
  const response = await fetch(`${proxyUrl}/auth/session`, {
@@ -147,11 +166,19 @@ export function LBProvider({
147
166
  credentials: "include",
148
167
  });
149
168
 
169
+ console.log("[LBProvider] Session response status:", response.status);
170
+
150
171
  if (!response.ok) {
151
- throw new Error("Failed to create session");
172
+ const errorData = await response.json().catch(() => ({}));
173
+ console.error("[LBProvider] Failed to create session:", errorData);
174
+ throw new Error(errorData.message || "Failed to create session");
152
175
  }
153
176
 
154
177
  const sessionResult: LBSessionResult = await response.json();
178
+ console.log(
179
+ "[LBProvider] Session created successfully:",
180
+ sessionResult
181
+ );
155
182
 
156
183
  setState({
157
184
  status: "ready",
@@ -166,6 +193,7 @@ export function LBProvider({
166
193
  });
167
194
 
168
195
  setAccessToken(undefined); // Nettoyer l'access token temporaire
196
+ setApiKeys([]); // Nettoyer les clés API temporaires
169
197
  onStatusChange?.("ready");
170
198
  } catch (error) {
171
199
  const message =
@@ -181,14 +209,20 @@ export function LBProvider({
181
209
  );
182
210
 
183
211
  /**
184
- * Connexion utilisateur
212
+ * Connexion utilisateur (étape 1 : login)
213
+ * Retourne le token et les clés API sans créer de session
185
214
  */
186
215
  const login = useCallback(
187
216
  async (
188
217
  email: string,
189
218
  password: string
190
- ): Promise<{ success: boolean; error?: string }> => {
219
+ ): Promise<{
220
+ success: boolean;
221
+ error?: string;
222
+ needsKeySelection?: boolean;
223
+ }> => {
191
224
  try {
225
+ console.log("[LBProvider] Login attempt:", email);
192
226
  setState((prev: LBAuthState) => ({ ...prev, status: "loading" }));
193
227
 
194
228
  const response = await fetch(`${proxyUrl}/auth/login`, {
@@ -198,9 +232,12 @@ export function LBProvider({
198
232
  credentials: "include",
199
233
  });
200
234
 
235
+ console.log("[LBProvider] Login response status:", response.status);
236
+
201
237
  if (!response.ok) {
202
238
  const error = await response.json();
203
239
  const errorMessage = error.message || "Login failed";
240
+ console.error("[LBProvider] Login failed:", errorMessage);
204
241
  setState({
205
242
  status: "needs_auth",
206
243
  error: errorMessage,
@@ -209,7 +246,27 @@ export function LBProvider({
209
246
  }
210
247
 
211
248
  const result: LBLoginResult = await response.json();
249
+ console.log("[LBProvider] Login successful:", result.user?.email);
250
+ console.log(
251
+ "[LBProvider] Access token received:",
252
+ result.accessToken ? "YES" : "NO"
253
+ );
254
+ console.log(
255
+ "[LBProvider] Token length:",
256
+ result.accessToken?.length || 0
257
+ );
258
+
212
259
  const token = result.accessToken;
260
+
261
+ if (!token) {
262
+ console.error("[LBProvider] No access token in login response!");
263
+ setState({
264
+ status: "needs_auth",
265
+ error: "Token manquant dans la réponse",
266
+ });
267
+ return { success: false, error: "Token manquant dans la réponse" };
268
+ }
269
+
213
270
  setAccessToken(token);
214
271
 
215
272
  setState({
@@ -217,38 +274,64 @@ export function LBProvider({
217
274
  user: result.user,
218
275
  });
219
276
 
220
- // Récupérer les clés API et sélectionner automatiquement la première active
277
+ // Récupérer les clés API
278
+ console.log("[LBProvider] About to fetch API keys...");
221
279
  try {
222
280
  const keys = await fetchApiKeys(token);
223
- const activeKey = keys.find((k) => k.isActive);
281
+ console.log("[LBProvider] Fetched keys:", keys.length);
224
282
 
225
- if (activeKey) {
226
- // Créer une session avec la première clé active
227
- await selectApiKey(token, activeKey.id);
228
- } else {
229
- // Aucune clé active trouvée
283
+ if (keys.length === 0) {
284
+ console.warn("[LBProvider] No API keys found for user");
230
285
  setState({
231
286
  status: "ready",
232
287
  user: result.user,
288
+ error: "Aucune clé API disponible",
233
289
  });
290
+ return { success: true, needsKeySelection: false };
291
+ }
292
+
293
+ // Si une seule clé active, la sélectionner automatiquement
294
+ const activeKeys = keys.filter((k) => k.isActive);
295
+ if (activeKeys.length === 1) {
296
+ console.log("[LBProvider] Auto-selecting single active key");
297
+ await selectApiKey(token, activeKeys[0].id);
298
+ return { success: true, needsKeySelection: false };
234
299
  }
235
300
 
236
- return { success: true };
301
+ // Sinon, laisser l'utilisateur choisir
302
+ console.log(
303
+ "[LBProvider] Multiple keys available, user needs to select"
304
+ );
305
+ setState({
306
+ status: "needs_key_selection",
307
+ user: result.user,
308
+ });
309
+ return { success: true, needsKeySelection: true };
237
310
  } catch (keyError) {
311
+ console.error("[LBProvider] Failed to fetch API keys:");
312
+ console.error("[LBProvider] Error details:", keyError);
238
313
  console.error(
239
- "[LBProvider] Failed to fetch/select API key:",
240
- keyError
314
+ "[LBProvider] Error message:",
315
+ keyError instanceof Error ? keyError.message : String(keyError)
241
316
  );
242
- // Login réussi mais impossible de récupérer les clés
317
+ console.error(
318
+ "[LBProvider] Error stack:",
319
+ keyError instanceof Error ? keyError.stack : "No stack"
320
+ );
321
+
243
322
  setState({
244
- status: "ready",
245
- user: result.user,
323
+ status: "needs_auth",
324
+ error: "Impossible de récupérer les clés API",
246
325
  });
247
- return { success: true };
326
+ return {
327
+ success: false,
328
+ error: "Impossible de récupérer les clés API",
329
+ };
248
330
  }
249
331
  } catch (error) {
250
332
  const message =
251
333
  error instanceof Error ? error.message : "Une erreur s'est produite";
334
+ console.error("[LBProvider] Login error:", message);
252
335
  setState({
253
336
  status: "error",
254
337
  error: message,
@@ -259,6 +342,19 @@ export function LBProvider({
259
342
  [proxyUrl, fetchApiKeys, selectApiKey]
260
343
  );
261
344
 
345
+ /**
346
+ * Sélectionne une clé API avec le token déjà stocké (après login)
347
+ */
348
+ const selectApiKeyWithToken = useCallback(
349
+ async (apiKeyId: string): Promise<void> => {
350
+ if (!accessToken) {
351
+ throw new Error("No access token available");
352
+ }
353
+ await selectApiKey(accessToken, apiKeyId);
354
+ },
355
+ [accessToken, selectApiKey]
356
+ );
357
+
262
358
  /**
263
359
  * Déconnexion
264
360
  */
@@ -291,6 +387,7 @@ export function LBProvider({
291
387
  logout,
292
388
  fetchApiKeys,
293
389
  selectApiKey,
390
+ selectApiKeyWithToken,
294
391
  refreshSession,
295
392
  apiKeys,
296
393
  accessToken,
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ export * from "./components/AiSettingsButton";
28
28
  export * from "./components/AiStatusButton";
29
29
  export * from "./components/LBConnectButton";
30
30
  export * from "./components/LBSigninModal";
31
+ export * from "./components/LBApiKeySelector";
31
32
  export * from "./components/LBKeyPicker";
32
33
 
33
34
  // Toast system