@lastbrain/ai-ui-react 1.0.75 → 1.0.77

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.
Files changed (49) hide show
  1. package/dist/components/AiContextButton.d.ts +1 -1
  2. package/dist/components/AiContextButton.d.ts.map +1 -1
  3. package/dist/components/AiContextButton.js +17 -1
  4. package/dist/components/AiImageButton.d.ts.map +1 -1
  5. package/dist/components/AiImageButton.js +19 -3
  6. package/dist/components/AiInput.d.ts +1 -1
  7. package/dist/components/AiInput.d.ts.map +1 -1
  8. package/dist/components/AiInput.js +21 -5
  9. package/dist/components/AiPromptPanel.js +30 -6
  10. package/dist/components/AiSelect.d.ts +1 -1
  11. package/dist/components/AiSelect.d.ts.map +1 -1
  12. package/dist/components/AiSelect.js +18 -2
  13. package/dist/components/AiStatusButton.d.ts.map +1 -1
  14. package/dist/components/AiStatusButton.js +18 -4
  15. package/dist/components/AiTextarea.d.ts +2 -1
  16. package/dist/components/AiTextarea.d.ts.map +1 -1
  17. package/dist/components/AiTextarea.js +25 -6
  18. package/dist/components/ErrorToast.js +3 -3
  19. package/dist/components/LBKeyPicker.js +9 -9
  20. package/dist/components/LBSigninModal.d.ts.map +1 -1
  21. package/dist/components/UsageToast.d.ts +3 -3
  22. package/dist/components/UsageToast.d.ts.map +1 -1
  23. package/dist/context/LBAuthProvider.d.ts +2 -0
  24. package/dist/context/LBAuthProvider.d.ts.map +1 -1
  25. package/dist/context/LBAuthProvider.js +43 -10
  26. package/dist/examples/AiImageGenerator.js +1 -1
  27. package/dist/hooks/useAiStatus.d.ts.map +1 -1
  28. package/dist/hooks/useAiStatus.js +58 -5
  29. package/dist/styles.css +3 -3
  30. package/dist/utils/errorHandler.d.ts +2 -2
  31. package/dist/utils/errorHandler.d.ts.map +1 -1
  32. package/package.json +2 -2
  33. package/src/components/AiContextButton.tsx +20 -2
  34. package/src/components/AiImageButton.tsx +21 -3
  35. package/src/components/AiInput.tsx +28 -5
  36. package/src/components/AiPromptPanel.tsx +31 -6
  37. package/src/components/AiSelect.tsx +21 -3
  38. package/src/components/AiStatusButton.tsx +35 -10
  39. package/src/components/AiTextarea.tsx +33 -9
  40. package/src/components/ErrorToast.tsx +3 -3
  41. package/src/components/LBKeyPicker.tsx +10 -10
  42. package/src/components/LBSigninModal.tsx +2 -1
  43. package/src/components/UsageToast.tsx +4 -4
  44. package/src/context/LBAuthProvider.tsx +46 -9
  45. package/src/examples/AiImageGenerator.tsx +1 -1
  46. package/src/hooks/useAiStatus.ts +62 -5
  47. package/src/styles.css +3 -3
  48. package/src/utils/errorHandler.ts +3 -3
  49. package/src/utils/modelManagement.ts +3 -3
@@ -2,7 +2,14 @@
2
2
 
3
3
  import "../styles/register";
4
4
  import type { AiStatus, LBUser } from "@lastbrain/ai-ui-core";
5
- import { useLayoutEffect, useMemo, useRef, useState, useContext } from "react";
5
+ import {
6
+ useLayoutEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ useContext,
11
+ useEffect,
12
+ } from "react";
6
13
  import { createPortal } from "react-dom";
