@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.
@@ -46,6 +46,8 @@ export interface BasicStatus {
46
46
  used?: number;
47
47
  total?: number;
48
48
  percentage?: number;
49
+ remaining?: number;
50
+ providerBudget?: number;
49
51
  };
50
52
  storage?: StorageStatus["storage"];
51
53
  }
@@ -58,6 +60,20 @@ export interface StorageStatus {
58
60
  } | null;
59
61
  }
60
62
 
63
+ interface WalletStatusResponse {
64
+ walletSellValueUsd?: number;
65
+ totalAdded?: number;
66
+ totalUsed?: number;
67
+ percentUsed?: number;
68
+ percentage?: number;
69
+ }
70
+
71
+ function hasServerSelectedApiKeyCookie(userData: unknown): boolean {
72
+ if (!userData || typeof userData !== "object") return false;
73
+ const value = (userData as Record<string, unknown>).hasApiKeySelectedCookie;
74
+ return value === true;
75
+ }
76
+
61
77
  interface LBProviderProps {
62
78
  children: ReactNode;
63
79
  /** URL de l'API LastBrain (ex: https://api.lastbrain.io) */
@@ -115,7 +131,7 @@ interface LBContextValue extends LBAuthState {
115
131
  isLoadingStatus: boolean;
116
132
  /** Indique si le storage est en cours de chargement */
117
133
  isLoadingStorage: boolean;
118
- /** True si le cookie api_key_selected est présent */
134
+ /** True si le cookie api_key_selected est présent (info backend) */
119
135
  hasSelectedApiKeyCookie: boolean;
120
136
  }
121
137
 
@@ -145,18 +161,10 @@ export function LBProvider({
145
161
  const [isLoadingStatus, setIsLoadingStatus] = useState(false);
146
162
  const [isLoadingStorage, setIsLoadingStorage] = useState(false);
147
163
  const [storageLastFetch, setStorageLastFetch] = useState<number>(0);
148
- const [hasSelectedApiKeyCookie, setHasSelectedApiKeyCookie] = useState(false);
164
+ const [hasSelectedApiKeyCookie, setHasSelectedApiKeyCookie] =
165
+ useState(false);
149
166
  const previousStatusRef = useRef<LBAuthState["status"]>("loading");
150
167
 
151
- const syncSelectedApiKeyCookie = useCallback(() => {
152
- if (typeof document === "undefined") return;
153
- const hasCookie = document.cookie
154
- .split(";")
155
- .map((part) => part.trim())
156
- .some((part) => part.startsWith("api_key_selected=") && part.length > 17);
157
- setHasSelectedApiKeyCookie(hasCookie);
158
- }, []);
159
-
160
168
  const lbClient = useMemo(
161
169
  () =>
162
170
  createLBClient({
@@ -175,6 +183,7 @@ export function LBProvider({
175
183
  const session = await lbClient.verifySession();
176
184
  if (session) {
177
185
  const userData = await lbClient.getUser().catch(() => null);
186
+ const hasCookie = hasServerSelectedApiKeyCookie(userData);
178
187
  const activeKey = userData?.apiKeyActive
179
188
  ? {
180
189
  id: userData.apiKeyActive.id,
@@ -193,13 +202,15 @@ export function LBProvider({
193
202
  id: session.userId,
194
203
  email: userData?.user?.email || "",
195
204
  },
196
- selectedKey: activeKey,
205
+ selectedKey: hasCookie ? activeKey : undefined,
197
206
  });
207
+ setHasSelectedApiKeyCookie(hasCookie);
198
208
  onStatusChange?.("ready");
199
209
  } else {
200
210
  // Supabase session mode (no lb_session cookie): try user endpoint directly
201
211
  const userData = await lbClient.getUser().catch(() => null);
202
212
  if (userData?.user?.id) {
213
+ const hasCookie = hasServerSelectedApiKeyCookie(userData);
203
214
  const activeKey = userData?.apiKeyActive
204
215
  ? {
205
216
  id: userData.apiKeyActive.id,
@@ -218,17 +229,20 @@ export function LBProvider({
218
229
  id: userData.user.id,
219
230
  email: userData.user.email || "",
220
231
  },
221
- selectedKey: activeKey,
232
+ selectedKey: hasCookie ? activeKey : undefined,
222
233
  });
234
+ setHasSelectedApiKeyCookie(hasCookie);
223
235
  onStatusChange?.("ready");
224
236
  } else {
225
237
  setState({ status: "needs_auth" });
238
+ setHasSelectedApiKeyCookie(false);
226
239
  onStatusChange?.("needs_auth");
227
240
  }
228
241
  }
229
242
  } catch (error) {
230
243
  console.error("[LBProvider] Session check failed:", error);
231
244
  setState({ status: "needs_auth" });
245
+ setHasSelectedApiKeyCookie(false);
232
246
  onStatusChange?.("needs_auth");
233
247
  }
234
248
  }, [lbClient, onStatusChange]);
@@ -237,12 +251,42 @@ export function LBProvider({
237
251
  checkSession();
238
252
  }, [checkSession]);
239
253
 
240
- useEffect(() => {
241
- syncSelectedApiKeyCookie();
242
- if (typeof window === "undefined") return;
243
- const interval = window.setInterval(syncSelectedApiKeyCookie, 1000);
244
- return () => window.clearInterval(interval);
245
- }, [syncSelectedApiKeyCookie]);
254
+ const fetchWalletSummary = useCallback(async (): Promise<{
255
+ used?: number;
256
+ total?: number;
257
+ percentage?: number;
258
+ remaining?: number;
259
+ } | null> => {
260
+ const headers = lbClient.getAuthHeaders(state.session?.sessionToken);
261
+ const walletUrls = Array.from(
262
+ new Set([
263
+ `${proxyUrl}/auth/wallet`,
264
+ `${proxyUrl.replace("/api/lastbrain", "/api/ai")}/auth/wallet`,
265
+ ])
266
+ );
267
+
268
+ for (const url of walletUrls) {
269
+ try {
270
+ const response = await fetch(url, {
271
+ method: "GET",
272
+ credentials: "include",
273
+ headers,
274
+ });
275
+ if (!response.ok) continue;
276
+ const data = (await response.json()) as WalletStatusResponse;
277
+ return {
278
+ used: Number(data.totalUsed || 0),
279
+ total: Number(data.totalAdded || 0),
280
+ percentage: Number(data.percentUsed ?? data.percentage ?? 0),
281
+ remaining: Number(data.walletSellValueUsd || 0),
282
+ };
283
+ } catch {
284
+ // try next endpoint
285
+ }
286
+ }
287
+
288
+ return null;
289
+ }, [lbClient, proxyUrl, state.session?.sessionToken]);
246
290
 
247
291
  /**
248
292
  * Récupère les clés API de l'utilisateur
@@ -294,7 +338,6 @@ export function LBProvider({
294
338
  setAccessToken(undefined); // Nettoyer l'access token temporaire
295
339
  setApiKeys([]); // Nettoyer les clés API temporaires
296
340
  setHasSelectedApiKeyCookie(true);
297
- setTimeout(() => syncSelectedApiKeyCookie(), 100);
298
341
  onStatusChange?.("ready");
299
342
  onAuthChange?.(); // Refresh provider after signin
300
343
  } catch (error) {
@@ -307,7 +350,7 @@ export function LBProvider({
307
350
  throw error;
308
351
  }
309
352
  },
310
- [lbClient, onAuthChange, onStatusChange, state.user, syncSelectedApiKeyCookie]
353
+ [lbClient, onAuthChange, onStatusChange, state.user]
311
354
  );
312
355
 
313
356
  /**
@@ -452,16 +495,20 @@ export function LBProvider({
452
495
  used: 0,
453
496
  total: userData.balance?.sellValueUsd || 0,
454
497
  percentage: 0,
498
+ remaining: userData.balance?.sellValueUsd || 0,
455
499
  },
456
500
  };
457
501
  }
502
+ const userData = await lbClient.getUser().catch(() => null);
503
+ setHasSelectedApiKeyCookie(hasServerSelectedApiKeyCookie(userData));
504
+ const walletSummary = await fetchWalletSummary().catch(() => null);
458
505
  const normalizedStatus = {
459
506
  ...data,
460
507
  authType: data?.authType,
461
- user: data?.user,
462
- apiKey: data?.apiKey || data?.api_key,
463
- api_key: data?.api_key || data?.apiKey,
464
- balance: data?.balance || {
508
+ user: data?.user || userData?.user,
509
+ apiKey: data?.apiKey || data?.api_key || userData?.apiKeyActive,
510
+ api_key: data?.api_key || data?.apiKey || userData?.apiKeyActive,
511
+ balance: walletSummary || data?.balance || {
465
512
  used: 0,
466
513
  total: 0,
467
514
  percentage: 0,
@@ -480,7 +527,7 @@ export function LBProvider({
480
527
  } finally {
481
528
  setIsLoadingStatus(false);
482
529
  }
483
- }, [lbClient, state.status, storageStatus]);
530
+ }, [fetchWalletSummary, lbClient, state.status, storageStatus]);
484
531
 
485
532
  /**
486
533
  * Récupère le storage - LENT avec cache (5 minutes)
@@ -572,9 +619,11 @@ export function LBProvider({
572
619
  }));
573
620
  }
574
621
  setHasSelectedApiKeyCookie(true);
575
- await refreshBasicStatus();
576
- setTimeout(() => refreshStorageStatus(), 100);
577
- setTimeout(() => syncSelectedApiKeyCookie(), 100);
622
+ // Ne pas bloquer la validation UI de sélection sur des requêtes status/wallet/storage.
623
+ void refreshBasicStatus();
624
+ setTimeout(() => {
625
+ void refreshStorageStatus();
626
+ }, 100);
578
627
  } else {
579
628
  throw new Error("No valid authentication method available");
580
629
  }
@@ -587,7 +636,6 @@ export function LBProvider({
587
636
  lbClient,
588
637
  refreshBasicStatus,
589
638
  refreshStorageStatus,
590
- syncSelectedApiKeyCookie,
591
639
  ]
592
640
  );
593
641
 
@@ -639,6 +687,12 @@ export function LBProvider({
639
687
  }
640
688
  }, [lbClient, state.status]);
641
689
 
690
+ useEffect(() => {
691
+ if (state.status !== "ready" || !state.session) return;
692
+ if (hasSelectedApiKeyCookie) return;
693
+ setState((prev) => ({ ...prev, selectedKey: undefined }));
694
+ }, [hasSelectedApiKeyCookie, state.session, state.status]);
695
+
642
696
  // Refresh status uniquement lors de la transition vers "ready"
643
697
  // (évite les boucles status/user causées par des callbacks recréés)
644
698
  useEffect(() => {
package/src/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 {