@lastbrain/ai-ui-react 1.0.63 → 1.0.65

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.
@@ -10,6 +10,7 @@ import {
10
10
  useContext,
11
11
  useEffect,
12
12
  useCallback,
13
+ useMemo,
13
14
  useState,
14
15
  type ReactNode,
15
16
  } from "react";
@@ -21,6 +22,7 @@ import type {
21
22
  LBSessionResult,
22
23
  AiStatus,
23
24
  } from "@lastbrain/ai-ui-core";
25
+ import { createLBClient } from "@lastbrain/ai-ui-core";
24
26
 
25
27
  interface LBProviderProps {
26
28
  children: ReactNode;
@@ -100,69 +102,81 @@ export function LBProvider({
100
102
  const [isLoadingStorage, setIsLoadingStorage] = useState(false);
101
103
  const [storageLastFetch, setStorageLastFetch] = useState<number>(0);
102
104
 
105
+ const lbClient = useMemo(
106
+ () =>
107
+ createLBClient({
108
+ baseUrl: proxyUrl,
109
+ mode: process.env.LB_API_KEY ? "env-key" : "auto",
110
+ selectedApiKeyId: state.selectedKey?.id,
111
+ }),
112
+ [proxyUrl, state.selectedKey?.id]
113
+ );
114
+
103
115
  /**
104
116
  * Vérifie si une session existe au chargement
105
117
  */
106
118
  const checkSession = useCallback(async () => {
107
119
  try {
108
- const response = await fetch(`${proxyUrl}/auth/session/verify`, {
109
- credentials: "include",
110
- });
111
-
112
- if (response.ok) {
113
- const session: LBSession = await response.json();
114
-
115
- // Récupérer les infos utilisateur depuis /auth/status
116
- try {
117
- const statusResponse = await fetch(`${proxyUrl}/auth/status`, {
118
- credentials: "include",
119
- });
120
-
121
- if (statusResponse.ok) {
122
- const statusData = await statusResponse.json();
123
- setState({
124
- status: "ready",
125
- session,
126
- user: {
127
- id: session.userId,
128
- email: statusData.user?.email || "",
129
- },
130
- });
131
- } else {
132
- // Fallback sans email
133
- setState({
134
- status: "ready",
135
- session,
136
- user: {
137
- id: session.userId,
138
- email: "",
139
- },
140
- });
141
- }
142
- } catch (statusError) {
143
- console.error("[LBProvider] Failed to fetch status:", statusError);
144
- // Fallback sans email
120
+ const session = await lbClient.verifySession();
121
+ if (session) {
122
+ const userData = await lbClient.getUser().catch(() => null);
123
+ const activeKey = userData?.apiKeyActive
124
+ ? {
125
+ id: userData.apiKeyActive.id,
126
+ name: userData.apiKeyActive.name,
127
+ keyPrefix: userData.apiKeyActive.prefix,
128
+ scopes: userData.apiKeyActive.scopes || [],
129
+ isActive: true,
130
+ createdAt: "",
131
+ }
132
+ : undefined;
133
+ setApiKeys(userData?.apiKeys || []);
134
+ setState({
135
+ status: "ready",
136
+ session,
137
+ user: {
138
+ id: session.userId,
139
+ email: userData?.user?.email || "",
140
+ },
141
+ selectedKey: activeKey,
142
+ });
143
+ onStatusChange?.("ready");
144
+ } else {
145
+ // Supabase session mode (no lb_session cookie): try user endpoint directly
146
+ const userData = await lbClient.getUser().catch(() => null);
147
+ if (userData?.user?.id) {
148
+ const activeKey = userData?.apiKeyActive
149
+ ? {
150
+ id: userData.apiKeyActive.id,
151
+ name: userData.apiKeyActive.name,
152
+ keyPrefix: userData.apiKeyActive.prefix,
153
+ scopes: userData.apiKeyActive.scopes || [],
154
+ isActive: true,
155
+ createdAt: "",
156
+ }
157
+ : undefined;
158
+
159
+ setApiKeys(userData.apiKeys || []);
145
160
  setState({
146
161
  status: "ready",
147
- session,
148
162
  user: {
149
- id: session.userId,
150
- email: "",
163
+ id: userData.user.id,
164
+ email: userData.user.email || "",
151
165
  },
166
+ selectedKey: activeKey,
152
167
  });
168
+ onStatusChange?.("ready");
169
+ } else {
170
+ setState({ status: "needs_auth" });
171
+ onStatusChange?.("needs_auth");
153
172
  }
154
-
155
- onStatusChange?.("ready");
156
- } else {
157
- setState({ status: "needs_auth" });
158
- onStatusChange?.("needs_auth");
159
173
  }
160
174
  } catch (error) {
161
175
  console.error("[LBProvider] Session check failed:", error);
162
176
  setState({ status: "needs_auth" });
163
177
  onStatusChange?.("needs_auth");
164
178
  }
165
- }, [proxyUrl, onStatusChange]);
179
+ }, [lbClient, onStatusChange]);
166
180
 
167
181
  useEffect(() => {
168
182
  checkSession();
@@ -174,31 +188,8 @@ export function LBProvider({
174
188
  const fetchApiKeys = useCallback(
175
189
  async (token: string): Promise<LBApiKey[]> => {
176
190
  try {
177
- console.log(
178
- "[LBProvider] Fetching API keys with token:",
179
- token.substring(0, 20) + "..."
180
- );
181
-
182
- const response = await fetch(`${proxyUrl}/public/user/api-keys`, {
183
- headers: {
184
- Authorization: `Bearer ${token}`,
185
- "Content-Type": "application/json",
186
- },
187
- credentials: "include",
188
- });
189
-
190
- console.log("[LBProvider] API keys response status:", response.status);
191
-
192
- if (!response.ok) {
193
- const errorData = await response.json().catch(() => ({}));
194
- console.error("[LBProvider] Failed to fetch API keys:", errorData);
195
- throw new Error(errorData.message || "Failed to fetch API keys");
196
- }
197
-
198
- const data = await response.json();
199
- console.log("[LBProvider] API keys received:", data);
200
-
201
- const keys: LBApiKey[] = data.apiKeys || data;
191
+ const data = await lbClient.getUser(token);
192
+ const keys: LBApiKey[] = data.apiKeys || [];
202
193
  setApiKeys(keys);
203
194
  return keys;
204
195
  } catch (error) {
@@ -206,7 +197,7 @@ export function LBProvider({
206
197
  throw error;
207
198
  }
208
199
  },
209
- [proxyUrl]
200
+ [lbClient]
210
201
  );
211
202
 
212
203
  /**
@@ -217,26 +208,10 @@ export function LBProvider({
217
208
  try {
218
209
  console.log("[LBProvider] Selecting API key:", apiKeyId);
219
210
  setState((prev: LBAuthState) => ({ ...prev, status: "loading" }));
220
-
221
- const response = await fetch(`${proxyUrl}/auth/session`, {
222
- method: "POST",
223
- headers: {
224
- "Content-Type": "application/json",
225
- Authorization: `Bearer ${token}`,
226
- },
227
- body: JSON.stringify({ api_key_id: apiKeyId }),
228
- credentials: "include",
229
- });
230
-
231
- console.log("[LBProvider] Session response status:", response.status);
232
-
233
- if (!response.ok) {
234
- const errorData = await response.json().catch(() => ({}));
235
- console.error("[LBProvider] Failed to create session:", errorData);
236
- throw new Error(errorData.message || "Failed to create session");
237
- }
238
-
239
- const sessionResult: LBSessionResult = await response.json();
211
+ const sessionResult: LBSessionResult = await lbClient.selectApiKey(
212
+ apiKeyId,
213
+ token
214
+ );
240
215
  console.log(
241
216
  "[LBProvider] Session created successfully:",
242
217
  sessionResult
@@ -268,7 +243,7 @@ export function LBProvider({
268
243
  throw error;
269
244
  }
270
245
  },
271
- [proxyUrl, state.user, onStatusChange, onAuthChange]
246
+ [lbClient, state.user, onStatusChange, onAuthChange]
272
247
  );
273
248
 
274
249
  /**
@@ -289,27 +264,7 @@ export function LBProvider({
289
264
  console.log("[LBProvider] Login attempt:", email);
290
265
  setState((prev: LBAuthState) => ({ ...prev, status: "loading" }));
291
266
 
292
- const response = await fetch(`${proxyUrl}/auth/login`, {
293
- method: "POST",
294
- headers: { "Content-Type": "application/json" },
295
- body: JSON.stringify({ email, password }),
296
- credentials: "include",
297
- });
298
-
299
- console.log("[LBProvider] Login response status:", response.status);
300
-
301
- if (!response.ok) {
302
- const error = await response.json();
303
- const errorMessage = error.message || "Login failed";
304
- console.error("[LBProvider] Login failed:", errorMessage);
305
- setState({
306
- status: "needs_auth",
307
- error: errorMessage,
308
- });
309
- return { success: false, error: errorMessage };
310
- }
311
-
312
- const result: LBLoginResult = await response.json();
267
+ const result: LBLoginResult = await lbClient.login(email, password);
313
268
  console.log("[LBProvider] Login successful:", result.user?.email);
314
269
  console.log(
315
270
  "[LBProvider] Access token received:",
@@ -403,7 +358,7 @@ export function LBProvider({
403
358
  return { success: false, error: message };
404
359
  }
405
360
  },
406
- [proxyUrl, fetchApiKeys, selectApiKey]
361
+ [lbClient, fetchApiKeys, selectApiKey]
407
362
  );
408
363
 
409
364
  /**
@@ -418,30 +373,50 @@ export function LBProvider({
418
373
 
419
374
  setIsLoadingStatus(true);
420
375
  try {
421
- const response = await fetch(`${proxyUrl}/auth/status?fast=true`, {
422
- credentials: "include",
423
- });
424
-
425
- if (response.ok) {
426
- const data = await response.json();
427
- setBasicStatus(data);
428
-
429
- // Combiner avec le storage existant si disponible
430
- const combinedStatus = {
431
- ...data,
432
- storage: storageStatus?.storage || data.storage,
376
+ let data: any;
377
+ try {
378
+ data = await lbClient.getStatus();
379
+ } catch {
380
+ // Backward compatibility: older backends may not expose /auth/status
381
+ const userData = await lbClient.getUser();
382
+ data = {
383
+ authType: userData.authType,
384
+ user: userData.user,
385
+ apiKey: userData.apiKeyActive,
386
+ api_key: userData.apiKeyActive,
387
+ balance: {
388
+ used: 0,
389
+ total: userData.balance?.sellValueUsd || 0,
390
+ percentage: 0,
391
+ },
433
392
  };
434
- setApiStatus(combinedStatus);
435
- } else {
436
- setBasicStatus(null);
437
393
  }
394
+ const normalizedStatus = {
395
+ ...data,
396
+ authType: data?.authType,
397
+ user: data?.user,
398
+ apiKey: data?.apiKey || data?.api_key,
399
+ api_key: data?.api_key || data?.apiKey,
400
+ balance: data?.balance || {
401
+ used: 0,
402
+ total: 0,
403
+ percentage: 0,
404
+ },
405
+ };
406
+ setBasicStatus(normalizedStatus);
407
+
408
+ const combinedStatus = {
409
+ ...normalizedStatus,
410
+ storage: storageStatus?.storage,
411
+ };
412
+ setApiStatus(combinedStatus as any);
438
413
  } catch (error) {
439
414
  console.error("[LBProvider] Failed to fetch basic status:", error);
440
415
  setBasicStatus(null);
441
416
  } finally {
442
417
  setIsLoadingStatus(false);
443
418
  }
444
- }, [proxyUrl, state.status, storageStatus]);
419
+ }, [lbClient, state.status, storageStatus]);
445
420
 
446
421
  /**
447
422
  * Récupère le storage - LENT avec cache (5 minutes)
@@ -467,42 +442,17 @@ export function LBProvider({
467
442
 
468
443
  setIsLoadingStorage(true);
469
444
  try {
470
- // Essayer l'endpoint spécialisé storage d'abord
471
- let response = await fetch(`${proxyUrl}/auth/status/storage`, {
472
- credentials: "include",
473
- });
445
+ const data = await lbClient.getStorageStatus();
446
+ const storageData = data?.storage ? { storage: data.storage } : data;
447
+ setStorageStatus(storageData);
448
+ setStorageLastFetch(now);
474
449
 
475
- // Si 404, faire un fallback vers l'endpoint normal (backward compatibility)
476
- if (!response.ok && response.status === 404) {
477
- console.log("[LBProvider] Storage endpoint not available, using fallback");
478
- response = await fetch(`${proxyUrl}/auth/status`, {
479
- credentials: "include",
480
- });
481
- }
482
-
483
- if (response.ok) {
484
- const data = await response.json();
485
-
486
- // Si c'est la réponse complète (fallback), extraire juste le storage
487
- const storageData = data.storage ? { storage: data.storage } : data;
488
-
489
- setStorageStatus(storageData);
490
- setStorageLastFetch(now);
491
-
492
- // Combiner avec le basic status
493
- const combinedStatus = {
494
- ...basicStatus,
495
- storage: storageData.storage,
496
- };
497
- setApiStatus(combinedStatus);
498
- } else {
499
- console.warn(
500
- "[LBProvider] Failed to fetch storage status:",
501
- response.status
502
- );
503
- // Arrêter les tentatives répétées si échec persistant
504
- setStorageLastFetch(now); // Marquer comme essayé pour éviter la boucle
505
- }
450
+ // Combiner avec le basic status
451
+ const combinedStatus = {
452
+ ...basicStatus,
453
+ storage: storageData?.storage,
454
+ };
455
+ setApiStatus(combinedStatus);
506
456
  } catch (error) {
507
457
  console.error("[LBProvider] Failed to fetch storage status:", error);
508
458
  // Arrêter les tentatives répétées si erreur persistante
@@ -511,7 +461,7 @@ export function LBProvider({
511
461
  setIsLoadingStorage(false);
512
462
  }
513
463
  },
514
- [proxyUrl, state.status, basicStatus, storageLastFetch]
464
+ [lbClient, state.status, basicStatus, storageLastFetch]
515
465
  );
516
466
 
517
467
  /**
@@ -532,39 +482,41 @@ export function LBProvider({
532
482
  */
533
483
  const switchApiKey = useCallback(
534
484
  async (apiKeyId: string): Promise<void> => {
535
- if (state.status === "ready") {
536
- // Utiliser la route de switch avec session
537
- const response = await fetch(
538
- `${proxyUrl}/auth/session/switch-api-key`,
539
- {
540
- method: "POST",
541
- credentials: "include",
542
- headers: { "Content-Type": "application/json" },
543
- body: JSON.stringify({ api_key_id: apiKeyId }),
485
+ if (state.status === "ready" || accessToken) {
486
+ // lb_session / login token flow: persist selection server-side when possible.
487
+ // api_key flow may legitimately reject this route; we still support local selection
488
+ // via x-lb-api-key-selected header on subsequent requests.
489
+ if (accessToken || state.session?.sessionToken) {
490
+ await lbClient.selectApiKey(apiKeyId, accessToken);
491
+ } else {
492
+ try {
493
+ await lbClient.selectApiKey(apiKeyId, accessToken);
494
+ } catch (error) {
495
+ console.warn(
496
+ "[LBProvider] selectApiKey API call failed, applying local switch only",
497
+ error
498
+ );
544
499
  }
545
- );
546
-
547
- if (!response.ok) {
548
- const errorData = await response.json().catch(() => ({}));
549
- throw new Error(errorData.error || "Failed to switch API key");
550
500
  }
551
-
552
- // Refresh le status après le changement
501
+ const selectedKey = apiKeys.find((key) => key.id === apiKeyId);
502
+ if (selectedKey) {
503
+ setState((prev) => ({
504
+ ...prev,
505
+ selectedKey,
506
+ }));
507
+ }
553
508
  await refreshBasicStatus();
554
- // Refresh storage en arrière-plan
555
509
  setTimeout(() => refreshStorageStatus(), 100);
556
- } else if (accessToken) {
557
- // Utiliser la méthode avec access token
558
- await selectApiKey(accessToken, apiKeyId);
559
510
  } else {
560
511
  throw new Error("No valid authentication method available");
561
512
  }
562
513
  },
563
514
  [
564
515
  state.status,
565
- proxyUrl,
516
+ state.session?.sessionToken,
566
517
  accessToken,
567
- selectApiKey,
518
+ apiKeys,
519
+ lbClient,
568
520
  refreshBasicStatus,
569
521
  refreshStorageStatus,
570
522
  ]
@@ -575,20 +527,22 @@ export function LBProvider({
575
527
  */
576
528
  const logout = useCallback(async (): Promise<void> => {
577
529
  try {
578
- await fetch(`${proxyUrl}/auth/session/logout`, {
579
- method: "POST",
580
- credentials: "include",
581
- });
530
+ await lbClient.logout();
582
531
  } catch (error) {
583
532
  console.error("[LBProvider] Logout failed:", error);
584
533
  } finally {
585
534
  setState({ status: "needs_auth" });
586
535
  setApiKeys([]);
587
536
  setAccessToken(undefined);
537
+ // Reset tous les statuts après logout
538
+ setApiStatus(null);
539
+ setBasicStatus(null);
540
+ setStorageStatus(null);
541
+ setStorageLastFetch(0);
588
542
  onStatusChange?.("needs_auth");
589
543
  onAuthChange?.(); // Refresh provider after logout
590
544
  }
591
- }, [proxyUrl, onStatusChange, onAuthChange]);
545
+ }, [lbClient, onStatusChange, onAuthChange]);
592
546
 
593
547
  /**
594
548
  * Recharge la session
@@ -607,24 +561,13 @@ export function LBProvider({
607
561
  }
608
562
 
609
563
  try {
610
- console.log("[LBProvider] Fetching API keys with session...");
611
- const response = await fetch(`${proxyUrl}/auth/api-keys`, {
612
- credentials: "include",
613
- });
614
-
615
- if (response.ok) {
616
- const data = await response.json();
617
- console.log("[LBProvider] API keys received:", data);
618
- setApiKeys(data.apiKeys || []);
619
- } else {
620
- console.warn("[LBProvider] Failed to fetch API keys:", response.status);
621
- setApiKeys([]);
622
- }
564
+ const data = await lbClient.getUser();
565
+ setApiKeys(data.apiKeys || []);
623
566
  } catch (error) {
624
567
  console.error("[LBProvider] Failed to fetch API keys:", error);
625
568
  setApiKeys([]);
626
569
  }
627
- }, [proxyUrl, state.status]);
570
+ }, [lbClient, state.status]);
628
571
 
629
572
  // Refresh status quand la session devient ready
630
573
  useEffect(() => {
@@ -59,6 +59,13 @@ export function usePrompts(): UsePromptsReturn {
59
59
  const [loading, setLoading] = useState(false);
60
60
  const [error, setError] = useState<string | null>(null);
61
61
 
62
+ const isAuthTokenCandidate = (value?: string): boolean => {
63
+ if (!value) return false;
64
+ const token = value.trim();
65
+ if (!token) return false;
66
+ return token.startsWith("lb_") || token.split(".").length === 3;
67
+ };
68
+
62
69
  const fetchPrompts = useCallback(
63
70
  async (options?: UsePromptsOptions) => {
64
71
  try {
@@ -133,7 +140,7 @@ export function usePrompts(): UsePromptsReturn {
133
140
 
134
141
  const headers: HeadersInit = {};
135
142
  // Ajouter l'API key pour les appels publics directs (pas de proxy externe ni auth interne)
136
- if (isPublicApi && apiKeyId) {
143
+ if (isPublicApi && isAuthTokenCandidate(apiKeyId)) {
137
144
  headers["Authorization"] = `Bearer ${apiKeyId}`;
138
145
  }
139
146
 
@@ -179,7 +186,7 @@ export function usePrompts(): UsePromptsReturn {
179
186
  : "/api/ai/auth/prompts";
180
187
 
181
188
  const headers: HeadersInit = { "Content-Type": "application/json" };
182
- if (!isExternalProxy && apiKeyId) {
189
+ if (!isExternalProxy && isAuthTokenCandidate(apiKeyId)) {
183
190
  headers["Authorization"] = `Bearer ${apiKeyId}`;
184
191
  }
185
192
 
@@ -225,7 +232,7 @@ export function usePrompts(): UsePromptsReturn {
225
232
  : "/api/ai/auth/prompts";
226
233
 
227
234
  const headers: HeadersInit = { "Content-Type": "application/json" };
228
- if (isPublicApi && apiKeyId) {
235
+ if (isPublicApi && isAuthTokenCandidate(apiKeyId)) {
229
236
  headers["Authorization"] = `Bearer ${apiKeyId}`;
230
237
  }
231
238
 
@@ -271,7 +278,7 @@ export function usePrompts(): UsePromptsReturn {
271
278
  : `/api/ai/auth/prompts?id=${id}`;
272
279
 
273
280
  const headers: HeadersInit = {};
274
- if (isPublicApi && apiKeyId) {
281
+ if (isPublicApi && isAuthTokenCandidate(apiKeyId)) {
275
282
  headers["Authorization"] = `Bearer ${apiKeyId}`;
276
283
  }
277
284
 
@@ -7,6 +7,14 @@ export interface ModelToggleOptions {
7
7
  baseUrl?: string; // URL de base de l'API
8
8
  }
9
9
 
10
+ function isAuthTokenCandidate(value?: string): boolean {
11
+ if (!value) return false;
12
+ const token = value.trim();
13
+ if (!token) return false;
14
+ // Raw API key or JWT access token
15
+ return token.startsWith("lb_") || token.split(".").length === 3;
16
+ }
17
+
10
18
  /**
11
19
  * Active ou désactive un modèle pour l'utilisateur courant
12
20
  */
@@ -22,7 +30,7 @@ export async function toggleUserModel(
22
30
  };
23
31
 
24
32
  // Ajouter la clé API si fournie
25
- if (apiKey) {
33
+ if (isAuthTokenCandidate(apiKey)) {
26
34
  headers["Authorization"] = `Bearer ${apiKey}`;
27
35
  }
28
36
 
@@ -77,12 +85,12 @@ export async function getAvailableModels(
77
85
  const isPublicApi = baseUrl && baseUrl.includes("/api/public/v1");
78
86
 
79
87
  const endpoint = isExternalProxy
80
- ? `${baseUrl}/ai/models/available` // Proxy routes to public API
88
+ ? `${baseUrl}/auth/models` // Proxy routes to /api/ai/auth/models
81
89
  : isPublicApi
82
- ? `${baseUrl}/ai/models/available` // → /api/public/v1/ai/models/available
90
+ ? `${baseUrl}/gateway-models` // → /api/public/v1/gateway-models
83
91
  : baseUrl
84
- ? `${baseUrl}/auth/ai-models-available` // → /api/ai/auth/ai-models-available
85
- : `/api/ai/auth/ai-models-available`; // fallback
92
+ ? `${baseUrl}/auth/models` // → /api/ai/auth/models
93
+ : `/api/ai/auth/models`; // fallback
86
94
 
87
95
  console.log(
88
96
  "[getAvailableModels] isExternalProxy:",
@@ -96,7 +104,7 @@ export async function getAvailableModels(
96
104
  const headers: Record<string, string> = {};
97
105
 
98
106
  // Ajouter la clé API pour les appels publics directs
99
- if (isPublicApi && apiKey) {
107
+ if (isPublicApi && isAuthTokenCandidate(apiKey)) {
100
108
  headers["Authorization"] = `Bearer ${apiKey}`;
101
109
  }
102
110
 
@@ -113,7 +121,16 @@ export async function getAvailableModels(
113
121
  }
114
122
 
115
123
  const data = await response.json();
116
- return data.models || [];
124
+
125
+ if (Array.isArray(data?.models)) {
126
+ return data.models;
127
+ }
128
+ if (Array.isArray(data?.providers)) {
129
+ return data.providers.flatMap((provider: any) =>
130
+ Array.isArray(provider.models) ? provider.models : []
131
+ );
132
+ }
133
+ return [];
117
134
  }
118
135
 
119
136
  /**
@@ -148,7 +165,7 @@ export async function getUserModels(
148
165
  const headers: Record<string, string> = {};
149
166
 
150
167
  // Ajouter la clé API pour tous les types d'appels si disponible
151
- if (apiKey) {
168
+ if (isAuthTokenCandidate(apiKey)) {
152
169
  if (isPublicApi) {
153
170
  headers["Authorization"] = `Bearer ${apiKey}`;
154
171
  } else {