@lastbrain/ai-ui-react 1.0.78 → 1.0.80

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.
@@ -7,6 +7,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
7
7
  import { createContext, useContext, useEffect, useCallback, useMemo, useRef, useState, } from "react";
8
8
  import { createLBClient } from "@lastbrain/ai-ui-core";
9
9
  import { I18nProvider } from "./I18nContext";
10
+ function hasServerSelectedApiKeyCookie(userData) {
11
+ if (!userData || typeof userData !== "object")
12
+ return false;
13
+ const value = userData.hasApiKeySelectedCookie;
14
+ return value === true;
15
+ }
10
16
  const LBContext = createContext(undefined);
11
17
  // Export pour usage dans d'autres composants
12
18
  export { LBContext };
@@ -24,15 +30,6 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
24
30
  const [storageLastFetch, setStorageLastFetch] = useState(0);
25
31
  const [hasSelectedApiKeyCookie, setHasSelectedApiKeyCookie] = useState(false);
26
32
  const previousStatusRef = useRef("loading");
27
- const syncSelectedApiKeyCookie = useCallback(() => {
28
- if (typeof document === "undefined")
29
- return;
30
- const hasCookie = document.cookie
31
- .split(";")
32
- .map((part) => part.trim())
33
- .some((part) => part.startsWith("api_key_selected=") && part.length > 17);
34
- setHasSelectedApiKeyCookie(hasCookie);
35
- }, []);
36
33
  const lbClient = useMemo(() => createLBClient({
37
34
  baseUrl: proxyUrl,
38
35
  mode: process.env.LB_API_KEY ? "env-key" : "auto",
@@ -46,6 +43,7 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
46
43
  const session = await lbClient.verifySession();
47
44
  if (session) {
48
45
  const userData = await lbClient.getUser().catch(() => null);
46
+ const hasCookie = hasServerSelectedApiKeyCookie(userData);
49
47
  const activeKey = userData?.apiKeyActive
50
48
  ? {
51
49
  id: userData.apiKeyActive.id,
@@ -64,14 +62,16 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
64
62
  id: session.userId,
65
63
  email: userData?.user?.email || "",
66
64
  },
67
- selectedKey: activeKey,
65
+ selectedKey: hasCookie ? activeKey : undefined,
68
66
  });
67
+ setHasSelectedApiKeyCookie(hasCookie);
69
68
  onStatusChange?.("ready");
70
69
  }
71
70
  else {
72
71
  // Supabase session mode (no lb_session cookie): try user endpoint directly
73
72
  const userData = await lbClient.getUser().catch(() => null);
74
73
  if (userData?.user?.id) {
74
+ const hasCookie = hasServerSelectedApiKeyCookie(userData);
75
75
  const activeKey = userData?.apiKeyActive
76
76
  ? {
77
77
  id: userData.apiKeyActive.id,
@@ -89,12 +89,14 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
89
89
  id: userData.user.id,
90
90
  email: userData.user.email || "",
91
91
  },
92
- selectedKey: activeKey,
92
+ selectedKey: hasCookie ? activeKey : undefined,
93
93
  });
94
+ setHasSelectedApiKeyCookie(hasCookie);
94
95
  onStatusChange?.("ready");
95
96
  }
96
97
  else {
97
98
  setState({ status: "needs_auth" });
99
+ setHasSelectedApiKeyCookie(false);
98
100
  onStatusChange?.("needs_auth");
99
101
  }
100
102
  }
@@ -102,19 +104,42 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
102
104
  catch (error) {
103
105
  console.error("[LBProvider] Session check failed:", error);
104
106
  setState({ status: "needs_auth" });
107
+ setHasSelectedApiKeyCookie(false);
105
108
  onStatusChange?.("needs_auth");
106
109
  }
107
110
  }, [lbClient, onStatusChange]);
108
111
  useEffect(() => {
109
112
  checkSession();
110
113
  }, [checkSession]);
