@lastbrain/ai-ui-react 1.0.72 → 1.0.74

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 (83) hide show
  1. package/dist/components/AiChipLabel.d.ts.map +1 -1
  2. package/dist/components/AiChipLabel.js +10 -7
  3. package/dist/components/AiContextButton.d.ts +1 -1
  4. package/dist/components/AiContextButton.d.ts.map +1 -1
  5. package/dist/components/AiContextButton.js +25 -12
  6. package/dist/components/AiImageButton.d.ts.map +1 -1
  7. package/dist/components/AiImageButton.js +32 -16
  8. package/dist/components/AiInput.d.ts.map +1 -1
  9. package/dist/components/AiInput.js +15 -5
  10. package/dist/components/AiModelSelect.d.ts.map +1 -1
  11. package/dist/components/AiModelSelect.js +3 -1
  12. package/dist/components/AiPromptPanel.d.ts.map +1 -1
  13. package/dist/components/AiPromptPanel.js +72 -47
  14. package/dist/components/AiSelect.d.ts.map +1 -1
  15. package/dist/components/AiSelect.js +8 -3
  16. package/dist/components/AiStatusButton.d.ts.map +1 -1
  17. package/dist/components/AiStatusButton.js +55 -28
  18. package/dist/components/AiTextarea.d.ts.map +1 -1
  19. package/dist/components/AiTextarea.js +19 -6
  20. package/dist/components/ErrorToast.d.ts.map +1 -1
  21. package/dist/components/ErrorToast.js +4 -2
  22. package/dist/components/LBApiKeySelector.d.ts.map +1 -1
  23. package/dist/components/LBApiKeySelector.js +13 -5
  24. package/dist/components/LBConnectButton.d.ts.map +1 -1
  25. package/dist/components/LBConnectButton.js +8 -3
  26. package/dist/components/LBKeyPicker.d.ts.map +1 -1
  27. package/dist/components/LBKeyPicker.js +8 -4
  28. package/dist/components/LBSigninModal.d.ts.map +1 -1
  29. package/dist/components/LBSigninModal.js +13 -7
  30. package/dist/components/UsageToast.d.ts.map +1 -1
  31. package/dist/components/UsageToast.js +4 -2
  32. package/dist/context/I18nContext.d.ts +15 -0
  33. package/dist/context/I18nContext.d.ts.map +1 -0
  34. package/dist/context/I18nContext.js +44 -0
  35. package/dist/context/LBAuthProvider.d.ts +4 -1
  36. package/dist/context/LBAuthProvider.d.ts.map +1 -1
  37. package/dist/context/LBAuthProvider.js +3 -2
  38. package/dist/hooks/useAiCallImage.d.ts.map +1 -1
  39. package/dist/hooks/useAiCallImage.js +1 -107
  40. package/dist/hooks/useAiCallText.d.ts.map +1 -1
  41. package/dist/hooks/useAiCallText.js +1 -25
  42. package/dist/hooks/useLoadingTimer.d.ts +5 -0
  43. package/dist/hooks/useLoadingTimer.d.ts.map +1 -0
  44. package/dist/hooks/useLoadingTimer.js +27 -0
  45. package/dist/i18n/de.json +62 -0
  46. package/dist/i18n/en.json +128 -0
  47. package/dist/i18n/es.json +70 -0
  48. package/dist/i18n/fr.json +128 -0
  49. package/dist/i18n/it.json +62 -0
  50. package/dist/i18n/pt.json +62 -0
  51. package/dist/index.d.ts +2 -0
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +2 -0
  54. package/dist/styles.css +142 -1
  55. package/package.json +3 -3
  56. package/src/components/AiChipLabel.tsx +17 -8
  57. package/src/components/AiContextButton.tsx +44 -20
  58. package/src/components/AiImageButton.tsx +52 -25
  59. package/src/components/AiInput.tsx +20 -5
  60. package/src/components/AiModelSelect.tsx +3 -1
  61. package/src/components/AiPromptPanel.tsx +177 -59
  62. package/src/components/AiSelect.tsx +8 -3
  63. package/src/components/AiStatusButton.tsx +100 -57
  64. package/src/components/AiTextarea.tsx +24 -6
  65. package/src/components/ErrorToast.tsx +4 -2
  66. package/src/components/LBApiKeySelector.tsx +33 -13
  67. package/src/components/LBConnectButton.tsx +9 -3
  68. package/src/components/LBKeyPicker.tsx +10 -4
  69. package/src/components/LBSigninModal.tsx +31 -15
  70. package/src/components/UsageToast.tsx +4 -2
  71. package/src/context/I18nContext.tsx +71 -0
  72. package/src/context/LBAuthProvider.tsx +9 -1
  73. package/src/hooks/useAiCallImage.ts +1 -149
  74. package/src/hooks/useAiCallText.ts +1 -30
  75. package/src/hooks/useLoadingTimer.ts +32 -0
  76. package/src/i18n/de.json +62 -0
  77. package/src/i18n/en.json +128 -0
  78. package/src/i18n/es.json +70 -0
  79. package/src/i18n/fr.json +128 -0
  80. package/src/i18n/it.json +62 -0
  81. package/src/i18n/pt.json +62 -0
  82. package/src/index.ts +2 -0
  83. package/src/styles.css +142 -1
