@lastbrain/ai-ui-react 1.0.43 → 1.0.44

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,kDA2TvB"}
@@ -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,kDA0bpE"}
@@ -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;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAChF,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,2CAkSjB;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,53 @@ 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);
137
154
  const token = result.accessToken;
138
155
  setAccessToken(token);
139
156
  setState({
140
157
  status: "loading",
141
158
  user: result.user,
142
159
  });
143
- // Récupérer les clés API et sélectionner automatiquement la première active
160
+ // Récupérer les clés API
144
161
  try {
145
162
  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
163
+ console.log("[LBProvider] Fetched keys:", keys.length);
164
+ if (keys.length === 0) {
165
+ console.warn("[LBProvider] No API keys found for user");
153
166
  setState({
154
167
  status: "ready",
155
168
  user: result.user,
169
+ error: "Aucune clé API disponible",
156
170
  });
171
+ return { success: true, needsKeySelection: false };
172
+ }
173
+ // Si une seule clé active, la sélectionner automatiquement
174
+ const activeKeys = keys.filter((k) => k.isActive);
175
+ if (activeKeys.length === 1) {
176
+ console.log("[LBProvider] Auto-selecting single active key");
177
+ await selectApiKey(token, activeKeys[0].id);
178
+ return { success: true, needsKeySelection: false };
157
179
  }
158
- return { success: true };
180
+ // Sinon, laisser l'utilisateur choisir
181
+ console.log("[LBProvider] Multiple keys available, user needs to select");
182
+ setState({
183
+ status: "needs_key_selection",
184
+ user: result.user,
185
+ });
186
+ return { success: true, needsKeySelection: true };
159
187
  }
160
188
  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
189
+ console.error("[LBProvider] Failed to fetch API keys:", keyError);
163
190
  setState({
164
- status: "ready",
165
- user: result.user,
191
+ status: "needs_auth",
192
+ error: "Impossible de récupérer les clés API",
166
193
  });
167
- return { success: true };
194
+ return { success: false, error: "Impossible de récupérer les clés API" };
168
195
  }
169
196
  }
170
197
  catch (error) {
171
198
  const message = error instanceof Error ? error.message : "Une erreur s'est produite";
199
+ console.error("[LBProvider] Login error:", message);
172
200
  setState({
173
201
  status: "error",
174
202
  error: message,
@@ -176,6 +204,15 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
176
204
  return { success: false, error: message };
177
205
  }
178
206
  }, [proxyUrl, fetchApiKeys, selectApiKey]);
207
+ /**
208
+ * Sélectionne une clé API avec le token déjà stocké (après login)
209
+ */
210
+ const selectApiKeyWithToken = useCallback(async (apiKeyId) => {
211
+ if (!accessToken) {
212
+ throw new Error("No access token available");
213
+ }
214
+ await selectApiKey(accessToken, apiKeyId);
215
+ }, [accessToken, selectApiKey]);
179
216
  /**
180
217
  * Déconnexion
181
218
  */
@@ -208,6 +245,7 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
208
245
  logout,
209
246
  fetchApiKeys,
210
247
  selectApiKey,
248
+ selectApiKeyWithToken,
211
249
  refreshSession,
212
250
  apiKeys,
213
251
  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.44",
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.32"
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,331 @@
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:
306
+ loading || !selectedKeyId ? "not-allowed" : "pointer",
307
+ opacity: loading || !selectedKeyId ? 0.5 : 1,
308
+ transition: "all 0.2s ease",
309
+ }}
310
+ onMouseEnter={(e) => {
311
+ if (!loading && selectedKeyId) {
312
+ e.currentTarget.style.transform = "translateY(-1px)";
313
+ e.currentTarget.style.boxShadow =
314
+ "0 8px 20px rgba(139, 92, 246, 0.4)";
315
+ }
316
+ }}
317
+ onMouseLeave={(e) => {
318
+ if (!loading && selectedKeyId) {
319
+ e.currentTarget.style.transform = "none";
320
+ e.currentTarget.style.boxShadow = "none";
321
+ }
322
+ }}
323
+ >
324
+ {loading ? "Connexion..." : "Continuer"}
325
+ </button>
326
+ </div>
327
+ </form>
328
+ </div>
329
+ </div>
330
+ );
331
+ }
@@ -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,18 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
14
15
  | ((
15
16
  email: string,
16
17
  password: string
17
- ) => Promise<{ success: boolean; error?: string }>)
18
+ ) => Promise<{ success: boolean; error?: string; needsKeySelection?: boolean }>)
18
19
  | undefined;
