@lastbrain/ai-ui-react 1.0.73 → 1.0.75

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 +26 -12
  6. package/dist/components/AiImageButton.d.ts.map +1 -1
  7. package/dist/components/AiImageButton.js +35 -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 +6 -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 +31 -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 +14 -8
  57. package/src/components/AiContextButton.tsx +53 -20
  58. package/src/components/AiImageButton.tsx +58 -25
  59. package/src/components/AiInput.tsx +20 -5
  60. package/src/components/AiModelSelect.tsx +5 -1
  61. package/src/components/AiPromptPanel.tsx +203 -76
  62. package/src/components/AiSelect.tsx +8 -3
  63. package/src/components/AiStatusButton.tsx +75 -46
  64. package/src/components/AiTextarea.tsx +24 -6
  65. package/src/components/ErrorToast.tsx +4 -2
  66. package/src/components/LBApiKeySelector.tsx +29 -9
  67. package/src/components/LBConnectButton.tsx +7 -3
  68. package/src/components/LBKeyPicker.tsx +10 -4
  69. package/src/components/LBSigninModal.tsx +33 -15
  70. package/src/components/UsageToast.tsx +4 -2
  71. package/src/context/I18nContext.tsx +75 -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 +38 -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;
@@ -227,8 +229,7 @@ export function AiStatusButton({
227
229
  : undefined;
228
230
  const requiresApiKeySelection =
229
231
  lbStatus === "ready" && !hasApiKeySelected && apiKeys.length > 0;
230
- const isApiKeyAuthMode =
231
- authTypeValue === "api_key";
232
+ const isApiKeyAuthMode = authTypeValue === "api_key";
232
233
 
233
234
  const [tooltipStyle, setTooltipStyle] = useState<Record<string, string>>({});
234
235
 
@@ -326,11 +327,11 @@ export function AiStatusButton({
326
327
 
327
328
  const hasApiKeyMeta = Boolean(
328
329
  effectiveStatus?.apiKey?.name ||
329
- effectiveStatus?.api_key?.name ||
330
- effectiveStatus?.apiKey?.env ||
331
- effectiveStatus?.api_key?.env ||
332
- effectiveStatus?.apiKey?.rate_limit_rpm ||
333
- effectiveStatus?.api_key?.rate_limit_rpm
330
+ effectiveStatus?.api_key?.name ||
331
+ effectiveStatus?.apiKey?.env ||
332
+ effectiveStatus?.api_key?.env ||
333
+ effectiveStatus?.apiKey?.rate_limit_rpm ||
334
+ effectiveStatus?.api_key?.rate_limit_rpm
334
335
  );
335
336
  const hasBalanceMeta = Boolean(effectiveStatus?.balance);
336
337
  const hasStorageMeta = Boolean(effectiveStatus?.storage);
@@ -420,11 +421,18 @@ export function AiStatusButton({
420
421
  {lbStatus === "ready" && user ? (
421
422
  <>
422
423
  <div className="ai-popover-body">
423
- <div className="ai-popover-header">API Status</div>
424
+ <div className="ai-popover-header">
425
+ {t("status.title", "API Status")}
426
+ </div>
424
427
  <div className="ai-popover-section ai-popover-section--first">
425
428
  <div className="ai-popover-row">
426
- <span className="ai-popover-label">User</span>
427
- <span className="ai-popover-value ai-truncate max-w-[200px]">
429
+ <span className="ai-popover-label">
430
+ {t("status.user", "User")}
431
+ </span>
432
+ <span
433
+ className="ai-popover-value ai-truncate"
434
+ style={{ maxWidth: 200 }}
435
+ >
428
436
  {user.email}
429
437
  </span>
430
438
  </div>
@@ -432,15 +440,17 @@ export function AiStatusButton({
432
440
 
433
441
  <div className="ai-popover-section">
434
442
  <div className="ai-popover-row">
435
- <span className="ai-popover-label">API Key</span>
443
+ <span className="ai-popover-label">
444
+ {t("status.apiKey", "API Key")}
445
+ </span>
436
446
  <div className="ai-row">
437
447
  {lbIsLoadingStatus || isImplicitStatusLoading ? (
438
- <div className="ai-kv-skeleton w-[110px]" />
448
+ <div className="ai-kv-skeleton ai-kv-skeleton--110" />
439
449
  ) : (
440
450
  <span className="ai-popover-value">
441
451
  {effectiveStatus?.apiKey?.name ||
442
452
  effectiveStatus?.api_key?.name ||
443
- "Unknown"}
453
+ t("common.unknown", "Unknown")}
444
454
  </span>
445
455
  )}
446
456
  {switchApiKey &&
@@ -454,7 +464,7 @@ export function AiStatusButton({
454
464
  setShowTooltip(false);
455
465
  setShowApiKeySelector(true);
456
466
  }}
457
- title="Changer de clé API"
467
+ title={t("status.changeApiKey", "Switch API key")}
458
468
  >
459
469
  <ArrowRightLeft size={12} />
460
470
  </button>
@@ -463,10 +473,12 @@ export function AiStatusButton({
463
473
  </div>
464
474
 
465
475
  <div className="ai-popover-row">
466
- <span className="ai-popover-label">Env</span>
476
+ <span className="ai-popover-label">
477
+ {t("status.env", "Env")}
478
+ </span>
467
479
  {(lbIsLoadingStatus || isImplicitStatusLoading) &&
468
480
  !effectiveStatus?.apiKey?.env ? (
469
- <div className="ai-kv-skeleton w-12" />
481
+ <div className="ai-kv-skeleton ai-kv-skeleton--48" />
470
482
  ) : (
471
483
  <span className="ai-popover-value">
472
484
  {effectiveStatus?.apiKey?.env ||
@@ -477,10 +489,12 @@ export function AiStatusButton({
477
489
  </div>
478
490
 
479
491
  <div className="ai-popover-row">
480
- <span className="ai-popover-label">Rate Limit</span>
492
+ <span className="ai-popover-label">
493
+ {t("status.rateLimit", "Rate Limit")}
494
+ </span>
481
495
  {(lbIsLoadingStatus || isImplicitStatusLoading) &&
482
496
  !effectiveStatus?.apiKey?.rate_limit_rpm ? (
483
- <div className="ai-kv-skeleton w-[92px]" />
497
+ <div className="ai-kv-skeleton ai-kv-skeleton--92" />
484
498
  ) : (
485
499
  <span className="ai-popover-value">
486
500
  {effectiveStatus?.apiKey?.rate_limit_rpm ||
@@ -492,26 +506,32 @@ export function AiStatusButton({
492
506
  </div>
493
507
 
494
508
  <div className="ai-popover-row">
495
- <span className="ai-popover-label">Auth</span>
509
+ <span className="ai-popover-label">
510
+ {t("status.auth", "Auth")}
511
+ </span>
496
512
  <span className="ai-popover-value">
497
513
  {lbIsLoadingStatus || isImplicitStatusLoading
498
514
  ? "..."
499
515
  : authTypeValue ||
500
516
  lbStatus ||
501
- "unknown"}
517
+ t("status.unknown", "unknown")}
502
518
  </span>
503
519
  </div>
504
520
  </div>
505
521
 
506
522
  <div className="ai-popover-section">
507
- <div className="ai-popover-header text-xs mb-2">Wallet</div>
523
+ <div className="ai-popover-header ai-popover-header--sub">
524
+ {t("status.wallet", "Wallet")}
525
+ </div>
508
526
  <div className="ai-popover-row">
509
- <span className="ai-popover-label">Total</span>
527
+ <span className="ai-popover-label">
528
+ {t("status.total", "Total")}
529
+ </span>
510
530
  {(lbIsLoadingStatus || isImplicitStatusLoading) &&
511
531
  !effectiveStatus?.balance ? (
512
532
  <div className="ai-row">
513
- <div className="ai-kv-skeleton w-[120px]" />
514
- <div className="ai-kv-skeleton w-7 h-7 rounded-full" />
533
+ <div className="ai-kv-skeleton ai-kv-skeleton--120" />
534
+ <div className="ai-kv-skeleton ai-kv-skeleton--circle" />
515
535
  </div>
516
536
  ) : (
517
537
  <div className="ai-row">
@@ -525,15 +545,17 @@ export function AiStatusButton({
525
545
  </div>
526
546
 
527
547
  <div className="ai-popover-section">
528
- <div className="ai-popover-header text-xs mb-2">
529
- Storage
548
+ <div className="ai-popover-header ai-popover-header--sub">
549
+ {t("status.storage", "Storage")}
530
550
  </div>
531
551
  <div className="ai-popover-row">
532
- <span className="ai-popover-label">Total</span>
552
+ <span className="ai-popover-label">
553
+ {t("status.total", "Total")}
554
+ </span>
533
555
  {lbIsLoadingStorage || isImplicitStorageLoading ? (
534
556
  <div className="ai-row">
535
- <div className="ai-kv-skeleton w-[120px]" />
536
- <div className="ai-kv-skeleton w-7 h-7 rounded-full" />
557
+ <div className="ai-kv-skeleton ai-kv-skeleton--120" />
558
+ <div className="ai-kv-skeleton ai-kv-skeleton--circle" />
537
559
  </div>
538
560
  ) : (
539
561
  <div className="ai-row">
@@ -550,13 +572,15 @@ export function AiStatusButton({
550
572
  <div className="ai-status-actions">
551
573
  {QUICK_LINKS.map((item) => {
552
574
  const Icon = item.icon;
575
+ const label = t(item.titleKey, item.titleKey);
553
576
  return (
554
577
  <button
555
578
  key={item.href}
556
579
  type="button"
557
580
  className="ai-status-action-btn"
558
581
  onClick={() => window.open(item.href, "_blank")}
559
- title={item.title}
582
+ title={label}
583
+ data-ai-tip={label}
560
584
  >
561
585
  <Icon size={17} />
562
586
  </button>
@@ -578,7 +602,8 @@ export function AiStatusButton({
578
602
  console.error("Logout failed:", error);
579
603
  }
580
604
  }}
581
- title="Logout"
605
+ title={t("status.logout", "Logout")}
606
+ data-ai-tip={t("status.logout", "Logout")}
582
607
  >
583
608
  <LogOut size={17} />
584
609
  </button>
@@ -589,20 +614,24 @@ export function AiStatusButton({
589
614
  ) : (
590
615
  <div className="ai-popover-body">
591
616
  <div className="ai-popover-header">
592
- LastBrain Authentication
617
+ {t("status.lastbrainAuth", "LastBrain Authentication")}
593
618
  </div>
594
- <p className="ai-signin-subtitle mt-0">
595
- Connectez-vous pour accéder aux fonctionnalités IA.
619
+ <p className="ai-signin-subtitle" style={{ marginTop: 0 }}>
620
+ {t(
621
+ "status.connectToAccess",
622
+ "Sign in to access AI features."
623
+ )}
596
624
  </p>
597
625
  <button
598
626
  type="button"
599
- className="ai-btn ai-btn--auth w-full mt-2"
627
+ className="ai-btn ai-btn--auth"
628
+ style={{ width: "100%", marginTop: 8 }}
600
629
  onClick={() => {
601
630
  setShowSigninModal(true);
602
631
  setShowTooltip(false);
603
632
  }}
604
633
  >
605
- Se connecter
634
+ {t("auth.signIn", "Sign in")}
606
635
  </button>
607
636
  </div>
608
637
  )}
@@ -613,7 +642,7 @@ export function AiStatusButton({
613
642
 
614
643
  return (
615
644
  <>
616
- <div className="relative inline-block">
645
+ <div style={{ position: "relative", display: "inline-block" }}>
617
646
  <button
618
647
  ref={buttonRef}
619
648
  className={triggerClass}
@@ -631,17 +660,17 @@ export function AiStatusButton({
631
660
  disabled={loading || isSelectingApiKey}
632
661
  title={
633
662
  requiresApiKeySelection
634
- ? "Sélectionnez une clé API"
635
- : "Voir le status"
663
+ ? t("status.selectApiKey", "Select an API key")
664
+ : t("status.view", "View status")
636
665
  }
637
- aria-label="AI status"
666
+ aria-label={t("status.aiStatusAria", "AI status")}
638
667
  >
639
668
  {renderTriggerIcon()}
640
669
  </button>
641
670
 
642
671
  {showCornerLoading ? (
643
672
  <span className="ai-status-loading-dot" aria-hidden="true">
644
- <Loader2 size={7} className="ai-spinner text-[var(--ai-primary)]" />
673
+ <Loader2 size={7} className="ai-spinner" />
645
674
  </span>
646
675
  ) : null}
647
676
  </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,18 +107,24 @@ 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
125
  <span className="ai-pill ai-pill--cost">
106
126
  <XCircle size={12} />
107
- Inactive
127
+ {t("common.inactive", "Inactive")}
108
128
  </span>
109
129
  )}
110
130
  </label>
@@ -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,9 @@ 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 ? t("auth.signOut", "Sign out") : connectLabel;
43
47
 
44
48
  return (
45
49
  <>
@@ -52,7 +56,7 @@ export function LBConnectButton({
52
56
  {status === "loading" ? (
53
57
  <>
54
58
  <Loader2 size={16} className="animate-spin" />
55
- Chargement...
59
+ {t("common.loading", "Loading...")}
56
60
  </>
57
61
  ) : status === "ready" && user ? (
58
62
  <>
@@ -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
  ))}