@@ -25,6 +25,7 @@ import { AiContext } from "../context/AiProvider";
25
25
  import { LBApiKeySelector } from "./LBApiKeySelector";
26
26
  import { LBSigninModal } from "./LBSigninModal";
27
27
  import type { AiRadius, AiSize } from "../types";
28
+ import { useI18n } from "../context/I18nContext";
28
29
 
29
30
  export interface AiStatusButtonProps {
30
31
  status: AiStatus | null;
@@ -53,27 +54,27 @@ type StorageUsage = {
53
54
  const QUICK_LINKS = [
54
55
  {
55
56
  href: "https://prompt.lastbrain.io/auth/ai/tokens",
56
- title: "Dashboard",
57
+ titleKey: "status.dashboard",
57
58
  icon: BarChart3,
58
59
  },
59
60
  {
60
61
  href: "https://prompt.lastbrain.io/auth/ai/history",
61
- title: "Historique",
62
+ titleKey: "status.history",
62
63
  icon: History,
63
64
  },
64
65
  {
65
66
  href: "https://prompt.lastbrain.io/auth/ai/settings",
66
- title: "Settings",
67
+ titleKey: "status.settings",
67
68
  icon: Settings,
68
69
  },
69
70
  {
70
71
  href: "https://prompt.lastbrain.io/auth/ai/prompts",
71
- title: "Prompts",
72
+ titleKey: "status.prompts",
72
73
  icon: FileText,
73
74
  },
74
75
  {
75
76
  href: "https://prompt.lastbrain.io/auth/folder",
76
- title: "Dossiers",
77
+ titleKey: "status.folders",
77
78
  icon: Folder,
78
79
  },
79
80
  ];
@@ -154,6 +155,7 @@ export function AiStatusButton({
154
155
  size = "md",
155
156
  radius = "full",
156
157
  }: AiStatusButtonProps) {
158
+ const { t } = useI18n();
157
159
  let lbStatus: string | undefined;
158
160
  let user: LBUser | null = null;
159
161
  let logout: (() => Promise<void>) | undefined;
@@ -219,12 +221,16 @@ export function AiStatusButton({
219
221
  effectiveStatus?.api_key?.id ||
220
222
  lbSelectedKey?.id
221
223
  );
224
+ const authTypeValue =
225
+ effectiveStatus &&
226
+ "authType" in effectiveStatus &&
227
+ typeof effectiveStatus.authType === "string"
228
+ ? effectiveStatus.authType
229
+ : undefined;
222
230
  const requiresApiKeySelection =
223
231
  lbStatus === "ready" && !hasApiKeySelected && apiKeys.length > 0;
224
232
  const isApiKeyAuthMode =
225
- effectiveStatus &&
226
- "authType" in effectiveStatus &&
227
- effectiveStatus.authType === "api_key";
233
+ authTypeValue === "api_key";
228
234
 
229
235
  const [tooltipStyle, setTooltipStyle] = useState<Record<string, string>>({});
230
236
 
@@ -320,15 +326,41 @@ export function AiStatusButton({
320
326
  storage.percentage ??
321
327
  (storageTotal > 0 ? Math.round((storageUsed / storageTotal) * 100) : 0);
322
328
 
329
+ const hasApiKeyMeta = Boolean(
330
+ 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
336
+ );
337
+ const hasBalanceMeta = Boolean(effectiveStatus?.balance);
338
+ const hasStorageMeta = Boolean(effectiveStatus?.storage);
339
+
340
+ const isImplicitStatusLoading =
341
+ lbStatus === "ready" &&
342
+ !!user &&
343
+ !requiresApiKeySelection &&
344
+ (!authTypeValue || !hasApiKeyMeta || !hasBalanceMeta);
345
+
346
+ const isImplicitStorageLoading =
347
+ lbStatus === "ready" &&
348
+ !!user &&
349
+ !requiresApiKeySelection &&
350
+ !hasStorageMeta;
351
+
323
352
  const showFastSkeleton =
324
353
  lbStatus === "ready" &&
325
- lbIsLoadingStatus &&
326
- !lbBasicStatus &&
327
- !effectiveStatus;
354
+ (lbIsLoadingStatus ||
355
+ isImplicitStatusLoading ||
356
+ (!lbBasicStatus && !effectiveStatus));
328
357
 
329
358
  const showCornerLoading =
330
359
  lbStatus === "ready" &&
331
- (showFastSkeleton || lbIsLoadingStatus || lbIsLoadingStorage);
360
+ (showFastSkeleton ||
361
+ lbIsLoadingStatus ||
362
+ lbIsLoadingStorage ||
363
+ isImplicitStorageLoading);
332
364
 
333
365
  const triggerTone = useMemo(() => {
334
366
  if (requiresApiKeySelection) return "warning";
@@ -390,11 +422,14 @@ export function AiStatusButton({
390
422
  {lbStatus === "ready" && user ? (
391
423
  <>
392
424
  <div className="ai-popover-body">
393
- <div className="ai-popover-header">API Status</div>
425
+ <div className="ai-popover-header">{t("status.title", "API Status")}</div>
394
426
  <div className="ai-popover-section ai-popover-section--first">
395
427
  <div className="ai-popover-row">
396
- <span className="ai-popover-label">User</span>
397
- <span className="ai-popover-value ai-truncate max-w-[200px]">
428
+ <span className="ai-popover-label">{t("status.user", "User")}</span>
429
+ <span
430
+ className="ai-popover-value ai-truncate"
431
+ style={{ maxWidth: 200 }}
432
+ >
398
433
  {user.email}
399
434
  </span>
400
435
  </div>
@@ -402,18 +437,20 @@ export function AiStatusButton({
402
437
 
403
438
  <div className="ai-popover-section">
404
439
  <div className="ai-popover-row">
405
- <span className="ai-popover-label">API Key</span>
440
+ <span className="ai-popover-label">{t("status.apiKey", "API Key")}</span>
406
441
  <div className="ai-row">
407
- {lbIsLoadingStatus ? (
408
- <div className="ai-kv-skeleton w-[110px]" />
442
+ {lbIsLoadingStatus || isImplicitStatusLoading ? (
443
+ <div className="ai-kv-skeleton ai-kv-skeleton--110" />
409
444
  ) : (
410
445
  <span className="ai-popover-value">
411
446
  {effectiveStatus?.apiKey?.name ||
412
447
  effectiveStatus?.api_key?.name ||
413
- "Unknown"}
448
+ t("common.unknown", "Unknown")}
414
449
  </span>
415
450
  )}
416
- {switchApiKey && !isApiKeyAuthMode ? (
451
+ {switchApiKey &&
452
+ !isApiKeyAuthMode &&
453
+ !isImplicitStatusLoading ? (
417
454
  <button
418
455
  type="button"
419
456
  className="ai-icon-btn"
@@ -422,7 +459,7 @@ export function AiStatusButton({
422
459
  setShowTooltip(false);
423
460
  setShowApiKeySelector(true);
424
461
  }}
425
- title="Changer de clé API"
462
+ title={t("status.changeApiKey", "Switch API key")}
426
463
  >
427
464
  <ArrowRightLeft size={12} />
428
465
  </button>
@@ -431,9 +468,10 @@ export function AiStatusButton({
431
468
  </div>
432
469
 
433
470
  <div className="ai-popover-row">
434
- <span className="ai-popover-label">Env</span>
435
- {lbIsLoadingStatus && !effectiveStatus?.apiKey?.env ? (
436
- <div className="ai-kv-skeleton w-12" />
471
+ <span className="ai-popover-label">{t("status.env", "Env")}</span>
472
+ {(lbIsLoadingStatus || isImplicitStatusLoading) &&
473
+ !effectiveStatus?.apiKey?.env ? (
474
+ <div className="ai-kv-skeleton ai-kv-skeleton--48" />
437
475
  ) : (
438
476
  <span className="ai-popover-value">
439
477
  {effectiveStatus?.apiKey?.env ||
@@ -444,42 +482,43 @@ export function AiStatusButton({
444
482
  </div>
445
483
 
446
484
  <div className="ai-popover-row">
447
- <span className="ai-popover-label">Rate Limit</span>
448
- {lbIsLoadingStatus &&
485
+ <span className="ai-popover-label">{t("status.rateLimit", "Rate Limit")}</span>
486
+ {(lbIsLoadingStatus || isImplicitStatusLoading) &&
449
487
  !effectiveStatus?.apiKey?.rate_limit_rpm ? (
450
- <div className="ai-kv-skeleton w-[92px]" />
488
+ <div className="ai-kv-skeleton ai-kv-skeleton--92" />
451
489
  ) : (
452
490
  <span className="ai-popover-value">
453
491
  {effectiveStatus?.apiKey?.rate_limit_rpm ||
454
492
  effectiveStatus?.api_key?.rate_limit_rpm ||
455
- 0}{" "}
493
+ 0}{" "}
456
494
  req/min
457
495
  </span>
458
496
  )}
459
497
  </div>
460
498
 
461
499
  <div className="ai-popover-row">
462
- <span className="ai-popover-label">Auth</span>
500
+ <span className="ai-popover-label">{t("status.auth", "Auth")}</span>
463
501
  <span className="ai-popover-value">
464
- {lbIsLoadingStatus
502
+ {lbIsLoadingStatus || isImplicitStatusLoading
465
503
  ? "..."
466
- : (effectiveStatus &&
467
- "authType" in effectiveStatus &&
468
- effectiveStatus.authType) ||
504
+ : authTypeValue ||
469
505
  lbStatus ||
470
- "unknown"}
506
+ t("status.unknown", "unknown")}
471
507
  </span>
472
508
  </div>
473
509
  </div>
474
510
 
475
511
  <div className="ai-popover-section">
476
- <div className="ai-popover-header text-xs mb-2">Wallet</div>
512
+ <div className="ai-popover-header ai-popover-header--sub">
513
+ {t("status.wallet", "Wallet")}
514
+ </div>
477
515
  <div className="ai-popover-row">
478
- <span className="ai-popover-label">Total</span>
479
- {lbIsLoadingStatus && !effectiveStatus?.balance ? (
516
+ <span className="ai-popover-label">{t("status.total", "Total")}</span>
517
+ {(lbIsLoadingStatus || isImplicitStatusLoading) &&
518
+ !effectiveStatus?.balance ? (
480
519
  <div className="ai-row">
481
- <div className="ai-kv-skeleton w-[120px]" />
482
- <div className="ai-kv-skeleton w-7 h-7 rounded-full" />
520
+ <div className="ai-kv-skeleton ai-kv-skeleton--120" />
521
+ <div className="ai-kv-skeleton ai-kv-skeleton--circle" />
483
522
  </div>
484
523
  ) : (
485
524
  <div className="ai-row">
@@ -493,15 +532,15 @@ export function AiStatusButton({
493
532
  </div>
494
533
 
495
534
  <div className="ai-popover-section">
496
- <div className="ai-popover-header text-xs mb-2">
497
- Storage
535
+ <div className="ai-popover-header ai-popover-header--sub">
536
+ {t("status.storage", "Storage")}
498
537
  </div>
499
538
  <div className="ai-popover-row">
500
- <span className="ai-popover-label">Total</span>
501
- {lbIsLoadingStorage ? (
539
+ <span className="ai-popover-label">{t("status.total", "Total")}</span>
540
+ {lbIsLoadingStorage || isImplicitStorageLoading ? (
502
541
  <div className="ai-row">
503
- <div className="ai-kv-skeleton w-[120px]" />
504
- <div className="ai-kv-skeleton w-7 h-7 rounded-full" />
542
+ <div className="ai-kv-skeleton ai-kv-skeleton--120" />
543
+ <div className="ai-kv-skeleton ai-kv-skeleton--circle" />
505
544
  </div>
506
545
  ) : (
507
546
  <div className="ai-row">
@@ -518,13 +557,15 @@ export function AiStatusButton({
518
557
  <div className="ai-status-actions">
519
558
  {QUICK_LINKS.map((item) => {
520
559
  const Icon = item.icon;
560
+ const label = t(item.titleKey, item.titleKey);
521
561
  return (
522
562
  <button
523
563
  key={item.href}
524
564
  type="button"
525
565
  className="ai-status-action-btn"
526
566
  onClick={() => window.open(item.href, "_blank")}
527
- title={item.title}
567
+ title={label}
568
+ data-ai-tip={label}
528
569
  >
529
570
  <Icon size={17} />
530
571
  </button>
@@ -546,7 +587,8 @@ export function AiStatusButton({
546
587
  console.error("Logout failed:", error);
547
588
  }
548
589
  }}
549
- title="Logout"
590
+ title={t("status.logout", "Logout")}
591
+ data-ai-tip={t("status.logout", "Logout")}
550
592
  >
551
593
  <LogOut size={17} />
552
594
  </button>
@@ -557,20 +599,21 @@ export function AiStatusButton({
557
599
  ) : (
558
600
  <div className="ai-popover-body">
559
601
  <div className="ai-popover-header">
560
- LastBrain Authentication
602
+ {t("status.lastbrainAuth", "LastBrain Authentication")}
561
603
  </div>
562
- <p className="ai-signin-subtitle mt-0">
563
- Connectez-vous pour accéder aux fonctionnalités IA.
604
+ <p className="ai-signin-subtitle" style={{ marginTop: 0 }}>
605
+ {t("status.connectToAccess", "Sign in to access AI features.")}
564
606
  </p>
565
607
  <button
566
608
  type="button"
567
- className="ai-btn ai-btn--auth w-full mt-2"
609
+ className="ai-btn ai-btn--auth"
610
+ style={{ width: "100%", marginTop: 8 }}
568
611
  onClick={() => {
569
612
  setShowSigninModal(true);
570
613
  setShowTooltip(false);
571
614
  }}
572
615
  >
573
- Se connecter
616
+ {t("auth.signIn", "Sign in")}
574
617
  </button>
575
618
  </div>
576
619
  )}
@@ -581,7 +624,7 @@ export function AiStatusButton({
581
624
 
582
625
  return (
583
626
  <>
584
- <div className="relative inline-block">
627
+ <div style={{ position: "relative", display: "inline-block" }}>
585
628
  <button
586
629
  ref={buttonRef}
587
630
  className={triggerClass}
@@ -599,17 +642,17 @@ export function AiStatusButton({
599
642
  disabled={loading || isSelectingApiKey}
600
643
  title={
601
644
  requiresApiKeySelection
602
- ? "Sélectionnez une clé API"
603
- : "Voir le status"
645
+ ? t("status.selectApiKey", "Select an API key")
646
+ : t("status.view", "View status")
604
647
  }
605
- aria-label="AI status"
648
+ aria-label={t("status.aiStatusAria", "AI status")}
606
649
  >
607
650
  {renderTriggerIcon()}
608
651
  </button>
609
652
 
610
653
  {showCornerLoading ? (
611
654
  <span className="ai-status-loading-dot" aria-hidden="true">
612
- <Loader2 size={7} className="ai-spinner text-[var(--ai-primary)]" />
655
+ <Loader2 size={7} className="ai-spinner" />
613
656
  </span>
614
657
  ) : null}
615
658
  </div>
@@ -17,6 +17,8 @@ import { handleAIError } from "../utils/errorHandler";
17
17
  import { useLB } from "../context/LBAuthProvider";
18
18
  import { LBSigninModal } from "./LBSigninModal";
19
19
  import { useAiContext } from "../context/AiProvider";
20
+ import { useI18n } from "../context/I18nContext";
21
+ import { useLoadingTimer } from "../hooks/useLoadingTimer";
20
22
 
21
23
  export interface AiTextareaProps
22
24
  extends
@@ -46,6 +48,7 @@ export function AiTextarea({
46
48
  className,
47
49
  ...textareaProps
48
50
  }: AiTextareaProps) {
51
+ const { t } = useI18n();
49
52
  const [isOpen, setIsOpen] = useState(false);
50
53
  const [showAuthModal, setShowAuthModal] = useState(false);
51
54
  const [textareaValue, setTextareaValue] = useState(
@@ -86,6 +89,7 @@ export function AiTextarea({
86
89
  modelType: "text-or-language",
87
90
  });
88
91
  const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
92
+ const { formatted: loadingElapsed } = useLoadingTimer(loading);
89
93
 
90
94
  const hasConfiguration = Boolean(model && prompt);
91
95
  const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
@@ -131,11 +135,17 @@ export function AiTextarea({
131
135
  textareaRef.current.value = result.text;
132
136
  }
133
137
  onValue?.(result.text);
134
- onToast?.({ type: "success", message: "AI generation successful" });
138
+ onToast?.({
139
+ type: "success",
140
+ message: t("ai.generationSuccess", "AI generation successful"),
141
+ });
135
142
  showUsageToast(result);
136
143
  }
137
144
  } catch (error) {
138
- onToast?.({ type: "error", message: "Failed to generate text" });
145
+ onToast?.({
146
+ type: "error",
147
+ message: t("ai.generationError", "Failed to generate text"),
148
+ });
139
149
  } finally {
140
150
  setIsOpen(false);
141
151
  }
@@ -165,7 +175,10 @@ export function AiTextarea({
165
175
  textareaRef.current.value = result.text;
166
176
  }
167
177
  onValue?.(result.text);
168
- onToast?.({ type: "success", message: "AI generation successful" });
178
+ onToast?.({
179
+ type: "success",
180
+ message: t("ai.generationSuccess", "AI generation successful"),
181
+ });
169
182
  showUsageToast(result);
170
183
  }
171
184
  } catch (error) {
@@ -224,10 +237,10 @@ export function AiTextarea({
224
237
  type="button"
225
238
  title={
226
239
  !isAuthReady
227
- ? "Authentication required"
240
+ ? t("auth.required", "Authentication required")
228
241
  : hasConfiguration
229
- ? "Generate with AI"
230
- : "Setup AI"
242
+ ? t("ai.generate", "Generate with AI")
243
+ : t("ai.setup", "Setup AI")
231
244
  }
232
245
  >
233
246
  {loading ? (
@@ -239,6 +252,11 @@ export function AiTextarea({
239
252
  )}
240
253
  </button>
241
254
  </div>
255
+ {loading ? (
256
+ <span className="ai-control-timer">
257
+ {t("ai.loading.elapsed", "{seconds}", { seconds: loadingElapsed })}
258
+ </span>
259
+ ) : null}
242
260
  {isOpen && (
243
261
  <AiPromptPanel
244
262
  isOpen={isOpen}
@@ -3,6 +3,7 @@
3
3
  import "../styles/register";
4
4
  import { useEffect, useRef, useState } from "react";
5
5
  import { X, AlertCircle } from "lucide-react";
6
+ import { useI18n } from "../context/I18nContext";
6
7
 
7
8
  interface ErrorToastData {
8
9
  message: string;
@@ -20,6 +21,7 @@ export function ErrorToast({
20
21
  position = "bottom-right",
21
22
  onComplete,
22
23
  }: ErrorToastProps) {
24
+ const { t } = useI18n();
23
25
  const [isVisible, setIsVisible] = useState(false);
24
26
  const [isClosing, setIsClosing] = useState(false);
25
27
  const fadeTimeoutRef = useRef<number | null>(null);
@@ -112,7 +114,7 @@ export function ErrorToast({
112
114
  <AlertCircle size={16} style={{ marginTop: "2px", flexShrink: 0 }} />
113
115
  <div style={{ flex: 1, minWidth: 0 }}>
114
116
  <div style={{ fontWeight: 600, marginBottom: "2px" }}>
115
- Erreur
117
+ {t("common.errorTitle", "Error")}
116
118
  {error.code && (
117
119
  <span
118
120
  style={{
@@ -153,7 +155,7 @@ export function ErrorToast({
153
155
  onMouseLeave={(e) => {
154
156
  e.currentTarget.style.backgroundColor = "transparent";
155
157
  }}
156
- title="Fermer"
158
+ title={t("common.closeLabel", "Close")}
157
159
  >
158
160
  <X size={14} />
159
161
  </button>
@@ -1,6 +1,9 @@
1
+ "use client";
2
+
1
3
  import React, { useState } from "react";
2
4
  import { CheckCircle2, KeyRound, Loader2, XCircle } from "lucide-react";
3
5
  import type { LBApiKey } from "@lastbrain/ai-ui-core";
6
+ import { useI18n } from "../context/I18nContext";
4
7
 
5
8
  interface LBApiKeySelectorProps {
6
9
  apiKeys: LBApiKey[];
@@ -15,6 +18,7 @@ export function LBApiKeySelector({
15
18
  onCancel,
16
19
  isOpen,
17
20
  }: LBApiKeySelectorProps) {
21
+ const { t } = useI18n();
18
22
  const [selectedKeyId, setSelectedKeyId] = useState<string>(
19
23
  apiKeys.find((k) => k.isActive)?.id || apiKeys[0]?.id || ""
20
24
  );
@@ -26,7 +30,7 @@ export function LBApiKeySelector({
26
30
  const handleSubmit = async (e: React.FormEvent) => {
27
31
  e.preventDefault();
28
32
  if (!selectedKeyId) {
29
- setError("Veuillez sélectionner une clé API");
33
+ setError(t("status.selectApiKey", "Select an API key"));
30
34
  return;
31
35
  }
32
36
 
@@ -37,7 +41,9 @@ export function LBApiKeySelector({
37
41
  await onSelect(selectedKeyId);
38
42
  } catch (err) {
39
43
  setError(
40
- err instanceof Error ? err.message : "Erreur lors de la sélection"
44
+ err instanceof Error
45
+ ? err.message
46
+ : t("auth.modal.selectionError", "Selection error")
41
47
  );
42
48
  setLoading(false);
43
49
  }
@@ -55,9 +61,14 @@ export function LBApiKeySelector({
55
61
  <KeyRound size={20} />
56
62
  </span>
57
63
  </div>
58
- <h2 className="ai-signin-title">Sélectionnez une clé API</h2>
64
+ <h2 className="ai-signin-title">
65
+ {t("status.selectApiKey", "Select an API key")}
66
+ </h2>
59
67
  <p className="ai-signin-subtitle">
60
- Choisissez la clé API à utiliser pour vos requêtes IA.
68
+ {t(
69
+ "status.selectApiKeySubtitle",
70
+ "Choose the API key to use for your AI requests."
71
+ )}
61
72
  </p>
62
73
  </div>
63
74
 
@@ -70,6 +81,9 @@ export function LBApiKeySelector({
70
81
  {apiKeys.map((key) => {
71
82
  const isSelected = key.id === selectedKeyId;
72
83
  const isActive = key.isActive;
84
+ const rawEnv = (key as LBApiKey & { env?: string }).env;
85
+ const keyEnv = rawEnv === "dev" ? "DEV" : "PROD";
86
+ const isDev = rawEnv === "dev";
73
87
  return (
74
88
  <label
75
89
  key={key.id}
@@ -93,20 +107,26 @@ export function LBApiKeySelector({
93
107
  <span>
94
108
  {key.keyPrefix || key.id.substring(0, 12) + "..."}
95
109
  </span>
110
+ <span
111
+ className={`ai-pill ai-pill--cost ${isDev ? "ai-pill--warning" : ""}`}
112
+ style={{ marginLeft: 8 }}
113
+ >
114
+ {keyEnv}
115
+ </span>
96
116
  </div>
97
117
  </div>
98
118
  </div>
99
119
  {isActive ? (
100
120
  <span className="ai-pill ai-pill--cost">
101
121
  <CheckCircle2 size={12} />
102
- Active
122
+ {t("common.active", "Active")}
103
123
  </span>
104
124
  ) : (
105
- <span className="ai-pill ai-pill--cost">
106
- <XCircle size={12} />
107
- Inactive
108
- </span>
109
- )}
125
+ <span className="ai-pill ai-pill--cost">
126
+ <XCircle size={12} />
127
+ {t("common.inactive", "Inactive")}
128
+ </span>
129
+ )}
110
130
  </label>
111
131
  );
112
132
  })}
@@ -126,7 +146,7 @@ export function LBApiKeySelector({
126
146
  disabled={loading}
127
147
  className="ai-btn ai-btn--ghost"
128
148
  >
129
- Annuler
149
+ {t("common.cancel", "Cancel")}
130
150
  </button>
131
151
  <button
132
152
  type="submit"
@@ -136,10 +156,10 @@ export function LBApiKeySelector({
136
156
  {loading ? (
137
157
  <>
138
158
  <Loader2 size={16} className="ai-spinner" />
139
- Connexion...
159
+ {t("auth.modal.connecting", "Signing in...")}
140
160
  </>
141
161
  ) : (
142
- "Continuer"
162
+ t("common.continue", "Continue")
143
163
  )}
144
164
  </button>
145
165
  </div>
@@ -5,6 +5,7 @@ import React from "react";
5
5
  import { Loader2, LogIn, LogOut } from "lucide-react";
6
6
  import { useLB } from "../context/LBAuthProvider";
7
7
  import { LBSigninModal } from "./LBSigninModal";
8
+ import { useI18n } from "../context/I18nContext";
8
9
 
9
10
  interface LBConnectButtonProps {
10
11
  label?: string;
@@ -14,11 +15,12 @@ interface LBConnectButtonProps {
14
15
  }
15
16
 
16
17
  export function LBConnectButton({
17
- label = "Se connecter",
18
+ label,
18
19
  className = "",
19
20
  onConnected,
20
21
  onOpenModal,
21
22
  }: LBConnectButtonProps) {
23
+ const { t } = useI18n();
22
24
  const { status, user, logout } = useLB();
23
25
  const [showModal, setShowModal] = React.useState(false);
24
26
 
@@ -39,7 +41,11 @@ export function LBConnectButton({
39
41
  onOpenModal?.();
40
42
  };
41
43
 
42
- const buttonLabel = status === "ready" && user ? "Déconnexion" : label;
44
+ const connectLabel = label || t("auth.signIn", "Sign in");
45
+ const buttonLabel =
46
+ status === "ready" && user
47
+ ? t("auth.signOut", "Sign out")
48
+ : connectLabel;
43
49
 
44
50
  return (
45
51
  <>
@@ -52,7 +58,7 @@ export function LBConnectButton({
52
58
  {status === "loading" ? (
53
59
  <>
54
60
  <Loader2 size={16} className="animate-spin" />
55
- Chargement...
61
+ {t("common.loading", "Loading...")}
56
62
  </>
57
63
  ) : status === "ready" && user ? (
58
64
  <>