20
+ let selectApiKeyWithToken: ((apiKeyId: string) => Promise<void>) | undefined;
21
+ let apiKeys: any[] = [];
22
+ let lbStatus: string | undefined;
19
23
 
20
24
  try {
21
25
  const lbContext = useLB();
22
26
  login = lbContext.login;
27
+ selectApiKeyWithToken = lbContext.selectApiKeyWithToken;
28
+ apiKeys = lbContext.apiKeys || [];
29
+ lbStatus = lbContext.status;
23
30
  } catch {
24
31
  // LBProvider n'est pas disponible, ne pas rendre le modal
25
32
  return null;
@@ -29,6 +36,14 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
29
36
  const [password, setPassword] = useState("");
30
37
  const [loading, setLoading] = useState(false);
31
38
  const [error, setError] = useState("");
39
+ const [showKeySelector, setShowKeySelector] = useState(false);
40
+
41
+ // Si le status est "needs_key_selection", afficher le sélecteur
42
+ useEffect(() => {
43
+ if (lbStatus === "needs_key_selection" && apiKeys.length > 0) {
44
+ setShowKeySelector(true);
45
+ }
46
+ }, [lbStatus, apiKeys.length]);
32
47
 
33
48
  if (!isOpen || !login) return null;
34
49
 
@@ -40,7 +55,13 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
40
55
  try {
41
56
  const result = await login(email, password);
42
57
  if (result.success) {
43
- onClose();
58
+ if (result.needsKeySelection) {
59
+ // L'utilisateur doit choisir une clé API
60
+ setShowKeySelector(true);
61
+ } else {
62
+ // Connexion réussie, fermer le modal
63
+ onClose();
64
+ }
44
65
  } else {
45
66
  setError(result.error || "Échec de la connexion");
46
67
  }
@@ -53,6 +74,40 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
53
74
  }
54
75
  };
55
76
 
