@lastbrain/ai-ui-react 1.0.72 → 1.0.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +55 -28
- 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 +142 -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 +100 -57
- 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 +142 -1
|
@@ -25,6 +25,7 @@ import { AiContext } from "../context/AiProvider";
|
|
|
25
25
|
import { LBApiKeySelector } from "./LBApiKeySelector";
|
|
26
26
|
import { LBSigninModal } from "./LBSigninModal";
|
|
27
27
|
import type { AiRadius, AiSize } from "../types";
|
|
28
|
+
import { useI18n } from "../context/I18nContext";
|
|
28
29
|
|
|
29
30
|
export interface AiStatusButtonProps {
|
|
30
31
|
status: AiStatus | null;
|
|
@@ -53,27 +54,27 @@ type StorageUsage = {
|
|
|
53
54
|
const QUICK_LINKS = [
|
|
54
55
|
{
|
|
55
56
|
href: "https://prompt.lastbrain.io/auth/ai/tokens",
|
|
56
|
-
|
|
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;
|
|
@@ -219,12 +221,16 @@ export function AiStatusButton({
|
|
|
219
221
|
effectiveStatus?.api_key?.id ||
|
|
220
222
|
lbSelectedKey?.id
|
|
221
223
|
);
|
|
224
|
+
const authTypeValue =
|
|
225
|
+
effectiveStatus &&
|
|
226
|
+
"authType" in effectiveStatus &&
|
|
227
|
+
typeof effectiveStatus.authType === "string"
|
|
228
|
+
? effectiveStatus.authType
|
|
229
|
+
: undefined;
|
|
222
230
|
const requiresApiKeySelection =
|
|
223
231
|
lbStatus === "ready" && !hasApiKeySelected && apiKeys.length > 0;
|
|
224
232
|
const isApiKeyAuthMode =
|
|
225
|
-
|
|
226
|
-
"authType" in effectiveStatus &&
|
|
227
|
-
effectiveStatus.authType === "api_key";
|
|
233
|
+
authTypeValue === "api_key";
|
|
228
234
|
|
|
229
235
|
const [tooltipStyle, setTooltipStyle] = useState<Record<string, string>>({});
|
|
230
236
|
|
|
@@ -320,15 +326,41 @@ export function AiStatusButton({
|
|
|
320
326
|
storage.percentage ??
|
|
321
327
|
(storageTotal > 0 ? Math.round((storageUsed / storageTotal) * 100) : 0);
|
|
322
328
|
|
|
329
|
+
const hasApiKeyMeta = Boolean(
|
|
330
|
+
effectiveStatus?.apiKey?.name ||
|
|
331
|
+
effectiveStatus?.api_key?.name ||
|
|
332
|
+
effectiveStatus?.apiKey?.env ||
|
|
333
|
+
effectiveStatus?.api_key?.env ||
|
|
334
|
+
effectiveStatus?.apiKey?.rate_limit_rpm ||
|
|
335
|
+
effectiveStatus?.api_key?.rate_limit_rpm
|
|
336
|
+
);
|
|
337
|
+
const hasBalanceMeta = Boolean(effectiveStatus?.balance);
|
|
338
|
+
const hasStorageMeta = Boolean(effectiveStatus?.storage);
|
|
339
|
+
|
|
340
|
+
const isImplicitStatusLoading =
|
|
341
|
+
lbStatus === "ready" &&
|
|
342
|
+
!!user &&
|
|
343
|
+
!requiresApiKeySelection &&
|
|
344
|
+
(!authTypeValue || !hasApiKeyMeta || !hasBalanceMeta);
|
|
345
|
+
|
|
346
|
+
const isImplicitStorageLoading =
|
|
347
|
+
lbStatus === "ready" &&
|
|
348
|
+
!!user &&
|
|
349
|
+
!requiresApiKeySelection &&
|
|
350
|
+
!hasStorageMeta;
|
|
351
|
+
|
|
323
352
|
const showFastSkeleton =
|
|
324
353
|
lbStatus === "ready" &&
|
|
325
|
-
lbIsLoadingStatus
|
|
326
|
-
|
|
327
|
-
|
|
354
|
+
(lbIsLoadingStatus ||
|
|
355
|
+
isImplicitStatusLoading ||
|
|
356
|
+
(!lbBasicStatus && !effectiveStatus));
|
|
328
357
|
|
|
329
358
|
const showCornerLoading =
|
|
330
359
|
lbStatus === "ready" &&
|
|
331
|
-
(showFastSkeleton ||
|
|
360
|
+
(showFastSkeleton ||
|
|
361
|
+
lbIsLoadingStatus ||
|
|
362
|
+
lbIsLoadingStorage ||
|
|
363
|
+
isImplicitStorageLoading);
|
|
332
364
|
|
|
333
365
|
const triggerTone = useMemo(() => {
|
|
334
366
|
if (requiresApiKeySelection) return "warning";
|
|
@@ -390,11 +422,14 @@ export function AiStatusButton({
|
|
|
390
422
|
{lbStatus === "ready" && user ? (
|
|
391
423
|
<>
|
|
392
424
|
<div className="ai-popover-body">
|
|
393
|
-
<div className="ai-popover-header">API Status</div>
|
|
425
|
+
<div className="ai-popover-header">{t("status.title", "API Status")}</div>
|
|
394
426
|
<div className="ai-popover-section ai-popover-section--first">
|
|
395
427
|
<div className="ai-popover-row">
|
|
396
|
-
<span className="ai-popover-label">User</span>
|
|
397
|
-
<span
|
|
428
|
+
<span className="ai-popover-label">{t("status.user", "User")}</span>
|
|
429
|
+
<span
|
|
430
|
+
className="ai-popover-value ai-truncate"
|
|
431
|
+
style={{ maxWidth: 200 }}
|
|
432
|
+
>
|
|
398
433
|
{user.email}
|
|
399
434
|
</span>
|
|
400
435
|
</div>
|
|
@@ -402,18 +437,20 @@ export function AiStatusButton({
|
|
|
402
437
|
|
|
403
438
|
<div className="ai-popover-section">
|
|
404
439
|
<div className="ai-popover-row">
|
|
405
|
-
<span className="ai-popover-label">API Key</span>
|
|
440
|
+
<span className="ai-popover-label">{t("status.apiKey", "API Key")}</span>
|
|
406
441
|
<div className="ai-row">
|
|
407
|
-
{lbIsLoadingStatus ? (
|
|
408
|
-
<div className="ai-kv-skeleton
|
|
442
|
+
{lbIsLoadingStatus || isImplicitStatusLoading ? (
|
|
443
|
+
<div className="ai-kv-skeleton ai-kv-skeleton--110" />
|
|
409
444
|
) : (
|
|
410
445
|
<span className="ai-popover-value">
|
|
411
446
|
{effectiveStatus?.apiKey?.name ||
|
|
412
447
|
effectiveStatus?.api_key?.name ||
|
|
413
|
-
"Unknown"}
|
|
448
|
+
t("common.unknown", "Unknown")}
|
|
414
449
|
</span>
|
|
415
450
|
)}
|
|
416
|
-
{switchApiKey &&
|
|
451
|
+
{switchApiKey &&
|
|
452
|
+
!isApiKeyAuthMode &&
|
|
453
|
+
!isImplicitStatusLoading ? (
|
|
417
454
|
<button
|
|
418
455
|
type="button"
|
|
419
456
|
className="ai-icon-btn"
|
|
@@ -422,7 +459,7 @@ export function AiStatusButton({
|
|
|
422
459
|
setShowTooltip(false);
|
|
423
460
|
setShowApiKeySelector(true);
|
|
424
461
|
}}
|
|
425
|
-
title="
|
|
462
|
+
title={t("status.changeApiKey", "Switch API key")}
|
|
426
463
|
>
|
|
427
464
|
<ArrowRightLeft size={12} />
|
|
428
465
|
</button>
|
|
@@ -431,9 +468,10 @@ export function AiStatusButton({
|
|
|
431
468
|
</div>
|
|
432
469
|
|
|
433
470
|
<div className="ai-popover-row">
|
|
434
|
-
<span className="ai-popover-label">Env</span>
|
|
435
|
-
{lbIsLoadingStatus
|
|
436
|
-
|
|
471
|
+
<span className="ai-popover-label">{t("status.env", "Env")}</span>
|
|
472
|
+
{(lbIsLoadingStatus || isImplicitStatusLoading) &&
|
|
473
|
+
!effectiveStatus?.apiKey?.env ? (
|
|
474
|
+
<div className="ai-kv-skeleton ai-kv-skeleton--48" />
|
|
437
475
|
) : (
|
|
438
476
|
<span className="ai-popover-value">
|
|
439
477
|
{effectiveStatus?.apiKey?.env ||
|
|
@@ -444,42 +482,43 @@ export function AiStatusButton({
|
|
|
444
482
|
</div>
|
|
445
483
|
|
|
446
484
|
<div className="ai-popover-row">
|
|
447
|
-
<span className="ai-popover-label">Rate Limit</span>
|
|
448
|
-
{lbIsLoadingStatus &&
|
|
485
|
+
<span className="ai-popover-label">{t("status.rateLimit", "Rate Limit")}</span>
|
|
486
|
+
{(lbIsLoadingStatus || isImplicitStatusLoading) &&
|
|
449
487
|
!effectiveStatus?.apiKey?.rate_limit_rpm ? (
|
|
450
|
-
<div className="ai-kv-skeleton
|
|
488
|
+
<div className="ai-kv-skeleton ai-kv-skeleton--92" />
|
|
451
489
|
) : (
|
|
452
490
|
<span className="ai-popover-value">
|
|
453
491
|
{effectiveStatus?.apiKey?.rate_limit_rpm ||
|
|
454
492
|
effectiveStatus?.api_key?.rate_limit_rpm ||
|
|
455
|
-
|
|
493
|
+
0}{" "}
|
|
456
494
|
req/min
|
|
457
495
|
</span>
|
|
458
496
|
)}
|
|
459
497
|
</div>
|
|
460
498
|
|
|
461
499
|
<div className="ai-popover-row">
|
|
462
|
-
<span className="ai-popover-label">Auth</span>
|
|
500
|
+
<span className="ai-popover-label">{t("status.auth", "Auth")}</span>
|
|
463
501
|
<span className="ai-popover-value">
|
|
464
|
-
{lbIsLoadingStatus
|
|
502
|
+
{lbIsLoadingStatus || isImplicitStatusLoading
|
|
465
503
|
? "..."
|
|
466
|
-
:
|
|
467
|
-
"authType" in effectiveStatus &&
|
|
468
|
-
effectiveStatus.authType) ||
|
|
504
|
+
: authTypeValue ||
|
|
469
505
|
lbStatus ||
|
|
470
|
-
"unknown"}
|
|
506
|
+
t("status.unknown", "unknown")}
|
|
471
507
|
</span>
|
|
472
508
|
</div>
|
|
473
509
|
</div>
|
|
474
510
|
|
|
475
511
|
<div className="ai-popover-section">
|
|
476
|
-
<div className="ai-popover-header
|
|
512
|
+
<div className="ai-popover-header ai-popover-header--sub">
|
|
513
|
+
{t("status.wallet", "Wallet")}
|
|
514
|
+
</div>
|
|
477
515
|
<div className="ai-popover-row">
|
|
478
|
-
<span className="ai-popover-label">Total</span>
|
|
479
|
-
{lbIsLoadingStatus
|
|
516
|
+
<span className="ai-popover-label">{t("status.total", "Total")}</span>
|
|
517
|
+
{(lbIsLoadingStatus || isImplicitStatusLoading) &&
|
|
518
|
+
!effectiveStatus?.balance ? (
|
|
480
519
|
<div className="ai-row">
|
|
481
|
-
<div className="ai-kv-skeleton
|
|
482
|
-
<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" />
|
|
483
522
|
</div>
|
|
484
523
|
) : (
|
|
485
524
|
<div className="ai-row">
|
|
@@ -493,15 +532,15 @@ export function AiStatusButton({
|
|
|
493
532
|
</div>
|
|
494
533
|
|
|
495
534
|
<div className="ai-popover-section">
|
|
496
|
-
<div className="ai-popover-header
|
|
497
|
-
Storage
|
|
535
|
+
<div className="ai-popover-header ai-popover-header--sub">
|
|
536
|
+
{t("status.storage", "Storage")}
|
|
498
537
|
</div>
|
|
499
538
|
<div className="ai-popover-row">
|
|
500
|
-
<span className="ai-popover-label">Total</span>
|
|
501
|
-
{lbIsLoadingStorage ? (
|
|
539
|
+
<span className="ai-popover-label">{t("status.total", "Total")}</span>
|
|
540
|
+
{lbIsLoadingStorage || isImplicitStorageLoading ? (
|
|
502
541
|
<div className="ai-row">
|
|
503
|
-
<div className="ai-kv-skeleton
|
|
504
|
-
<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" />
|
|
505
544
|
</div>
|
|
506
545
|
) : (
|
|
507
546
|
<div className="ai-row">
|
|
@@ -518,13 +557,15 @@ export function AiStatusButton({
|
|
|
518
557
|
<div className="ai-status-actions">
|
|
519
558
|
{QUICK_LINKS.map((item) => {
|
|
520
559
|
const Icon = item.icon;
|
|
560
|
+
const label = t(item.titleKey, item.titleKey);
|
|
521
561
|
return (
|
|
522
562
|
<button
|
|
523
563
|
key={item.href}
|
|
524
564
|
type="button"
|
|
525
565
|
className="ai-status-action-btn"
|
|
526
566
|
onClick={() => window.open(item.href, "_blank")}
|
|
527
|
-
|
|
567
|
+
title={label}
|
|
568
|
+
data-ai-tip={label}
|
|
528
569
|
>
|
|
529
570
|
<Icon size={17} />
|
|
530
571
|
</button>
|
|
@@ -546,7 +587,8 @@ export function AiStatusButton({
|
|
|
546
587
|
console.error("Logout failed:", error);
|
|
547
588
|
}
|
|
548
589
|
}}
|
|
549
|
-
title="Logout"
|
|
590
|
+
title={t("status.logout", "Logout")}
|
|
591
|
+
data-ai-tip={t("status.logout", "Logout")}
|
|
550
592
|
>
|
|
551
593
|
<LogOut size={17} />
|
|
552
594
|
</button>
|
|
@@ -557,20 +599,21 @@ export function AiStatusButton({
|
|
|
557
599
|
) : (
|
|
558
600
|
<div className="ai-popover-body">
|
|
559
601
|
<div className="ai-popover-header">
|
|
560
|
-
LastBrain Authentication
|
|
602
|
+
{t("status.lastbrainAuth", "LastBrain Authentication")}
|
|
561
603
|
</div>
|
|
562
|
-
<p className="ai-signin-subtitle
|
|
563
|
-
|
|
604
|
+
<p className="ai-signin-subtitle" style={{ marginTop: 0 }}>
|
|
605
|
+
{t("status.connectToAccess", "Sign in to access AI features.")}
|
|
564
606
|
</p>
|
|
565
607
|
<button
|
|
566
608
|
type="button"
|
|
567
|
-
className="ai-btn ai-btn--auth
|
|
609
|
+
className="ai-btn ai-btn--auth"
|
|
610
|
+
style={{ width: "100%", marginTop: 8 }}
|
|
568
611
|
onClick={() => {
|
|
569
612
|
setShowSigninModal(true);
|
|
570
613
|
setShowTooltip(false);
|
|
571
614
|
}}
|
|
572
615
|
>
|
|
573
|
-
|
|
616
|
+
{t("auth.signIn", "Sign in")}
|
|
574
617
|
</button>
|
|
575
618
|
</div>
|
|
576
619
|
)}
|
|
@@ -581,7 +624,7 @@ export function AiStatusButton({
|
|
|
581
624
|
|
|
582
625
|
return (
|
|
583
626
|
<>
|
|
584
|
-
<div
|
|
627
|
+
<div style={{ position: "relative", display: "inline-block" }}>
|
|
585
628
|
<button
|
|
586
629
|
ref={buttonRef}
|
|
587
630
|
className={triggerClass}
|
|
@@ -599,17 +642,17 @@ export function AiStatusButton({
|
|
|
599
642
|
disabled={loading || isSelectingApiKey}
|
|
600
643
|
title={
|
|
601
644
|
requiresApiKeySelection
|
|
602
|
-
? "
|
|
603
|
-
: "
|
|
645
|
+
? t("status.selectApiKey", "Select an API key")
|
|
646
|
+
: t("status.view", "View status")
|
|
604
647
|
}
|
|
605
|
-
aria-label="AI status"
|
|
648
|
+
aria-label={t("status.aiStatusAria", "AI status")}
|
|
606
649
|
>
|
|
607
650
|
{renderTriggerIcon()}
|
|
608
651
|
</button>
|
|
609
652
|
|
|
610
653
|
{showCornerLoading ? (
|
|
611
654
|
<span className="ai-status-loading-dot" aria-hidden="true">
|
|
612
|
-
<Loader2 size={7} className="ai-spinner
|
|
655
|
+
<Loader2 size={7} className="ai-spinner" />
|
|
613
656
|
</span>
|
|
614
657
|
) : null}
|
|
615
658
|
</div>
|
|
@@ -17,6 +17,8 @@ import { handleAIError } from "../utils/errorHandler";
|
|
|
17
17
|
import { useLB } from "../context/LBAuthProvider";
|
|
18
18
|
import { LBSigninModal } from "./LBSigninModal";
|
|
19
19
|
import { useAiContext } from "../context/AiProvider";
|
|
20
|
+
import { useI18n } from "../context/I18nContext";
|
|
21
|
+
import { useLoadingTimer } from "../hooks/useLoadingTimer";
|
|
20
22
|
|
|
21
23
|
export interface AiTextareaProps
|
|
22
24
|
extends
|
|
@@ -46,6 +48,7 @@ export function AiTextarea({
|
|
|
46
48
|
className,
|
|
47
49
|
...textareaProps
|
|
48
50
|
}: AiTextareaProps) {
|
|
51
|
+
const { t } = useI18n();
|
|
49
52
|
const [isOpen, setIsOpen] = useState(false);
|
|
50
53
|
const [showAuthModal, setShowAuthModal] = useState(false);
|
|
51
54
|
const [textareaValue, setTextareaValue] = useState(
|
|
@@ -86,6 +89,7 @@ export function AiTextarea({
|
|
|
86
89
|
modelType: "text-or-language",
|
|
87
90
|
});
|
|
88
91
|
const { generateText, loading } = useAiCallText({ baseUrl, apiKeyId });
|
|
92
|
+
const { formatted: loadingElapsed } = useLoadingTimer(loading);
|
|
89
93
|
|
|
90
94
|
const hasConfiguration = Boolean(model && prompt);
|
|
91
95
|
const isAuthReady = lbStatus === "ready" || Boolean(process.env.LB_API_KEY);
|
|
@@ -131,11 +135,17 @@ export function AiTextarea({
|
|
|
131
135
|
textareaRef.current.value = result.text;
|
|
132
136
|
}
|
|
133
137
|
onValue?.(result.text);
|
|
134
|
-
onToast?.({
|
|
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
|
<>
|