@lastbrain/ai-ui-react 1.0.74 → 1.0.76

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 (63) hide show
  1. package/dist/components/AiChipLabel.d.ts.map +1 -1
  2. package/dist/components/AiContextButton.d.ts +1 -1
  3. package/dist/components/AiContextButton.d.ts.map +1 -1
  4. package/dist/components/AiContextButton.js +3 -2
  5. package/dist/components/AiImageButton.d.ts.map +1 -1
  6. package/dist/components/AiImageButton.js +6 -3
  7. package/dist/components/AiInput.d.ts +1 -1
  8. package/dist/components/AiInput.d.ts.map +1 -1
  9. package/dist/components/AiInput.js +4 -4
  10. package/dist/components/AiModelSelect.d.ts.map +1 -1
  11. package/dist/components/AiPromptPanel.js +30 -6
  12. package/dist/components/AiSelect.d.ts +1 -1
  13. package/dist/components/AiSelect.d.ts.map +1 -1
  14. package/dist/components/AiSelect.js +1 -1
  15. package/dist/components/AiStatusButton.d.ts.map +1 -1
  16. package/dist/components/AiStatusButton.js +2 -2
  17. package/dist/components/AiTextarea.d.ts +2 -1
  18. package/dist/components/AiTextarea.d.ts.map +1 -1
  19. package/dist/components/AiTextarea.js +16 -6
  20. package/dist/components/ErrorToast.js +3 -3
  21. package/dist/components/LBConnectButton.d.ts.map +1 -1
  22. package/dist/components/LBConnectButton.js +1 -3
  23. package/dist/components/LBKeyPicker.js +9 -9
  24. package/dist/components/LBSigninModal.d.ts.map +1 -1
  25. package/dist/components/UsageToast.d.ts +3 -3
  26. package/dist/components/UsageToast.d.ts.map +1 -1
  27. package/dist/context/I18nContext.d.ts +1 -1
  28. package/dist/context/I18nContext.d.ts.map +1 -1
  29. package/dist/context/I18nContext.js +1 -1
  30. package/dist/context/LBAuthProvider.d.ts.map +1 -1
  31. package/dist/context/LBAuthProvider.js +12 -5
  32. package/dist/examples/AiImageGenerator.js +1 -1
  33. package/dist/hooks/useAiStatus.d.ts.map +1 -1
  34. package/dist/hooks/useAiStatus.js +43 -5
  35. package/dist/hooks/useLoadingTimer.d.ts.map +1 -1
  36. package/dist/hooks/useLoadingTimer.js +11 -7
  37. package/dist/styles.css +3 -3
  38. package/dist/utils/errorHandler.d.ts +2 -2
  39. package/dist/utils/errorHandler.d.ts.map +1 -1
  40. package/package.json +2 -2
  41. package/src/components/AiChipLabel.tsx +1 -4
  42. package/src/components/AiContextButton.tsx +15 -6
  43. package/src/components/AiImageButton.tsx +10 -4
  44. package/src/components/AiInput.tsx +9 -4
  45. package/src/components/AiModelSelect.tsx +3 -1
  46. package/src/components/AiPromptPanel.tsx +83 -49
  47. package/src/components/AiSelect.tsx +2 -2
  48. package/src/components/AiStatusButton.tsx +48 -27
  49. package/src/components/AiTextarea.tsx +25 -9
  50. package/src/components/ErrorToast.tsx +3 -3
  51. package/src/components/LBApiKeySelector.tsx +5 -5
  52. package/src/components/LBConnectButton.tsx +1 -3
  53. package/src/components/LBKeyPicker.tsx +10 -10
  54. package/src/components/LBSigninModal.tsx +12 -9
  55. package/src/components/UsageToast.tsx +4 -4
  56. package/src/context/I18nContext.tsx +6 -2
  57. package/src/context/LBAuthProvider.tsx +12 -5
  58. package/src/examples/AiImageGenerator.tsx +1 -1
  59. package/src/hooks/useAiStatus.ts +47 -5
  60. package/src/hooks/useLoadingTimer.ts +14 -8
  61. package/src/styles.css +3 -3
  62. package/src/utils/errorHandler.ts +3 -3
  63. package/src/utils/modelManagement.ts +3 -3