77
+ const handleKeySelect = async (apiKeyId: string) => {
78
+ if (!selectApiKeyWithToken) return;
79
+
80
+ try {
81
+ await selectApiKeyWithToken(apiKeyId);
82
+ setShowKeySelector(false);
83
+ onClose();
84
+ } catch (err) {
85
+ setError(
86
+ err instanceof Error ? err.message : "Erreur lors de la sélection"
87
+ );
88
+ setShowKeySelector(false);
89
+ }
90
+ };
91
+
92
+ const handleCancelKeySelection = () => {
93
+ setShowKeySelector(false);
94
+ setEmail("");
95
+ setPassword("");
96
+ setError("");
97
+ };
98
+
99
+ // Si on doit afficher le sélecteur de clés
100
+ if (showKeySelector && apiKeys.length > 0) {
101
+ return (
102
+ <LBApiKeySelector
103
+ apiKeys={apiKeys}
104
+ onSelect={handleKeySelect}
105
+ onCancel={handleCancelKeySelection}
106
+ isOpen={true}
107
+ />
108
+ );
109
+ }
110
+
56
111
  const handleKeyDown = (e: React.KeyboardEvent) => {
57
112
  if (e.key === "Escape") {
58
113
  onClose();
@@ -36,13 +36,15 @@ interface LBContextValue extends LBAuthState {
36
36
  login: (
37
37
  email: string,
38
38
  password: string
39
- ) => Promise<{ success: boolean; error?: string }>;
39
+ ) => Promise<{ success: boolean; error?: string; needsKeySelection?: boolean }>;
40
40
  /** Fonction de déconnexion */
41
41
  logout: () => Promise<void>;
42
42
  /** Récupère les clés API de l'utilisateur */
43
43
  fetchApiKeys: (accessToken: string) => Promise<LBApiKey[]>;
44
- /** Sélectionne une clé API et crée une session */
44
+ /** Sélectionne une clé API et crée une session (après login) */
45
45
  selectApiKey: (accessToken: string, apiKeyId: string) => Promise<void>;
46
+ /** Sélectionne une clé API avec le token stocké */
47
+ selectApiKeyWithToken: (apiKeyId: string) => Promise<void>;
46
48
  /** Recharge l'état de la session */
47
49
  refreshSession: () => Promise<void>;
48
50
  /** Clés API disponibles */
@@ -106,18 +108,27 @@ export function LBProvider({
106
108
  const fetchApiKeys = useCallback(
107
109
  async (token: string): Promise<LBApiKey[]> => {
108
110
  try {
111
+ console.log("[LBProvider] Fetching API keys with token:", token.substring(0, 20) + "...");
112
+
109
113
  const response = await fetch(`${proxyUrl}/public/user/api-keys`, {
110
114
  headers: {
111
115
  Authorization: `Bearer ${token}`,
116
+ "Content-Type": "application/json",
112
117
  },
113
118
  credentials: "include",
114
119
  });
115
120
 
121
+ console.log("[LBProvider] API keys response status:", response.status);
122
+
116
123
  if (!response.ok) {
117
- throw new Error("Failed to fetch API keys");
124
+ const errorData = await response.json().catch(() => ({}));
125
+ console.error("[LBProvider] Failed to fetch API keys:", errorData);
126
+ throw new Error(errorData.message || "Failed to fetch API keys");
118
127
  }
119
128
 
120
129
  const data = await response.json();
130
+ console.log("[LBProvider] API keys received:", data);
131
+
121
132
  const keys: LBApiKey[] = data.apiKeys || data;
122
133
  setApiKeys(keys);
123
134
  return keys;
@@ -135,6 +146,7 @@ export function LBProvider({
135
146
  const selectApiKey = useCallback(
136
147
  async (token: string, apiKeyId: string): Promise<void> => {
137
148
  try {
149
+ console.log("[LBProvider] Selecting API key:", apiKeyId);
138
150
  setState((prev: LBAuthState) => ({ ...prev, status: "loading" }));
139
151
 
140
152
  const response = await fetch(`${proxyUrl}/auth/session`, {
@@ -147,11 +159,16 @@ export function LBProvider({
147
159
  credentials: "include",
148
160
  });
149
161
 
162
+ console.log("[LBProvider] Session response status:", response.status);
163
+
150
164
  if (!response.ok) {
151
- throw new Error("Failed to create session");
165
+ const errorData = await response.json().catch(() => ({}));
166
+ console.error("[LBProvider] Failed to create session:", errorData);
167
+ throw new Error(errorData.message || "Failed to create session");
152
168
  }
153
169
 
154
170
  const sessionResult: LBSessionResult = await response.json();
171
+ console.log("[LBProvider] Session created successfully:", sessionResult);
155
172
 
156
173
  setState({
157
174
  status: "ready",
@@ -166,6 +183,7 @@ export function LBProvider({
166
183
  });
167
184
 
168
185
  setAccessToken(undefined); // Nettoyer l'access token temporaire
186
+ setApiKeys([]); // Nettoyer les clés API temporaires
169
187
  onStatusChange?.("ready");
170
188
  } catch (error) {
171
189
  const message =
@@ -181,14 +199,16 @@ export function LBProvider({
181
199
  );
182
200
 
183
201
  /**
184
- * Connexion utilisateur
202
+ * Connexion utilisateur (étape 1 : login)
203
+ * Retourne le token et les clés API sans créer de session
185
204
  */
186
205
  const login = useCallback(
187
206
  async (
188
207
  email: string,
189
208
  password: string
190
- ): Promise<{ success: boolean; error?: string }> => {
209
+ ): Promise<{ success: boolean; error?: string; needsKeySelection?: boolean }> => {
191
210
  try {
211
+ console.log("[LBProvider] Login attempt:", email);
192
212
  setState((prev: LBAuthState) => ({ ...prev, status: "loading" }));
193
213
 
194
214
  const response = await fetch(`${proxyUrl}/auth/login`, {
@@ -198,9 +218,12 @@ export function LBProvider({
198
218
  credentials: "include",
199
219
  });
200
220
 
221
+ console.log("[LBProvider] Login response status:", response.status);
222
+
201
223
  if (!response.ok) {
202
224
  const error = await response.json();
203
225
  const errorMessage = error.message || "Login failed";
226
+ console.error("[LBProvider] Login failed:", errorMessage);
204
227
  setState({
205
228
  status: "needs_auth",
206
229
  error: errorMessage,
@@ -209,6 +232,8 @@ export function LBProvider({
209
232
  }
210
233
 
211
234
  const result: LBLoginResult = await response.json();
235
+ console.log("[LBProvider] Login successful:", result.user?.email);
236
+
212
237
  const token = result.accessToken;
213
238
  setAccessToken(token);
214
239
 
@@ -217,38 +242,52 @@ export function LBProvider({
217
242
  user: result.user,
218
243
  });
219
244
 
220
- // Récupérer les clés API et sélectionner automatiquement la première active
245
+ // Récupérer les clés API
221
246
  try {
222
247
  const keys = await fetchApiKeys(token);
223
- const activeKey = keys.find((k) => k.isActive);
248
+ console.log("[LBProvider] Fetched keys:", keys.length);
224
249
 
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
250
+ if (keys.length === 0) {
251
+ console.warn("[LBProvider] No API keys found for user");
230
252
  setState({
231
253
  status: "ready",
232
254
  user: result.user,
255
+ error: "Aucune clé API disponible",
233
256
  });
257
+ return { success: true, needsKeySelection: false };
234
258
  }
235
259
 
236
- return { success: true };
260
+ // Si une seule clé active, la sélectionner automatiquement
261
+ const activeKeys = keys.filter((k) => k.isActive);
262
+ if (activeKeys.length === 1) {
263
+ console.log("[LBProvider] Auto-selecting single active key");
264
+ await selectApiKey(token, activeKeys[0].id);
265
+ return { success: true, needsKeySelection: false };
266
+ }
267
+
268
+ // Sinon, laisser l'utilisateur choisir
269
+ console.log("[LBProvider] Multiple keys available, user needs to select");
270
+ setState({
271
+ status: "needs_key_selection",
272
+ user: result.user,
273
+ });
274
+ return { success: true, needsKeySelection: true };
275
+
237
276
  } catch (keyError) {
238
277
  console.error(
239
- "[LBProvider] Failed to fetch/select API key:",
278
+ "[LBProvider] Failed to fetch API keys:",
240
279
  keyError
241
280
  );
242
- // Login réussi mais impossible de récupérer les clés
243
281
  setState({
244
- status: "ready",
245
- user: result.user,
282
+ status: "needs_auth",
283
+ error: "Impossible de récupérer les clés API",
246
284
  });
247
- return { success: true };
285
+ return { success: false, error: "Impossible de récupérer les clés API" };
248
286
  }
249
287
  } catch (error) {
250
288
  const message =
251
289
  error instanceof Error ? error.message : "Une erreur s'est produite";
290
+ console.error("[LBProvider] Login error:", message);
252
291
  setState({
253
292
  status: "error",
254
293
  error: message,
@@ -259,6 +298,19 @@ export function LBProvider({
259
298
  [proxyUrl, fetchApiKeys, selectApiKey]
260
299
  );
261
300
 
301
+ /**
302
+ * Sélectionne une clé API avec le token déjà stocké (après login)
303
+ */
304
+ const selectApiKeyWithToken = useCallback(
305
+ async (apiKeyId: string): Promise<void> => {
306
+ if (!accessToken) {
307
+ throw new Error("No access token available");
308
+ }
309
+ await selectApiKey(accessToken, apiKeyId);
310
+ },
311
+ [accessToken, selectApiKey]
312
+ );
313
+
262
314
  /**
263
315
  * Déconnexion
264
316
  */
@@ -291,6 +343,7 @@ export function LBProvider({
291
343
  logout,
292
344
  fetchApiKeys,
293
345
  selectApiKey,
346
+ selectApiKeyWithToken,
294
347
  refreshSession,
295
348
  apiKeys,
296
349
  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