@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.
- 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 +26 -12
- package/dist/components/AiImageButton.d.ts.map +1 -1
- package/dist/components/AiImageButton.js +35 -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 +6 -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 +31 -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 +14 -8
- package/src/components/AiContextButton.tsx +53 -20
- package/src/components/AiImageButton.tsx +58 -25
- package/src/components/AiInput.tsx +20 -5
- package/src/components/AiModelSelect.tsx +5 -1
- package/src/components/AiPromptPanel.tsx +203 -76
- package/src/components/AiSelect.tsx +8 -3
- package/src/components/AiStatusButton.tsx +75 -46
- package/src/components/AiTextarea.tsx +24 -6
- package/src/components/ErrorToast.tsx +4 -2
- package/src/components/LBApiKeySelector.tsx +29 -9
- package/src/components/LBConnectButton.tsx +7 -3
- package/src/components/LBKeyPicker.tsx +10 -4
- package/src/components/LBSigninModal.tsx +33 -15
- package/src/components/UsageToast.tsx +4 -2
- package/src/context/I18nContext.tsx +75 -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 +38 -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;
|
|
@@ -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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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">
|
|
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">
|
|
427
|
-
|
|
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">
|
|
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
|
|
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="
|
|
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">
|
|
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
|
|
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">
|
|
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
|
|
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">
|
|
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
|
|
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">
|
|
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
|
|
514
|
-
<div className="ai-kv-skeleton
|
|
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
|
|
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">
|
|
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
|
|
536
|
-
<div className="ai-kv-skeleton
|
|
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={
|
|
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
|
|
595
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
? "
|
|
635
|
-
: "
|
|
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
|
|
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?.({
|
|
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,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
|
-
|
|
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,9 @@ 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 ? 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
|
-
|
|
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("
|
|
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
|
))}
|