7
14
  import {
8
15
  ArrowRightLeft,
@@ -169,6 +176,8 @@ export function AiStatusButton({
169
176
  let lbSelectedKey: LBApiKey | null = null;
170
177
  let lbRefreshBasicStatus: (() => Promise<void>) | undefined;
171
178
  let lbRefreshStorageStatus: ((force?: boolean) => Promise<void>) | undefined;
179
+ let lbSessionToken: string | undefined;
180
+ let lbHasSelectedApiKeyCookie = false;
172
181
 
173
182
  const lbContext = useContext(LBContext);
174
183
  if (lbContext) {
@@ -185,6 +194,8 @@ export function AiStatusButton({
185
194
  lbSelectedKey = lbContext.selectedKey || null;
186
195
  lbRefreshBasicStatus = lbContext.refreshBasicStatus;
187
196
  lbRefreshStorageStatus = lbContext.refreshStorageStatus;
197
+ lbSessionToken = lbContext.session?.sessionToken;
198
+ lbHasSelectedApiKeyCookie = lbContext.hasSelectedApiKeyCookie;
188
199
  } else {
189
200
  lbStatus = undefined;
190
201
  }
@@ -207,14 +218,17 @@ export function AiStatusButton({
207
218
 
208
219
  const canPortal = typeof document !== "undefined";
209
220
 
210
- const effectiveStatus =
211
- lbStatus === "ready"
212
- ? {
213
- ...(lbApiStatus || {}),
214
- ...(lbBasicStatus || {}),
215
- storage: lbStorageStatus?.storage || lbApiStatus?.storage,
216
- }
217
- : status || null;
221
+ const effectiveStatus = useMemo(
222
+ () =>
223
+ lbStatus === "ready"
224
+ ? {
225
+ ...(lbApiStatus || {}),
226
+ ...(lbBasicStatus || {}),
227
+ storage: lbStorageStatus?.storage || lbApiStatus?.storage,
228
+ }
229
+ : status || null,
230
+ [lbApiStatus, lbBasicStatus, lbStatus, lbStorageStatus, status]
231
+ );
218
232
 
219
233
  const hasApiKeySelected = Boolean(
220
234
  effectiveStatus?.apiKey?.id ||
@@ -227,12 +241,23 @@ export function AiStatusButton({
227
241
  typeof effectiveStatus.authType === "string"
228
242
  ? effectiveStatus.authType
229
243
  : undefined;
244
+ const hasLbSession = Boolean(lbSessionToken);
230
245
  const requiresApiKeySelection =
231
- lbStatus === "ready" && !hasApiKeySelected && apiKeys.length > 0;
246
+ lbStatus === "ready" &&
247
+ hasLbSession &&
248
+ (!lbHasSelectedApiKeyCookie || !hasApiKeySelected) &&
249
+ apiKeys.length > 0;
232
250
  const isApiKeyAuthMode = authTypeValue === "api_key";
233
251
 
234
252
  const [tooltipStyle, setTooltipStyle] = useState<Record<string, string>>({});
235
253
 
254
+ useEffect(() => {
255
+ if (requiresApiKeySelection) {
256
+ setShowApiKeySelector(true);
257
+ setShowTooltip(false);
258
+ }
259
+ }, [requiresApiKeySelection]);
260
+
236
261
  useLayoutEffect(() => {
237
262
  if (!showTooltip || !buttonRef.current || !canPortal) {
238
263
  return;
@@ -27,6 +27,7 @@ export interface AiTextareaProps
27
27
  uiMode?: "modal" | "drawer";
28
28
  size?: AiSize;
29
29
  radius?: AiRadius;
30
+ onPanelOpenChange?: (isOpen: boolean) => void;
30
31
  }
31
32
 
32
33
  export function AiTextarea({
@@ -35,10 +36,11 @@ export function AiTextarea({
35
36
  uiMode = "modal",
36
37
  size = "md",
37
38
  radius = "lg",
39
+ onPanelOpenChange,
38
40
  context,
39
41
  model,
40
42
  prompt,
41
- editMode = false,
43
+ editMode: _editMode = false,
42
44
  enableModelManagement,
43
45
  storeOutputs,
44
46
  artifactTitle,
@@ -61,12 +63,21 @@ export function AiTextarea({
61
63
 
62
64
  // Rendre l'authentification optionnelle
63
65
  let lbStatus: string | undefined;
66
+ let lbHasSession = false;
67
+ let lbHasSelectedKey = false;
68
+ let lbHasSelectedApiKeyCookie = false;
69
+ let hasLBProvider = false;
64
70
  try {
65
71
  const lbContext = useLB();
66
72
  lbStatus = lbContext.status;
73
+ lbHasSession = Boolean(lbContext.session?.sessionToken);
74
+ lbHasSelectedKey = Boolean(lbContext.selectedKey?.id);
75
+ lbHasSelectedApiKeyCookie = lbContext.hasSelectedApiKeyCookie;
76
+ hasLBProvider = true;
67
77
  } catch {
68
78
  // LBProvider n'est pas disponible, ignorer
69
79
  lbStatus = undefined;
80
+ hasLBProvider = false;
70
81
  }
71
82
 
72
83
  let ctxBaseUrl: string | undefined;
@@ -82,8 +93,13 @@ export function AiTextarea({
82
93
 
83
94
  const baseUrl = propBaseUrl ?? ctxBaseUrl;
84
95
  const apiKeyId = propApiKeyId ?? ctxApiKeyId;
96
+ const needsApiKeySelection =
97
+ hasLBProvider &&
98
+ lbStatus === "ready" &&
99
+ lbHasSession &&
100
+ (!lbHasSelectedKey || !lbHasSelectedApiKeyCookie);
85
101
 
86
- const { models } = useAiModels({
102
+ const { models: _models } = useAiModels({
87
103
  baseUrl,
88
104
  apiKeyId,
89
105
  modelType: "text-or-language",
@@ -92,7 +108,10 @@ export function AiTextarea({
92
108
  const { formatted: loadingElapsed } = useLoadingTimer(loading);
93
109
 
94
110
  const hasConfiguration = Boolean(model && prompt);
95
- const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
111
+ const isAuthReady = hasLBProvider
112
+ ? !needsApiKeySelection &&
113
+ (lbStatus === "ready" || Boolean(process.env.LB_API_KEY))
114
+ : Boolean(process.env.LB_API_KEY);
96
115
  const shouldShowSparkles = isAuthReady && !disabled;
97
116
 
98
117
  const handleOpenPanel = () => {
@@ -101,16 +120,18 @@ export function AiTextarea({
101
120
  return;
102
121
  }
103
122
  setIsOpen(true);
123
+ onPanelOpenChange?.(true);
104
124
  };
105
125
 
106
126
  const handleClosePanel = () => {
107
127
  setIsOpen(false);
128
+ onPanelOpenChange?.(false);
108
129
  };
109
130
 
110
131
  const handleSubmit = async (
111
132
  selectedModel: string,
112
133
  selectedPrompt: string,
113
- promptId?: string
134
+ _promptId?: string
114
135
  ) => {
115
136
  try {
116
137
  const resolvedContext = textareaValue || context || undefined;
@@ -141,13 +162,14 @@ export function AiTextarea({
141
162
  });
142
163
  showUsageToast(result);
143
164
  }
144
- } catch (error) {
165
+ } catch {
145
166
  onToast?.({
146
167
  type: "error",
147
168
  message: t("ai.generationError", "Failed to generate text"),
148
169
  });
149
170
  } finally {
150
171
  setIsOpen(false);
172
+ onPanelOpenChange?.(false);
151
173
  }
152
174
  };
153
175
 
@@ -280,10 +302,12 @@ export function AiTextarea({
280
302
  onComplete={clearToast}
281
303
  />
282
304
  )}
283
- <LBSigninModal
284
- isOpen={showAuthModal}
285
- onClose={() => setShowAuthModal(false)}
286
- />
305
+ {hasLBProvider ? (
306
+ <LBSigninModal
307
+ isOpen={showAuthModal}
308
+ onClose={() => setShowAuthModal(false)}
309
+ />
310
+ ) : null}
287
311
  </div>
288
312
  );
289
313
  }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import "../styles/register";
4
- import { useEffect, useRef, useState } from "react";
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
5
  import { X, AlertCircle } from "lucide-react";
6
6
  import { useI18n } from "../context/I18nContext";
7
7
 
@@ -27,7 +27,7 @@ export function ErrorToast({
27
27
  const fadeTimeoutRef = useRef<number | null>(null);
28
28
  const autoCloseTimeoutRef = useRef<number | null>(null);
29
29
 
30
- const handleClose = () => {
30
+ const handleClose = useCallback(() => {
31
31
  if (isClosing) return;
32
32
 
33
33
  // Clear auto-close timeout if user closes manually
@@ -40,7 +40,7 @@ export function ErrorToast({
40
40
  setIsVisible(false);
41
41
  onComplete?.();
42
42
  }, 200);
43
- };
43
+ }, [isClosing, onComplete]);
44
44
 
45
45
  useEffect(() => {
46
46
  if (error) {
@@ -6,7 +6,7 @@ import "../styles/register";
6
6
  * Permet de changer de clé API sans se reconnecter
7
7
  */
8
8
 
9
- import { useEffect, useState } from "react";
9
+ import { useCallback, useEffect, useState } from "react";
10
10
  import { useLB } from "../hooks/useLB";
11
11
  import type { LBApiKey } from "@lastbrain/ai-ui-core";
12
12
  import { useI18n } from "../context/I18nContext";
@@ -30,25 +30,25 @@ export function LBKeyPicker({
30
30
  const [error, setError] = useState("");
31
31
  const [showDropdown, setShowDropdown] = useState(false);
32
32
 
33
- useEffect(() => {
34
- if (status === "ready" && accessToken) {
35
- loadKeys();
36
- }
37
- }, [status, accessToken]);
38
-
39
- const loadKeys = async () => {
33
+ const loadKeys = useCallback(async () => {
40
34
  if (!accessToken) return;
41
35
 
42
36
  try {
43
37
  setLoading(true);
44
38
  const keys = await fetchApiKeys(accessToken);
45
39
  setApiKeys(keys);
46
- } catch (err) {
40
+ } catch {
47
41
  setError(t("lb.keypicker.loadError", "Unable to load API keys"));
48
42
  } finally {
49
43
  setLoading(false);
50
44
  }
51
- };
45
+ }, [accessToken, fetchApiKeys, t]);
46
+
47
+ useEffect(() => {
48
+ if (status === "ready" && accessToken) {
49
+ loadKeys();
50
+ }
51
+ }, [accessToken, loadKeys, status]);
52
52
 
53
53
  const handleSelectKey = async (keyId: string) => {
54
54
  if (!accessToken) return;
@@ -7,6 +7,7 @@ import { AlertCircle, Loader2, Lock, Mail, Sparkles, X } from "lucide-react";
7
7
  import { useLB } from "../context/LBAuthProvider";
8
8
  import { LBApiKeySelector } from "./LBApiKeySelector";
9
9
  import { useI18n } from "../context/I18nContext";
10
+ import type { LBApiKey } from "@lastbrain/ai-ui-core";
10
11
 
11
12
  export interface LBSigninModalProps {
12
13
  isOpen: boolean;
@@ -23,7 +24,7 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
23
24
  const [loading, setLoading] = useState(false);
24
25
  const [error, setError] = useState("");
25
26
  const [showKeySelector, setShowKeySelector] = useState(false);
26
- const [currentApiKeys, setCurrentApiKeys] = useState<any[]>([]);
27
+ const [currentApiKeys, setCurrentApiKeys] = useState<LBApiKey[]>([]);
27
28
 
28
29
  const { login, selectApiKeyWithToken, fetchApiKeys } = lbContext || {};
29
30
 
@@ -6,7 +6,7 @@ import { X } from "lucide-react";
6
6
  import { useI18n } from "../context/I18nContext";
7
7
 
8
8
  interface UsageToastProps {
9
- result: unknown;
9
+ result: any;
10
10
  position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
11
11
  onComplete?: () => void;
12
12
  }
@@ -46,7 +46,7 @@ export function UsageToast({
46
46
  }, 200);
47
47
  };
48
48
 
49
- const extractUsageMessage = (data: unknown) => {
49
+ const extractUsageMessage = (data: any) => {
50
50
  const result = data as any;
51
51
 
52
52
  // Extract cost from various possible locations
@@ -165,10 +165,10 @@ export function UsageToast({
165
165
  }
166
166
 
167
167
  export function useUsageToast() {
168
- const [toastData, setToastData] = useState<unknown>(null);
168
+ const [toastData, setToastData] = useState<any>(null);
169
169
  const [toastKey, setToastKey] = useState(0);
170
170
 
171
- const showUsageToast = (result: unknown) => {
171
+ const showUsageToast = (result: any) => {
172
172
  // Replace any existing toast with new one
173
173
  setToastKey((prev) => prev + 1);
174
174
  setToastData(result);
@@ -11,6 +11,7 @@ import {
11
11
  useEffect,
12
12
  useCallback,
13
13
  useMemo,
14
+ useRef,
14
15
  useState,
15
16
  type ReactNode,
16
17
  } from "react";
@@ -114,6 +115,8 @@ interface LBContextValue extends LBAuthState {
114
115
  isLoadingStatus: boolean;
115
116
  /** Indique si le storage est en cours de chargement */
116
117
  isLoadingStorage: boolean;
118
+ /** True si le cookie api_key_selected est présent */
119
+ hasSelectedApiKeyCookie: boolean;
117
120
  }
118
121
 
119
122
  const LBContext = createContext<LBContextValue | undefined>(undefined);
@@ -142,6 +145,17 @@ export function LBProvider({
142
145
  const [isLoadingStatus, setIsLoadingStatus] = useState(false);
143
146
  const [isLoadingStorage, setIsLoadingStorage] = useState(false);
144
147
  const [storageLastFetch, setStorageLastFetch] = useState<number>(0);
148
+ const [hasSelectedApiKeyCookie, setHasSelectedApiKeyCookie] = useState(false);
149
+ const previousStatusRef = useRef<LBAuthState["status"]>("loading");
150
+
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
+ }, []);
145
159
 
146
160
  const lbClient = useMemo(
147
161
  () =>
@@ -223,6 +237,13 @@ export function LBProvider({
223
237
  checkSession();
224
238
  }, [checkSession]);
225
239
 
240
+ useEffect(() => {
241
+ syncSelectedApiKeyCookie();
242
+ if (typeof window === "undefined") return;
243
+ const interval = window.setInterval(syncSelectedApiKeyCookie, 1000);
244
+ return () => window.clearInterval(interval);
245
+ }, [syncSelectedApiKeyCookie]);
246
+
226
247
  /**
227
248
  * Récupère les clés API de l'utilisateur
228
249
  */
@@ -272,6 +293,7 @@ export function LBProvider({
272
293
 
273
294
  setAccessToken(undefined); // Nettoyer l'access token temporaire
274
295
  setApiKeys([]); // Nettoyer les clés API temporaires
296
+ setTimeout(() => syncSelectedApiKeyCookie(), 100);
275
297
  onStatusChange?.("ready");
276
298
  onAuthChange?.(); // Refresh provider after signin
277
299
  } catch (error) {
@@ -284,7 +306,7 @@ export function LBProvider({
284
306
  throw error;
285
307
  }
286
308
  },
287
- [lbClient, state.user, onStatusChange, onAuthChange]
309
+ [lbClient, onAuthChange, onStatusChange, state.user, syncSelectedApiKeyCookie]
288
310
  );
289
311
 
290
312
  /**
@@ -416,7 +438,7 @@ export function LBProvider({
416
438
  try {
417
439
  let data: BasicStatus;
418
440
  try {
419
- data = await lbClient.getStatus();
441
+ data = (await lbClient.getStatus()) as BasicStatus;
420
442
  } catch {
421
443
  // Backward compatibility: older backends may not expose /auth/status
422
444
  const userData = await lbClient.getUser();
@@ -483,15 +505,17 @@ export function LBProvider({
483
505
 
484
506
  setIsLoadingStorage(true);
485
507
  try {
486
- const data = await lbClient.getStorageStatus();
487
- const storageData = data?.storage ? { storage: data.storage } : data;
508
+ const data = (await lbClient.getStorageStatus()) as StorageStatus;
509
+ const storageData: StorageStatus = data?.storage
510
+ ? { storage: data.storage }
511
+ : data;
488
512
  setStorageStatus(storageData);
489
513
  setStorageLastFetch(now);
490
514
 
491
515
  // Combiner avec le basic status
492
516
  const combinedStatus = {
493
517
  ...basicStatus,
494
- storage: storageData?.storage,
518
+ storage: storageData.storage,
495
519
  };
496
520
  setApiStatus(combinedStatus as AiStatus);
497
521
  } catch (error) {
@@ -548,6 +572,7 @@ export function LBProvider({
548
572
  }
549
573
  await refreshBasicStatus();
550
574
  setTimeout(() => refreshStorageStatus(), 100);
575
+ setTimeout(() => syncSelectedApiKeyCookie(), 100);
551
576
  } else {
552
577
  throw new Error("No valid authentication method available");
553
578
  }
@@ -560,6 +585,7 @@ export function LBProvider({
560
585
  lbClient,
561
586
  refreshBasicStatus,
562
587
  refreshStorageStatus,
588
+ syncSelectedApiKeyCookie,
563
589
  ]
564
590
  );
565
591
 
@@ -580,6 +606,7 @@ export function LBProvider({
580
606
  setBasicStatus(null);
581
607
  setStorageStatus(null);
582
608
  setStorageLastFetch(0);
609
+ setHasSelectedApiKeyCookie(false);
583
610
  onStatusChange?.("needs_auth");
584
611
  onAuthChange?.(); // Refresh provider after logout
585
612
  }
@@ -610,21 +637,30 @@ export function LBProvider({
610
637
  }
611
638
  }, [lbClient, state.status]);
612
639
 
613
- // Refresh status quand la session devient ready
640
+ // Refresh status uniquement lors de la transition vers "ready"
641
+ // (évite les boucles status/user causées par des callbacks recréés)
614
642
  useEffect(() => {
615
- if (state.status === "ready") {
643
+ const wasReady = previousStatusRef.current === "ready";
644
+ previousStatusRef.current = state.status;
645
+
646
+ if (state.status === "ready" && !wasReady) {
616
647
  // Appel rapide d'abord
617
648
  refreshBasicStatus();
618
649
  // Storage en arrière-plan après 100ms
619
650
  setTimeout(() => refreshStorageStatus(), 100);
620
651
  fetchApiKeysWithSession(); // Also fetch API keys list
621
- } else {
652
+ } else if (state.status !== "ready") {
622
653
  setApiStatus(null);
623
654
  setBasicStatus(null);
624
655
  setStorageStatus(null);
625
656
  setApiKeys([]);
626
657
  }
627
- }, [state.status]); // Supprimer les fonctions des dépendances pour éviter la boucle
658
+ }, [
659
+ fetchApiKeysWithSession,
660
+ refreshBasicStatus,
661
+ refreshStorageStatus,
662
+ state.status,
663
+ ]);
628
664
 
629
665
  const value: LBContextValue = {
630
666
  ...state,
@@ -645,6 +681,7 @@ export function LBProvider({
645
681
  refreshStorageStatus,
646
682
  isLoadingStatus,
647
683
  isLoadingStorage,
684
+ hasSelectedApiKeyCookie,
648
685
  };
649
686
 
650
687
  return (
@@ -125,7 +125,7 @@ export function ModelManagementList({
125
125
  }) {
126
126
  const {
127
127
  availableModels,
128
- userModels,
128
+ userModels: _userModels,
129
129
  loading,
130
130
  error,
131
131
  toggleModel,
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect, useCallback } from "react";
3
+ import { useState, useEffect, useCallback, useContext, useMemo } from "react";
4
4
  import type { AiStatus } from "@lastbrain/ai-ui-core";
5
5
  import { useAiClient } from "./useAiClient";
6
+ import { LBContext } from "../context/LBAuthProvider";
6
7
 
7
8
  export interface UseAiStatusOptions {
8
9
  baseUrl?: string;
@@ -18,11 +19,48 @@ export interface UseAiStatusResult {
18
19
 
19
20
  export function useAiStatus(options?: UseAiStatusOptions): UseAiStatusResult {
20
21
  const client = useAiClient(options);
22
+ const lbContext = useContext(LBContext);
21
23
  const [status, setStatus] = useState<AiStatus | null>(null);
22
24
  const [loading, setLoading] = useState(false);
23
25
  const [error, setError] = useState<Error | null>(null);
24
26
 
27
+ const statusFromLB = useMemo<AiStatus | null>(() => {
28
+ if (!lbContext || lbContext.status !== "ready") {
29
+ return null;
30
+ }
31
+ if (lbContext.apiStatus) {
32
+ return lbContext.apiStatus;
33
+ }
34
+ if (lbContext.basicStatus) {
35
+ return {
36
+ ...(lbContext.basicStatus as AiStatus),
37
+ storage:
38
+ lbContext.storageStatus?.storage ||
39
+ (lbContext.basicStatus as AiStatus)?.storage,
40
+ } as AiStatus;
41
+ }
42
+ return null;
43
+ }, [lbContext]);
44
+
25
45
  const fetchStatus = useCallback(async () => {
46
+ // If LBProvider exists, never call getStatus directly here:
47
+ // - ready: delegate refresh to provider
48
+ // - non-ready: avoid unauthorized polling loops
49
+ if (lbContext) {
50
+ if (lbContext.status === "ready") {
51
+ try {
52
+ await lbContext.refreshBasicStatus();
53
+ } catch {
54
+ // Ignore: provider already tracks errors/status
55
+ }
56
+ } else {
57
+ setStatus(null);
58
+ setLoading(false);
59
+ setError(null);
60
+ }
61
+ return;
62
+ }
63
+
26
64
  console.log("[useAiStatus] Starting status fetch");
27
65
 
28
66
  setLoading(true);
@@ -39,15 +77,34 @@ export function useAiStatus(options?: UseAiStatusOptions): UseAiStatusResult {
39
77
  } finally {
40
78
  setLoading(false);
41
79
  }
42
- }, [client]);
80
+ }, [client, lbContext]);
43
81
 
44
82
  useEffect(() => {
83
+ if (lbContext && lbContext.status !== "ready") {
84
+ setStatus(null);
85
+ setLoading(false);
86
+ setError(null);
87
+ return;
88
+ }
89
+
90
+ if (statusFromLB) {
91
+ setStatus(statusFromLB);
92
+ setLoading(false);
93
+ setError(null);
94
+ return;
95
+ }
45
96
  fetchStatus();
46
- }, [fetchStatus]);
97
+ }, [fetchStatus, lbContext, statusFromLB]);
98
+
99
+ const isLoadingFromLB =
100
+ !!lbContext &&
101
+ lbContext.status === "ready" &&
102
+ !statusFromLB &&
103
+ (lbContext.isLoadingStatus || lbContext.isLoadingStorage);
47
104
 
48
105
  return {
49
- status,
50
- loading,
106
+ status: statusFromLB || status,
107
+ loading: isLoadingFromLB || loading,
51
108
  error,
52
109
  refetch: fetchStatus,
53
110
  };
package/src/styles.css CHANGED
@@ -195,7 +195,7 @@
195
195
  align-items: center;
196
196
  gap: 10px;
197
197
  min-height: var(--ai-control-h, var(--ai-size-md-h));
198
- padding: 0 12px;
198
+ padding: 0 8px;
199
199
  border-radius: var(--ai-radius-current, var(--ai-radius-full));
200
200
  border: 1px solid var(--ai-border);
201
201
  background:
@@ -362,8 +362,8 @@
362
362
  .ai-shell--textarea .ai-control-action,
363
363
  .ai-shell--textarea .ai-spark {
364
364
  position: absolute;
365
- right: 12px;
366
- bottom: 12px;
365
+ right: 8px;
366
+ bottom: 8px;
367
367
  }
368
368
 
369
369
  /* Generic buttons */
@@ -13,7 +13,7 @@ type NormalizedErrorLike = {
13
13
  message: string;
14
14
  };
15
15
 
16
- function isNormalizedErrorLike(value: unknown): value is NormalizedErrorLike {
16
+ function isNormalizedErrorLike(value: any): value is NormalizedErrorLike {
17
17
  if (!value || typeof value !== "object") {
18
18
  return false;
19
19
  }
@@ -24,7 +24,7 @@ function isNormalizedErrorLike(value: unknown): value is NormalizedErrorLike {
24
24
  /**
25
25
  * Parse et uniformise la gestion des erreurs des composants AI
26
26
  */
27
- export function parseAIError(error: unknown): ParsedError {
27
+ export function parseAIError(error: any): ParsedError {
28
28
  // Si l'erreur est déjà un objet normalisé
29
29
  if (isNormalizedErrorLike(error)) {
30
30
  return {
@@ -173,7 +173,7 @@ export interface ErrorToastCallback {
173
173
  * @param showInternalToast Callback interne optionnelle pour afficher un toast dans le composant
174
174
  */
175
175
  export function handleAIError(
176
- error: unknown,
176
+ error: any,
177
177
  onToast?: ErrorToastCallback,
178
178
  showInternalToast?: (error: { message: string; code?: string }) => void
179
179
  ): void {
@@ -15,7 +15,7 @@ interface ProvidersResponse {
15
15
  providers?: Provider[];
16
16
  }
17
17
 
18
- interface ModelsResponse {
18
+ interface _ModelsResponse {
19
19
  models?: ModelInfo[];
20
20
  }
21
21
 
@@ -32,7 +32,7 @@ interface ModelInfo {
32
32
  costPer1M?: number;
33
33
  }
34
34
 
35
- function isModelCategory(value: unknown): value is ModelCategory {
35
+ function isModelCategory(value: any): value is ModelCategory {
36
36
  return (
37
37
  value === "text" ||
38
38
  value === "image" ||
@@ -41,7 +41,7 @@ function isModelCategory(value: unknown): value is ModelCategory {
41
41
  );
42
42
  }
43
43
 
44
- function isModelInfo(value: unknown): value is ModelInfo {
44
+ function isModelInfo(value: any): value is ModelInfo {
45
45
  if (!value || typeof value !== "object") return false;
46
46
  const v = value as Record<string, unknown>;
47
47
  return (