111
- useEffect(() => {
112
- syncSelectedApiKeyCookie();
113
- if (typeof window === "undefined")
114
- return;
115
- const interval = window.setInterval(syncSelectedApiKeyCookie, 1000);
116
- return () => window.clearInterval(interval);
117
- }, [syncSelectedApiKeyCookie]);
114
+ const fetchWalletSummary = useCallback(async () => {
115
+ const headers = lbClient.getAuthHeaders(state.session?.sessionToken);
116
+ const walletUrls = Array.from(new Set([
117
+ `${proxyUrl}/auth/wallet`,
118
+ `${proxyUrl.replace("/api/lastbrain", "/api/ai")}/auth/wallet`,
119
+ ]));
120
+ for (const url of walletUrls) {
121
+ try {
122
+ const response = await fetch(url, {
123
+ method: "GET",
124
+ credentials: "include",
125
+ headers,
126
+ });
127
+ if (!response.ok)
128
+ continue;
129
+ const data = (await response.json());
130
+ return {
131
+ used: Number(data.totalUsed || 0),
132
+ total: Number(data.totalAdded || 0),
133
+ percentage: Number(data.percentUsed ?? data.percentage ?? 0),
134
+ remaining: Number(data.walletSellValueUsd || 0),
135
+ };
136
+ }
137
+ catch {
138
+ // try next endpoint
139
+ }
140
+ }
141
+ return null;
142
+ }, [lbClient, proxyUrl, state.session?.sessionToken]);
118
143
  /**
119
144
  * Récupère les clés API de l'utilisateur
120
145
  */
@@ -153,7 +178,6 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
153
178
  setAccessToken(undefined); // Nettoyer l'access token temporaire
154
179
  setApiKeys([]); // Nettoyer les clés API temporaires
155
180
  setHasSelectedApiKeyCookie(true);
156
- setTimeout(() => syncSelectedApiKeyCookie(), 100);
157
181
  onStatusChange?.("ready");
158
182
  onAuthChange?.(); // Refresh provider after signin
159
183
  }
@@ -165,7 +189,7 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
165
189
  });
166
190
  throw error;
167
191
  }
168
- }, [lbClient, onAuthChange, onStatusChange, state.user, syncSelectedApiKeyCookie]);
192
+ }, [lbClient, onAuthChange, onStatusChange, state.user]);
169
193
  /**
170
194
  * Connexion utilisateur (étape 1 : login)
171
195
  * Retourne le token et les clés API sans créer de session
@@ -273,16 +297,20 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
273
297
  used: 0,
274
298
  total: userData.balance?.sellValueUsd || 0,
275
299
  percentage: 0,
300
+ remaining: userData.balance?.sellValueUsd || 0,
276
301
  },
277
302
  };
278
303
  }
304
+ const userData = await lbClient.getUser().catch(() => null);
305
+ setHasSelectedApiKeyCookie(hasServerSelectedApiKeyCookie(userData));
306
+ const walletSummary = await fetchWalletSummary().catch(() => null);
279
307
  const normalizedStatus = {
280
308
  ...data,
281
309
  authType: data?.authType,
282
- user: data?.user,
283
- apiKey: data?.apiKey || data?.api_key,
284
- api_key: data?.api_key || data?.apiKey,
285
- balance: data?.balance || {
310
+ user: data?.user || userData?.user,
311
+ apiKey: data?.apiKey || data?.api_key || userData?.apiKeyActive,
312
+ api_key: data?.api_key || data?.apiKey || userData?.apiKeyActive,
313
+ balance: walletSummary || data?.balance || {
286
314
  used: 0,
287
315
  total: 0,
288
316
  percentage: 0,
@@ -302,7 +330,7 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
302
330
  finally {
303
331
  setIsLoadingStatus(false);
304
332
  }
305
- }, [lbClient, state.status, storageStatus]);
333
+ }, [fetchWalletSummary, lbClient, state.status, storageStatus]);
306
334
  /**
307
335
  * Récupère le storage - LENT avec cache (5 minutes)
308
336
  */
@@ -380,9 +408,11 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
380
408
  }));
381
409
  }
382
410
  setHasSelectedApiKeyCookie(true);
383
- await refreshBasicStatus();
384
- setTimeout(() => refreshStorageStatus(), 100);
385
- setTimeout(() => syncSelectedApiKeyCookie(), 100);
411
+ // Ne pas bloquer la validation UI de sélection sur des requêtes status/wallet/storage.
412
+ void refreshBasicStatus();
413
+ setTimeout(() => {
414
+ void refreshStorageStatus();
415
+ }, 100);
386
416
  }
387
417
  else {
388
418
  throw new Error("No valid authentication method available");
@@ -395,7 +425,6 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
395
425
  lbClient,
396
426
  refreshBasicStatus,
397
427
  refreshStorageStatus,
398
- syncSelectedApiKeyCookie,
399
428
  ]);
