@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.
- package/dist/components/AiChipLabel.d.ts.map +1 -1
- package/dist/components/AiChipLabel.js +10 -7
- package/dist/components/AiContextButton.d.ts +1 -1
- package/dist/components/AiContextButton.d.ts.map +1 -1
- package/dist/components/AiContextButton.js +25 -12
- package/dist/components/AiImageButton.d.ts.map +1 -1
- package/dist/components/AiImageButton.js +32 -16
- package/dist/components/AiInput.d.ts.map +1 -1
- package/dist/components/AiInput.js +15 -5
- package/dist/components/AiModelSelect.d.ts.map +1 -1
- package/dist/components/AiModelSelect.js +3 -1
- package/dist/components/AiPromptPanel.d.ts.map +1 -1
- package/dist/components/AiPromptPanel.js +72 -47
- package/dist/components/AiSelect.d.ts.map +1 -1
- package/dist/components/AiSelect.js +8 -3
- package/dist/components/AiStatusButton.d.ts.map +1 -1
- package/dist/components/AiStatusButton.js +23 -20
- package/dist/components/AiTextarea.d.ts.map +1 -1
- package/dist/components/AiTextarea.js +19 -6
- package/dist/components/ErrorToast.d.ts.map +1 -1
- package/dist/components/ErrorToast.js +4 -2
- package/dist/components/LBApiKeySelector.d.ts.map +1 -1
- package/dist/components/LBApiKeySelector.js +13 -5
- package/dist/components/LBConnectButton.d.ts.map +1 -1
- package/dist/components/LBConnectButton.js +8 -3
- package/dist/components/LBKeyPicker.d.ts.map +1 -1
- package/dist/components/LBKeyPicker.js +8 -4
- package/dist/components/LBSigninModal.d.ts.map +1 -1
- package/dist/components/LBSigninModal.js +13 -7
- package/dist/components/UsageToast.d.ts.map +1 -1
- package/dist/components/UsageToast.js +4 -2
- package/dist/context/I18nContext.d.ts +15 -0
- package/dist/context/I18nContext.d.ts.map +1 -0
- package/dist/context/I18nContext.js +44 -0
- package/dist/context/LBAuthProvider.d.ts +4 -1
- package/dist/context/LBAuthProvider.d.ts.map +1 -1
- package/dist/context/LBAuthProvider.js +3 -2
- package/dist/hooks/useAiCallImage.d.ts.map +1 -1
- package/dist/hooks/useAiCallImage.js +1 -107
- package/dist/hooks/useAiCallText.d.ts.map +1 -1
- package/dist/hooks/useAiCallText.js +1 -25
- package/dist/hooks/useLoadingTimer.d.ts +5 -0
- package/dist/hooks/useLoadingTimer.d.ts.map +1 -0
- package/dist/hooks/useLoadingTimer.js +27 -0
- package/dist/i18n/de.json +62 -0
- package/dist/i18n/en.json +128 -0
- package/dist/i18n/es.json +70 -0
- package/dist/i18n/fr.json +128 -0
- package/dist/i18n/it.json +62 -0
- package/dist/i18n/pt.json +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/styles.css +141 -1
- package/package.json +3 -3
- package/src/components/AiChipLabel.tsx +17 -8
- package/src/components/AiContextButton.tsx +44 -20
- package/src/components/AiImageButton.tsx +52 -25
- package/src/components/AiInput.tsx +20 -5
- package/src/components/AiModelSelect.tsx +3 -1
- package/src/components/AiPromptPanel.tsx +177 -59
- package/src/components/AiSelect.tsx +8 -3
- package/src/components/AiStatusButton.tsx +51 -40
- package/src/components/AiTextarea.tsx +24 -6
- package/src/components/ErrorToast.tsx +4 -2
- package/src/components/LBApiKeySelector.tsx +33 -13
- package/src/components/LBConnectButton.tsx +9 -3
- package/src/components/LBKeyPicker.tsx +10 -4
- package/src/components/LBSigninModal.tsx +31 -15
- package/src/components/UsageToast.tsx +4 -2
- package/src/context/I18nContext.tsx +71 -0
- package/src/context/LBAuthProvider.tsx +9 -1
- package/src/hooks/useAiCallImage.ts +1 -149
- package/src/hooks/useAiCallText.ts +1 -30
- package/src/hooks/useLoadingTimer.ts +32 -0
- package/src/i18n/de.json +62 -0
- package/src/i18n/en.json +128 -0
- package/src/i18n/es.json +70 -0
- package/src/i18n/fr.json +128 -0
- package/src/i18n/it.json +62 -0
- package/src/i18n/pt.json +62 -0
- package/src/index.ts +2 -0
- 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
|
-
|
|
57
|
+
titleKey: "status.dashboard",
|
|
57
58
|
icon: BarChart3,
|
|
58
59
|
},
|
|
59
60
|
{
|
|
60
61
|
href: "https://prompt.lastbrain.io/auth/ai/history",
|
|
61
|
-
|
|
62
|
+
titleKey: "status.history",
|
|
62
63
|
icon: History,
|
|
63
64
|
},
|
|
64
65
|
{
|
|
65
66
|
href: "https://prompt.lastbrain.io/auth/ai/settings",
|
|
66
|
-
|
|
67
|
+
titleKey: "status.settings",
|
|
67
68
|
icon: Settings,
|
|
68
69
|
},
|
|
69
70
|
{
|
|
70
71
|
href: "https://prompt.lastbrain.io/auth/ai/prompts",
|
|
71
|
-
|
|
72
|
+
titleKey: "status.prompts",
|
|
72
73
|
icon: FileText,
|
|
73
74
|
},
|
|
74
75
|
{
|
|
75
76
|
href: "https://prompt.lastbrain.io/auth/folder",
|
|
76
|
-
|
|
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
|
|
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
|
|
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="
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
514
|
-
<div className="ai-kv-skeleton
|
|
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
|
|
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
|
|
536
|
-
<div className="ai-kv-skeleton
|
|
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
|
-
|
|
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
|
|
595
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
? "
|
|
635
|
-
: "
|
|
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
|
|
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?.({
|
|
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?.({
|
|
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?.({
|
|
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
|
-
|
|
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="
|
|
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("
|
|
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
|
|
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">
|
|
64
|
+
<h2 className="ai-signin-title">
|
|
65
|
+
{t("status.selectApiKey", "Select an API key")}
|
|
66
|
+
</h2>
|
|
59
67
|
<p className="ai-signin-subtitle">
|
|
60
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
+
{t("auth.modal.connecting", "Signing in...")}
|
|
140
160
|
</>
|
|
141
161
|
) : (
|
|
142
|
-
"
|
|
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
|
|
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
|
|
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
|
-
|
|
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("
|
|
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
|
|
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
|
-
|
|
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">
|
|
133
|
+
<div className="text-xs text-red-600">
|
|
134
|
+
{t("common.inactive", "Inactive")}
|
|
135
|
+
</div>
|
|
130
136
|
)}
|
|
131
137
|
</button>
|
|
132
138
|
))}
|