@lastbrain/ai-ui-react 1.0.73 → 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 +23 -20
  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 +141 -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 +51 -40
  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 +141 -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;
@@ -420,11 +422,14 @@ export function AiStatusButton({
420
422
  {lbStatus === "ready" && user ? (
421
423
  <>
422
424
  <div className="ai-popover-body">
423
- <div className="ai-popover-header">API Status</div>
425
+ <div className="ai-popover-header">{t("status.title", "API Status")}</div>
424
426
  <div className="ai-popover-section ai-popover-section--first">
425
427
  <div className="ai-popover-row">
426
- <span className="ai-popover-label">User</span>
427
- <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
+ >
428
433
  {user.email}
429
434
  </span>
430
435
  </div>
@@ -432,15 +437,15 @@ export function AiStatusButton({
432
437
 
433
438
  <div className="ai-popover-section">
434
439
  <div className="ai-popover-row">
435
- <span className="ai-popover-label">API Key</span>
440
+ <span className="ai-popover-label">{t("status.apiKey", "API Key")}</span>
436
441
  <div className="ai-row">
437
442
  {lbIsLoadingStatus || isImplicitStatusLoading ? (
438
- <div className="ai-kv-skeleton w-[110px]" />
443
+ <div className="ai-kv-skeleton ai-kv-skeleton--110" />
439
444
  ) : (
440
445
  <span className="ai-popover-value">
441
446
  {effectiveStatus?.apiKey?.name ||
442
447
  effectiveStatus?.api_key?.name ||
443
- "Unknown"}
448
+ t("common.unknown", "Unknown")}
444
449
  </span>
445
450
  )}
446
451
  {switchApiKey &&
@@ -454,7 +459,7 @@ export function AiStatusButton({
454
459
  setShowTooltip(false);
455
460
  setShowApiKeySelector(true);
456
461
  }}
457
- title="Changer de clé API"
462
+ title={t("status.changeApiKey", "Switch API key")}
458
463
  >
459
464
  <ArrowRightLeft size={12} />
460
465
  </button>
@@ -463,10 +468,10 @@ export function AiStatusButton({
463
468
  </div>
464
469
 
465
470
  <div className="ai-popover-row">
466
- <span className="ai-popover-label">Env</span>
471
+ <span className="ai-popover-label">{t("status.env", "Env")}</span>
467
472
  {(lbIsLoadingStatus || isImplicitStatusLoading) &&
468
473
  !effectiveStatus?.apiKey?.env ? (
469
- <div className="ai-kv-skeleton w-12" />
474
+ <div className="ai-kv-skeleton ai-kv-skeleton--48" />
470
475
  ) : (
471
476
  <span className="ai-popover-value">
472
477
  {effectiveStatus?.apiKey?.env ||
@@ -477,41 +482,43 @@ export function AiStatusButton({
477
482
  </div>
478
483
 
479
484
  <div className="ai-popover-row">
480
- <span className="ai-popover-label">Rate Limit</span>
485
+ <span className="ai-popover-label">{t("status.rateLimit", "Rate Limit")}</span>
481
486
  {(lbIsLoadingStatus || isImplicitStatusLoading) &&
482
487
  !effectiveStatus?.apiKey?.rate_limit_rpm ? (
483
- <div className="ai-kv-skeleton w-[92px]" />
488
+ <div className="ai-kv-skeleton ai-kv-skeleton--92" />
484
489
  ) : (
485
490
  <span className="ai-popover-value">
486
491
  {effectiveStatus?.apiKey?.rate_limit_rpm ||
487
492
  effectiveStatus?.api_key?.rate_limit_rpm ||
488
- 0}{" "}
493
+ 0}{" "}
489
494
  req/min
490
495
  </span>
491
496
  )}
492
497
  </div>
493
498
 
494
499
  <div className="ai-popover-row">
495
- <span className="ai-popover-label">Auth</span>
500
+ <span className="ai-popover-label">{t("status.auth", "Auth")}</span>
496
501
  <span className="ai-popover-value">
497
502
  {lbIsLoadingStatus || isImplicitStatusLoading
498
503
  ? "..."
499
504
  : authTypeValue ||
500
505
  lbStatus ||
501
- "unknown"}
506
+ t("status.unknown", "unknown")}
502
507
  </span>
503
508
  </div>
504
509
  </div>
505
510
 
506
511
  <div className="ai-popover-section">
507
- <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>
508
515
  <div className="ai-popover-row">
509
- <span className="ai-popover-label">Total</span>
516
+ <span className="ai-popover-label">{t("status.total", "Total")}</span>
510
517
  {(lbIsLoadingStatus || isImplicitStatusLoading) &&
511
518
  !effectiveStatus?.balance ? (
512
519
  <div className="ai-row">
513
- <div className="ai-kv-skeleton w-[120px]" />
514
- <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" />
515
522
  </div>
516
523
  ) : (
517
524
  <div className="ai-row">
@@ -525,15 +532,15 @@ export function AiStatusButton({
525
532
  </div>
526
533
 
527
534
  <div className="ai-popover-section">
528
- <div className="ai-popover-header text-xs mb-2">
529
- Storage
535
+ <div className="ai-popover-header ai-popover-header--sub">
536
+ {t("status.storage", "Storage")}
530
537
  </div>
531
538
  <div className="ai-popover-row">
532
- <span className="ai-popover-label">Total</span>
539
+ <span className="ai-popover-label">{t("status.total", "Total")}</span>
533
540
  {lbIsLoadingStorage || isImplicitStorageLoading ? (
534
541
  <div className="ai-row">
535
- <div className="ai-kv-skeleton w-[120px]" />
536
- <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" />
537
544
  </div>
538
545
  ) : (
539
546
  <div className="ai-row">
@@ -550,13 +557,15 @@ export function AiStatusButton({
550
557
  <div className="ai-status-actions">
551
558
  {QUICK_LINKS.map((item) => {
552
559
  const Icon = item.icon;
560
+ const label = t(item.titleKey, item.titleKey);
553
561
  return (
554
562
  <button
555
563
  key={item.href}
556
564
  type="button"
557
565
  className="ai-status-action-btn"
558
566
  onClick={() => window.open(item.href, "_blank")}
559
- title={item.title}
567
+ title={label}
568
+ data-ai-tip={label}
560
569
  >
561
570
  <Icon size={17} />
562
571
  </button>
@@ -578,7 +587,8 @@ export function AiStatusButton({
578
587
  console.error("Logout failed:", error);
579
588
  }
580
589
  }}
581
- title="Logout"
590
+ title={t("status.logout", "Logout")}
591
+ data-ai-tip={t("status.logout", "Logout")}
582
592
  >
583
593
  <LogOut size={17} />
584
594
  </button>
@@ -589,20 +599,21 @@ export function AiStatusButton({
589
599
  ) : (
590
600
  <div className="ai-popover-body">
591
601
  <div className="ai-popover-header">
592
- LastBrain Authentication
602
+ {t("status.lastbrainAuth", "LastBrain Authentication")}
593
603
  </div>
594
- <p className="ai-signin-subtitle mt-0">
595
- 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.")}
596
606
  </p>
597
607
  <button
598
608
  type="button"
599
- className="ai-btn ai-btn--auth w-full mt-2"
609
+ className="ai-btn ai-btn--auth"
610
+ style={{ width: "100%", marginTop: 8 }}
600
611
  onClick={() => {
601
612
  setShowSigninModal(true);
602
613
  setShowTooltip(false);
603
614
  }}
604
615
  >
605
- Se connecter
616
+ {t("auth.signIn", "Sign in")}
606
617
  </button>
607
618
  </div>
608
619
  )}
@@ -613,7 +624,7 @@ export function AiStatusButton({
613
624
 
614
625
  return (
615
626
  <>
616
- <div className="relative inline-block">
627
+ <div style={{ position: "relative", display: "inline-block" }}>
617
628
  <button
618
629
  ref={buttonRef}
619
630
  className={triggerClass}
@@ -631,17 +642,17 @@ export function AiStatusButton({
631
642
  disabled={loading || isSelectingApiKey}
632
643
  title={
633
644
  requiresApiKeySelection
634
- ? "Sélectionnez une clé API"
635
- : "Voir le status"
645
+ ? t("status.selectApiKey", "Select an API key")
646
+ : t("status.view", "View status")
636
647
  }
637
- aria-label="AI status"
648
+ aria-label={t("status.aiStatusAria", "AI status")}
638
649
  >
639
650
  {renderTriggerIcon()}
640
651
  </button>
641
652
 
642
653
  {showCornerLoading ? (
643
654
  <span className="ai-status-loading-dot" aria-hidden="true">
644
- <Loader2 size={7} className="ai-spinner text-[var(--ai-primary)]" />
655
+ <Loader2 size={7} className="ai-spinner" />
645
656
  </span>
646
657
  ) : null}
647
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
  <>
@@ -9,6 +9,7 @@ import "../styles/register";
9
9
  import { useEffect, useState } from "react";
10
10
  import { useLB } from "../hooks/useLB";
11
11
  import type { LBApiKey } from "@lastbrain/ai-ui-core";
12
+ import { useI18n } from "../context/I18nContext";
12
13
 
13
14
  interface LBKeyPickerProps {
14
15
  /** Classe CSS personnalisée */
@@ -21,6 +22,7 @@ export function LBKeyPicker({
21
22
  className = "",
22
23
  onKeyChanged,
23
24
  }: LBKeyPickerProps) {
25
+ const { t } = useI18n();
24
26
  const { status, selectedKey, fetchApiKeys, selectApiKey, accessToken } =
25
27
  useLB();
26
28
  const [apiKeys, setApiKeys] = useState<LBApiKey[]>([]);
@@ -42,7 +44,7 @@ export function LBKeyPicker({
42
44
  const keys = await fetchApiKeys(accessToken);
43
45
  setApiKeys(keys);
44
46
  } catch (err) {
45
- setError("Impossible de charger les clés API");
47
+ setError(t("lb.keypicker.loadError", "Unable to load API keys"));
46
48
  } finally {
47
49
  setLoading(false);
48
50
  }
@@ -62,7 +64,9 @@ export function LBKeyPicker({
62
64
  setShowDropdown(false);
63
65
  } catch (err) {
64
66
  setError(
65
- err instanceof Error ? err.message : "Échec du changement de clé"
67
+ err instanceof Error
68
+ ? err.message
69
+ : t("lb.keypicker.switchError", "Failed to switch API key")
66
70
  );
67
71
  } finally {
68
72
  setLoading(false);
@@ -105,7 +109,7 @@ export function LBKeyPicker({
105
109
  <div className="absolute right-0 mt-2 w-64 bg-white dark:bg-gray-800 border rounded-lg shadow-lg z-20">
106
110
  <div className="p-2">
107
111
  <div className="text-sm font-medium text-gray-700 dark:text-gray-300 px-3 py-2">
108
- Changer de clé API
112
+ {t("lb.keypicker.changeApiKey", "Switch API key")}
109
113
  </div>
110
114
  <div className="space-y-1">
111
115
  {apiKeys.map((key) => (
@@ -126,7 +130,9 @@ export function LBKeyPicker({
126
130
  {key.keyPrefix}...
127
131
  </div>
128
132
  {!key.isActive && (
129
- <div className="text-xs text-red-600">Inactive</div>
133
+ <div className="text-xs text-red-600">
134
+ {t("common.inactive", "Inactive")}
135
+ </div>
130
136
  )}
131
137
  </button>
132
138
  ))}