400
429
  /**
401
430
  * Déconnexion
@@ -444,6 +473,13 @@ export function LBProvider({ children, baseUrl: _baseUrl = "/api/lastbrain", pro
444
473
  setApiKeys([]);
445
474
  }
446
475
  }, [lbClient, state.status]);
476
+ useEffect(() => {
477
+ if (state.status !== "ready" || !state.session)
478
+ return;
479
+ if (hasSelectedApiKeyCookie)
480
+ return;
481
+ setState((prev) => ({ ...prev, selectedKey: undefined }));
482
+ }, [hasSelectedApiKeyCookie, state.session, state.status]);
447
483
  // Refresh status uniquement lors de la transition vers "ready"
448
484
  // (évite les boucles status/user causées par des callbacks recréés)
449
485
  useEffect(() => {
package/dist/styles.css CHANGED
@@ -310,7 +310,7 @@
310
310
  min-height: 96px;
311
311
  resize: none;
312
312
  overflow: hidden;
313
- padding: 6px 2px 8px;
313
+ padding: 6px 2px 32px;
314
314
  }
315
315
 
316
316
  .ai-control[aria-invalid="true"],
@@ -641,7 +641,7 @@
641
641
 
642
642
  .ai-status-tooltip {
643
643
  width: min(370px, calc(100vw - 16px));
644
- z-index: 2147483647;
644
+ z-index: 2147483646;
645
645
  }
646
646
 
647
647
  .ai-status-actions {
@@ -685,7 +685,7 @@
685
685
  white-space: nowrap;
686
686
  opacity: 0;
687
687
  pointer-events: none;
688
- z-index: 2147483647;
688
+ z-index: 2147483646;
689
689
  padding: 4px 8px;
690
690
  border-radius: 8px;
691
691
  border: 1px solid var(--ai-border);
@@ -772,7 +772,7 @@
772
772
  [data-ai-settings-panel] {
773
773
  position: fixed;
774
774
  inset: 0;
775
- z-index: 2147483645;
775
+ z-index: 2147483647;
776
776
  display: flex;
777
777
  align-items: center;
778
778
  justify-content: center;
@@ -1109,6 +1109,8 @@
1109
1109
 
1110
1110
  .ai-key-modal-panel {
1111
1111
  max-width: 520px;
1112
+ position: relative;
1113
+ z-index: 2147483647;
1112
1114
  }
1113
1115
 
1114
1116
  .ai-key-radio {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastbrain/ai-ui-react",
3
- "version": "1.0.78",
3
+ "version": "1.0.80",
4
4
  "description": "Headless React components for LastBrain AI UI Kit",
5
5
  "private": false,
6
6
  "type": "module",
@@ -84,20 +84,19 @@ export function AiContextButton({
84
84
 
85
85
  let lbStatus: string | undefined;
86
86
  let lbHasSession = false;
87
- let lbHasSelectedKey = false;
88
87
  let lbHasSelectedApiKeyCookie = false;
88
+ let lbHasSelectedKey = false;
89
89
  let hasLBProvider = false;
90
90
  try {
91
91
  const lbContext = useLB();
92
92
  lbStatus = lbContext.status;
93
93
  lbHasSession = Boolean(lbContext.session?.sessionToken);
94
- lbHasSelectedKey = Boolean(lbContext.selectedKey?.id);
95
94
  lbHasSelectedApiKeyCookie = lbContext.hasSelectedApiKeyCookie;
95
+ lbHasSelectedKey = Boolean(lbContext.selectedKey?.id);
96
96
  hasLBProvider = true;
97
97
  } catch {
98
98
  lbStatus = undefined;
99
99
  lbHasSession = false;
100
- lbHasSelectedKey = false;
101
100
  hasLBProvider = false;
102
101
  }
103
102
 
@@ -114,7 +113,8 @@ export function AiContextButton({
114
113
  hasLBProvider &&
115
114
  lbStatus === "ready" &&
116
115
  lbHasSession &&
117
- (!lbHasSelectedKey || !lbHasSelectedApiKeyCookie);
116
+ !lbHasSelectedApiKeyCookie &&
117
+ !lbHasSelectedKey;
118
118
  const isAuthReady =
119
119
  !needsApiKeySelection &&
120
120
  (lbStatus === "ready" || Boolean(process.env.LB_API_KEY));
@@ -250,8 +250,14 @@ export function AiContextButton({
250
250
  <div className="relative inline-block ai-glow">
251
251
  <button
252
252
  {...buttonProps}
253
- onClick={handleOpenPanel}
254
- disabled={disabled || loading || !isAuthReady}
253
+ onClick={() => {
254
+ if (!isAuthReady) {
255
+ setShowAuthModal(true);
256
+ return;
257
+ }
258
+ handleOpenPanel();
259
+ }}
260
+ disabled={disabled || loading}
255
261
  className={`ai-btn ai-context-btn ${variantClass} ${sizeClass} ${radiusClass} ${className || ""}`}
256
262
  title={
257
263
  !isAuthReady
@@ -292,6 +298,8 @@ export function AiContextButton({
292
298
  modelCategory="text"
293
299
  baseUrl={baseUrl}
294
300
  apiKey={apiKeyId}
301
+ contextPreview={formatContextData(contextData)}
302
+ contextPreviewTitle={resolvedContextDescription}
295
303
  />
296
304
  ) : null}
297
305
  </div>
@@ -80,21 +80,20 @@ export function AiImageButton({
80
80
  // Rendre l'authentification optionnelle
81
81
  let lbStatus: string | undefined;
82
82
  let lbHasSession = false;
83
- let lbHasSelectedKey = false;
84
83
  let lbHasSelectedApiKeyCookie = false;
84
+ let lbHasSelectedKey = false;
85
85
  let hasLBProvider = false;
86
86
  try {
87
87
  const lbContext = useLB();
88
88
  lbStatus = lbContext.status;
89
89
  lbHasSession = Boolean(lbContext.session?.sessionToken);
90
- lbHasSelectedKey = Boolean(lbContext.selectedKey?.id);
91
90
  lbHasSelectedApiKeyCookie = lbContext.hasSelectedApiKeyCookie;
91
+ lbHasSelectedKey = Boolean(lbContext.selectedKey?.id);
92
92
  hasLBProvider = true;
93
93
  } catch {
94
94
  // LBProvider n'est pas disponible, ignorer
95
95
  lbStatus = undefined;
96
96
  lbHasSession = false;
97
- lbHasSelectedKey = false;
98
97
  hasLBProvider = false;
99
98
  }
100
99
 
@@ -110,7 +109,8 @@ export function AiImageButton({
110
109
  hasLBProvider &&
111
110
  lbStatus === "ready" &&
112
111
  lbHasSession &&
113
- (!lbHasSelectedKey || !lbHasSelectedApiKeyCookie);
112
+ !lbHasSelectedApiKeyCookie &&
113
+ !lbHasSelectedKey;
114
114
  const isAuthReady =
115
115
  !needsApiKeySelection &&
116
116
  (lbStatus === "ready" || Boolean(process.env.LB_API_KEY));
@@ -229,8 +229,14 @@ export function AiImageButton({
229
229
  <div className="relative inline-block ai-glow">
230
230
  <button
231
231
  {...buttonProps}
232
- onClick={handleOpenPanel}
233
- disabled={disabled || loading || !isAuthReady}
232
+ onClick={() => {
233
+ if (!isAuthReady) {
234
+ setShowAuthModal(true);
235
+ return;
236
+ }
237
+ handleOpenPanel();
238
+ }}
239
+ disabled={disabled || loading}
234
240
  className={`ai-btn ai-image-btn ${variantClass} ${sizeClass} ${radiusClass} ${className || ""}`}
235
241
  style={buttonProps.style}
236
242
  data-ai-image-button
@@ -61,21 +61,20 @@ export function AiInput({
61
61
  // Rendre l'authentification optionnelle
62
62
  let lbStatus: string | undefined;
63
63
  let lbHasSession = false;
64
- let lbHasSelectedKey = false;
65
64
  let lbHasSelectedApiKeyCookie = false;
65
+ let lbHasSelectedKey = false;
66
66
  let hasLBProvider = false;
67
67
  try {
68
68
  const lbContext = useLB();
69
69
  lbStatus = lbContext.status;
70
70
  lbHasSession = Boolean(lbContext.session?.sessionToken);
71
- lbHasSelectedKey = Boolean(lbContext.selectedKey?.id);
72
71
  lbHasSelectedApiKeyCookie = lbContext.hasSelectedApiKeyCookie;
72
+ lbHasSelectedKey = Boolean(lbContext.selectedKey?.id);
73
73
  hasLBProvider = true;
74
74
  } catch {
75
75
  // LBProvider n'est pas disponible, ignorer
76
76
  lbStatus = undefined;
77
77
  lbHasSession = false;
78
- lbHasSelectedKey = false;
79
78
  hasLBProvider = false;
80
79
  }
81
80
 
@@ -106,7 +105,8 @@ export function AiInput({
106
105
  hasLBProvider &&
107
106
  lbStatus === "ready" &&
108
107
  lbHasSession &&
109
- (!lbHasSelectedKey || !lbHasSelectedApiKeyCookie);
108
+ !lbHasSelectedApiKeyCookie &&
109
+ !lbHasSelectedKey;
110
110
  const isAuthReady =
111
111
  !needsApiKeySelection &&
112
112
  (lbStatus === "ready" || Boolean(process.env.LB_API_KEY));