@@ -207,14 +207,17 @@ export function AiStatusButton({
207
207
 
208
208
  const canPortal = typeof document !== "undefined";
209
209
 
210
- const effectiveStatus =
211
- lbStatus === "ready"
212
- ? {
213
- ...(lbApiStatus || {}),
214
- ...(lbBasicStatus || {}),
215
- storage: lbStorageStatus?.storage || lbApiStatus?.storage,
216
- }
217
- : status || null;
210
+ const effectiveStatus = useMemo(
211
+ () =>
212
+ lbStatus === "ready"
213
+ ? {
214
+ ...(lbApiStatus || {}),
215
+ ...(lbBasicStatus || {}),
216
+ storage: lbStorageStatus?.storage || lbApiStatus?.storage,
217
+ }
218
+ : status || null,
219
+ [lbApiStatus, lbBasicStatus, lbStatus, lbStorageStatus, status]
220
+ );
218
221
 
219
222
  const hasApiKeySelected = Boolean(
220
223
  effectiveStatus?.apiKey?.id ||
@@ -229,8 +232,7 @@ export function AiStatusButton({
229
232
  : undefined;
230
233
  const requiresApiKeySelection =
231
234
  lbStatus === "ready" && !hasApiKeySelected && apiKeys.length > 0;
232
- const isApiKeyAuthMode =
233
- authTypeValue === "api_key";
235
+ const isApiKeyAuthMode = authTypeValue === "api_key";
234
236
 
235
237
  const [tooltipStyle, setTooltipStyle] = useState<Record<string, string>>({});
236
238
 
@@ -328,11 +330,11 @@ export function AiStatusButton({
328
330
 
329
331
  const hasApiKeyMeta = Boolean(
330
332
  effectiveStatus?.apiKey?.name ||
331
- effectiveStatus?.api_key?.name ||
332
- effectiveStatus?.apiKey?.env ||
333
- effectiveStatus?.api_key?.env ||
334
- effectiveStatus?.apiKey?.rate_limit_rpm ||
335
- effectiveStatus?.api_key?.rate_limit_rpm
333
+ effectiveStatus?.api_key?.name ||
334
+ effectiveStatus?.apiKey?.env ||
335
+ effectiveStatus?.api_key?.env ||
336
+ effectiveStatus?.apiKey?.rate_limit_rpm ||
337
+ effectiveStatus?.api_key?.rate_limit_rpm
336
338
  );
337
339
  const hasBalanceMeta = Boolean(effectiveStatus?.balance);
338
340
  const hasStorageMeta = Boolean(effectiveStatus?.storage);
@@ -422,10 +424,14 @@ export function AiStatusButton({
422
424
  {lbStatus === "ready" && user ? (
423
425
  <>
424
426
  <div className="ai-popover-body">
425
- <div className="ai-popover-header">{t("status.title", "API Status")}</div>
427
+ <div className="ai-popover-header">
428
+ {t("status.title", "API Status")}
429
+ </div>
426
430
  <div className="ai-popover-section ai-popover-section--first">
427
431
  <div className="ai-popover-row">
428
- <span className="ai-popover-label">{t("status.user", "User")}</span>
432
+ <span className="ai-popover-label">
433
+ {t("status.user", "User")}
434
+ </span>
429
435
  <span
430
436
  className="ai-popover-value ai-truncate"
431
437
  style={{ maxWidth: 200 }}
@@ -437,7 +443,9 @@ export function AiStatusButton({
437
443
 
438
444
  <div className="ai-popover-section">
439
445
  <div className="ai-popover-row">
440
- <span className="ai-popover-label">{t("status.apiKey", "API Key")}</span>
446
+ <span className="ai-popover-label">
447
+ {t("status.apiKey", "API Key")}
448
+ </span>
441
449
  <div className="ai-row">
442
450
  {lbIsLoadingStatus || isImplicitStatusLoading ? (
443
451
  <div className="ai-kv-skeleton ai-kv-skeleton--110" />
@@ -468,7 +476,9 @@ export function AiStatusButton({
468
476
  </div>
469
477
 
470
478
  <div className="ai-popover-row">
471
- <span className="ai-popover-label">{t("status.env", "Env")}</span>
479
+ <span className="ai-popover-label">
480
+ {t("status.env", "Env")}
481
+ </span>
472
482
  {(lbIsLoadingStatus || isImplicitStatusLoading) &&
473
483
  !effectiveStatus?.apiKey?.env ? (
474
484
  <div className="ai-kv-skeleton ai-kv-skeleton--48" />
@@ -482,7 +492,9 @@ export function AiStatusButton({
482
492
  </div>
483
493
 
484
494
  <div className="ai-popover-row">
485
- <span className="ai-popover-label">{t("status.rateLimit", "Rate Limit")}</span>
495
+ <span className="ai-popover-label">
496
+ {t("status.rateLimit", "Rate Limit")}
497
+ </span>
486
498
  {(lbIsLoadingStatus || isImplicitStatusLoading) &&
487
499
  !effectiveStatus?.apiKey?.rate_limit_rpm ? (
488
500
  <div className="ai-kv-skeleton ai-kv-skeleton--92" />
@@ -490,14 +502,16 @@ export function AiStatusButton({
490
502
  <span className="ai-popover-value">
491
503
  {effectiveStatus?.apiKey?.rate_limit_rpm ||
492
504
  effectiveStatus?.api_key?.rate_limit_rpm ||
493
- 0}{" "}
505
+ 0}{" "}
494
506
  req/min
495
507
  </span>
496
508
  )}
497
509
  </div>
498
510
 
499
511
  <div className="ai-popover-row">
500
- <span className="ai-popover-label">{t("status.auth", "Auth")}</span>
512
+ <span className="ai-popover-label">
513
+ {t("status.auth", "Auth")}
514
+ </span>
501
515
  <span className="ai-popover-value">
502
516
  {lbIsLoadingStatus || isImplicitStatusLoading
503
517
  ? "..."
@@ -513,7 +527,9 @@ export function AiStatusButton({
513
527
  {t("status.wallet", "Wallet")}
514
528
  </div>
515
529
  <div className="ai-popover-row">
516
- <span className="ai-popover-label">{t("status.total", "Total")}</span>
530
+ <span className="ai-popover-label">
531
+ {t("status.total", "Total")}
532
+ </span>
517
533
  {(lbIsLoadingStatus || isImplicitStatusLoading) &&
518
534
  !effectiveStatus?.balance ? (
519
535
  <div className="ai-row">
@@ -536,7 +552,9 @@ export function AiStatusButton({
536
552
  {t("status.storage", "Storage")}
537
553
  </div>
538
554
  <div className="ai-popover-row">
539
- <span className="ai-popover-label">{t("status.total", "Total")}</span>
555
+ <span className="ai-popover-label">
556
+ {t("status.total", "Total")}
557
+ </span>
540
558
  {lbIsLoadingStorage || isImplicitStorageLoading ? (
541
559
  <div className="ai-row">
542
560
  <div className="ai-kv-skeleton ai-kv-skeleton--120" />
@@ -564,8 +582,8 @@ export function AiStatusButton({
564
582
  type="button"
565
583
  className="ai-status-action-btn"
566
584
  onClick={() => window.open(item.href, "_blank")}
567
- title={label}
568
- data-ai-tip={label}
585
+ title={label}
586
+ data-ai-tip={label}
569
587
  >
570
588
  <Icon size={17} />
571
589
  </button>
@@ -602,7 +620,10 @@ export function AiStatusButton({
602
620
  {t("status.lastbrainAuth", "LastBrain Authentication")}
603
621
  </div>
604
622
  <p className="ai-signin-subtitle" style={{ marginTop: 0 }}>
605
- {t("status.connectToAccess", "Sign in to access AI features.")}
623
+ {t(
624
+ "status.connectToAccess",
625
+ "Sign in to access AI features."
626
+ )}
606
627
  </p>
607
628
  <button
608
629
  type="button"
@@ -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,15 @@ export function AiTextarea({
61
63
 
62
64
  // Rendre l'authentification optionnelle
63
65
  let lbStatus: string | undefined;
66
+ let hasLBProvider = false;
64
67
  try {
65
68
  const lbContext = useLB();
66
69
  lbStatus = lbContext.status;
70
+ hasLBProvider = true;
67
71
  } catch {
68
72
  // LBProvider n'est pas disponible, ignorer
69
73
  lbStatus = undefined;
74
+ hasLBProvider = false;
70
75
  }
71
76
 
72
77
  let ctxBaseUrl: string | undefined;
@@ -82,8 +87,11 @@ export function AiTextarea({
82
87
 
83
88
  const baseUrl = propBaseUrl ?? ctxBaseUrl;
84
89
  const apiKeyId = propApiKeyId ?? ctxApiKeyId;
90
+ const supportsSessionAuth =
91
+ typeof baseUrl === "string" &&
92
+ (baseUrl.includes("/api/ai") || baseUrl.includes("/api/lastbrain"));
85
93
 
86
- const { models } = useAiModels({
94
+ const { models: _models } = useAiModels({
87
95
  baseUrl,
88
96
  apiKeyId,
89
97
  modelType: "text-or-language",
@@ -92,7 +100,10 @@ export function AiTextarea({
92
100
  const { formatted: loadingElapsed } = useLoadingTimer(loading);
93
101
 
94
102
  const hasConfiguration = Boolean(model && prompt);
95
- const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
103
+ const isAuthReady =
104
+ supportsSessionAuth ||
105
+ lbStatus === "ready" ||
106
+ Boolean(process.env.LB_API_KEY);
96
107
  const shouldShowSparkles = isAuthReady && !disabled;
97
108
 
98
109
  const handleOpenPanel = () => {
@@ -101,16 +112,18 @@ export function AiTextarea({
101
112
  return;
102
113
  }
103
114
  setIsOpen(true);
115
+ onPanelOpenChange?.(true);
104
116
  };
105
117
 
106
118
  const handleClosePanel = () => {
107
119
  setIsOpen(false);
120
+ onPanelOpenChange?.(false);
108
121
  };
109
122
 
110
123
  const handleSubmit = async (
111
124
  selectedModel: string,
112
125
  selectedPrompt: string,
113
- promptId?: string
126
+ _promptId?: string
114
127
  ) => {
115
128
  try {
116
129
  const resolvedContext = textareaValue || context || undefined;
@@ -141,13 +154,14 @@ export function AiTextarea({
141
154
  });
142
155
  showUsageToast(result);
143
156
  }
144
- } catch (error) {
157
+ } catch {
145
158
  onToast?.({
146
159
  type: "error",
147
160
  message: t("ai.generationError", "Failed to generate text"),
148
161
  });
149
162
  } finally {
150
163
  setIsOpen(false);
164
+ onPanelOpenChange?.(false);
151
165
  }
152
166
  };
153
167
 
@@ -280,10 +294,12 @@ export function AiTextarea({
280
294
  onComplete={clearToast}
281
295
  />
282
296
  )}
283
- <LBSigninModal
284
- isOpen={showAuthModal}
285
- onClose={() => setShowAuthModal(false)}
286
- />
297
+ {hasLBProvider ? (
298
+ <LBSigninModal
299
+ isOpen={showAuthModal}
300
+ onClose={() => setShowAuthModal(false)}
301
+ />
302
+ ) : null}
287
303
  </div>
288
304
  );
289
305
  }
@@ -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) {
@@ -122,11 +122,11 @@ export function LBApiKeySelector({
122
122
  {t("common.active", "Active")}
123
123
  </span>
124
124
  ) : (
125
- <span className="ai-pill ai-pill--cost">
126
- <XCircle size={12} />
127
- {t("common.inactive", "Inactive")}
128
- </span>
129
- )}
125
+ <span className="ai-pill ai-pill--cost">
126
+ <XCircle size={12} />
127
+ {t("common.inactive", "Inactive")}
128
+ </span>
129
+ )}
130
130
  </label>
131
131
  );
132
132
  })}
@@ -43,9 +43,7 @@ export function LBConnectButton({
43
43
 
44
44
  const connectLabel = label || t("auth.signIn", "Sign in");
45
45
  const buttonLabel =
46
- status === "ready" && user
47
- ? t("auth.signOut", "Sign out")
48
- : connectLabel;
46
+ status === "ready" && user ? t("auth.signOut", "Sign out") : connectLabel;
49
47
 
50
48
  return (
51
49
  <>
@@ -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
 
@@ -64,9 +65,7 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
64
65
  }
65
66
 
66
67
  if (!fetchApiKeys || !result.accessToken) {
67
- setError(
68
- t("auth.modal.tokenMissing", "Access token unavailable")
69
- );
68
+ setError(t("auth.modal.tokenMissing", "Access token unavailable"));
70
69
  return;
71
70
  }
72
71
 
@@ -76,9 +75,7 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
76
75
  setShowKeySelector(true);
77
76
  } catch (keyError) {
78
77
  console.error("Failed to fetch API keys:", keyError);
79
- setError(
80
- t("auth.modal.apiKeysFetchError", "Failed to fetch API keys")
81
- );
78
+ setError(t("auth.modal.apiKeysFetchError", "Failed to fetch API keys"));
82
79
  }
83
80
  } catch (err) {
84
81
  setError(
@@ -186,7 +183,10 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
186
183
  required
187
184
  autoFocus
188
185
  autoComplete="email"
189
- placeholder={t("auth.modal.emailPlaceholder", "your@email.com")}
186
+ placeholder={t(
187
+ "auth.modal.emailPlaceholder",
188
+ "your@email.com"
189
+ )}
190
190
  />
191
191
  </div>
192
192
  </div>
@@ -210,7 +210,10 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
210
210
  onChange={(e) => setPassword(e.target.value)}
211
211
  required
212
212
  autoComplete="current-password"
213
- placeholder={t("auth.modal.passwordPlaceholder", "••••••••")}
213
+ placeholder={t(
214
+ "auth.modal.passwordPlaceholder",
215
+ "••••••••"
216
+ )}
214
217
  />
215
218
  </div>
216
219
  </div>
@@ -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);
@@ -46,14 +46,18 @@ export interface I18nProviderProps {
46
46
  lang?: LBSupportedLang;
47
47
  }
48
48
 
49
- export function I18nProvider({ children, lang = defaultLang }: I18nProviderProps) {
49
+ export function I18nProvider({
50
+ children,
51
+ lang = defaultLang,
52
+ }: I18nProviderProps) {
50
53
  const safeLang: LBSupportedLang = dictionaries[lang] ? lang : defaultLang;
51
54
 
52
55
  const value = useMemo<I18nContextValue>(() => {
53
56
  const dict = dictionaries[safeLang] || dictionaries[defaultLang];
54
57
 
55
58
  const t = (key: string, fallback?: string, params?: TranslateParams) => {
56
- const template = dict[key] || dictionaries[defaultLang][key] || fallback || key;
59
+ const template =
60
+ dict[key] || dictionaries[defaultLang][key] || fallback || key;
57
61
  if (!params) return template;
58
62
  return template.replace(/\{(\w+)\}/g, (_, p: string) =>
59
63
  params[p] !== undefined ? String(params[p]) : `{${p}}`
@@ -416,7 +416,7 @@ export function LBProvider({
416
416
  try {
417
417
  let data: BasicStatus;
418
418
  try {
419
- data = await lbClient.getStatus();
419
+ data = (await lbClient.getStatus()) as BasicStatus;
420
420
  } catch {
421
421
  // Backward compatibility: older backends may not expose /auth/status
422
422
  const userData = await lbClient.getUser();
@@ -483,15 +483,17 @@ export function LBProvider({
483
483
 
484
484
  setIsLoadingStorage(true);
485
485
  try {
486
- const data = await lbClient.getStorageStatus();
487
- const storageData = data?.storage ? { storage: data.storage } : data;
486
+ const data = (await lbClient.getStorageStatus()) as StorageStatus;
487
+ const storageData: StorageStatus = data?.storage
488
+ ? { storage: data.storage }
489
+ : data;
488
490
  setStorageStatus(storageData);
489
491
  setStorageLastFetch(now);
490
492
 
491
493
  // Combiner avec le basic status
492
494
  const combinedStatus = {
493
495
  ...basicStatus,
494
- storage: storageData?.storage,
496
+ storage: storageData.storage,
495
497
  };
496
498
  setApiStatus(combinedStatus as AiStatus);
497
499
  } catch (error) {
@@ -624,7 +626,12 @@ export function LBProvider({
624
626
  setStorageStatus(null);
625
627
  setApiKeys([]);
626
628
  }
627
- }, [state.status]); // Supprimer les fonctions des dépendances pour éviter la boucle
629
+ }, [
630
+ fetchApiKeysWithSession,
631
+ refreshBasicStatus,
632
+ refreshStorageStatus,
633
+ state.status,
634
+ ]);
628
635
 
629
636
  const value: LBContextValue = {
630
637
  ...state,
@@ -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,40 @@ 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 is available, it already handles fast status + storage split.
47
+ if (lbContext && lbContext.status === "ready") {
48
+ try {
49
+ await lbContext.refreshBasicStatus();
50
+ } catch {
51
+ // Ignore: provider already tracks errors/status
52
+ }
53
+ return;
54
+ }
55
+
26
56
  console.log("[useAiStatus] Starting status fetch");
27
57
 
28
58
  setLoading(true);
@@ -39,15 +69,27 @@ export function useAiStatus(options?: UseAiStatusOptions): UseAiStatusResult {
39
69
  } finally {
40
70
  setLoading(false);
41
71
  }
42
- }, [client]);
72
+ }, [client, lbContext]);
43
73
 
44
74
  useEffect(() => {
75
+ if (statusFromLB) {
76
+ setStatus(statusFromLB);
77
+ setLoading(false);
78
+ setError(null);
79
+ return;
80
+ }
45
81
  fetchStatus();
46
- }, [fetchStatus]);
82
+ }, [fetchStatus, statusFromLB]);
83
+
84
+ const isLoadingFromLB =
85
+ !!lbContext &&
86
+ lbContext.status === "ready" &&
87
+ !statusFromLB &&
88
+ (lbContext.isLoadingStatus || lbContext.isLoadingStorage);
47
89
 
48
90
  return {
49
- status,
50
- loading,
91
+ status: statusFromLB || status,
92
+ loading: isLoadingFromLB || loading,
51
93
  error,
52
94
  refetch: fetchStatus,
53
95
  };
@@ -14,19 +14,25 @@ export function useLoadingTimer(active: boolean) {
14
14
 
15
15
  useEffect(() => {
16
16
  if (!active) {
17
- setSeconds(0);
18
- return;
17
+ const timeoutId = window.setTimeout(() => setSeconds(0), 0);
18
+ return () => window.clearTimeout(timeoutId);
19
19
  }
20
- setSeconds(0);
21
- const id = window.setInterval(() => {
20
+
21
+ const resetId = window.setTimeout(() => setSeconds(0), 0);
22
+ const intervalId = window.setInterval(() => {
22
23
  setSeconds((prev) => prev + 1);
23
24
  }, 1000);
24
- return () => window.clearInterval(id);
25
+
26
+ return () => {
27
+ window.clearTimeout(resetId);
28
+ window.clearInterval(intervalId);
29
+ };
25
30
  }, [active]);
26
31
 
32
+ const displaySeconds = active ? seconds : 0;
33
+
27
34
  return {
28
- seconds,
29
- formatted: formatElapsed(seconds),
35
+ seconds: displaySeconds,
36
+ formatted: formatElapsed(displaySeconds),
30
37
  };
31
38
  }
32
-