@lastbrain/ai-ui-react 1.0.74 → 1.0.76
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/AiContextButton.d.ts +1 -1
- package/dist/components/AiContextButton.d.ts.map +1 -1
- package/dist/components/AiContextButton.js +3 -2
- package/dist/components/AiImageButton.d.ts.map +1 -1
- package/dist/components/AiImageButton.js +6 -3
- package/dist/components/AiInput.d.ts +1 -1
- package/dist/components/AiInput.d.ts.map +1 -1
- package/dist/components/AiInput.js +4 -4
- package/dist/components/AiModelSelect.d.ts.map +1 -1
- package/dist/components/AiPromptPanel.js +30 -6
- package/dist/components/AiSelect.d.ts +1 -1
- package/dist/components/AiSelect.d.ts.map +1 -1
- package/dist/components/AiSelect.js +1 -1
- package/dist/components/AiStatusButton.d.ts.map +1 -1
- package/dist/components/AiStatusButton.js +2 -2
- package/dist/components/AiTextarea.d.ts +2 -1
- package/dist/components/AiTextarea.d.ts.map +1 -1
- package/dist/components/AiTextarea.js +16 -6
- package/dist/components/ErrorToast.js +3 -3
- package/dist/components/LBConnectButton.d.ts.map +1 -1
- package/dist/components/LBConnectButton.js +1 -3
- package/dist/components/LBKeyPicker.js +9 -9
- package/dist/components/LBSigninModal.d.ts.map +1 -1
- package/dist/components/UsageToast.d.ts +3 -3
- package/dist/components/UsageToast.d.ts.map +1 -1
- package/dist/context/I18nContext.d.ts +1 -1
- package/dist/context/I18nContext.d.ts.map +1 -1
- package/dist/context/I18nContext.js +1 -1
- package/dist/context/LBAuthProvider.d.ts.map +1 -1
- package/dist/context/LBAuthProvider.js +12 -5
- package/dist/examples/AiImageGenerator.js +1 -1
- package/dist/hooks/useAiStatus.d.ts.map +1 -1
- package/dist/hooks/useAiStatus.js +43 -5
- package/dist/hooks/useLoadingTimer.d.ts.map +1 -1
- package/dist/hooks/useLoadingTimer.js +11 -7
- package/dist/styles.css +3 -3
- package/dist/utils/errorHandler.d.ts +2 -2
- package/dist/utils/errorHandler.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/AiChipLabel.tsx +1 -4
- package/src/components/AiContextButton.tsx +15 -6
- package/src/components/AiImageButton.tsx +10 -4
- package/src/components/AiInput.tsx +9 -4
- package/src/components/AiModelSelect.tsx +3 -1
- package/src/components/AiPromptPanel.tsx +83 -49
- package/src/components/AiSelect.tsx +2 -2
- package/src/components/AiStatusButton.tsx +48 -27
- package/src/components/AiTextarea.tsx +25 -9
- package/src/components/ErrorToast.tsx +3 -3
- package/src/components/LBApiKeySelector.tsx +5 -5
- package/src/components/LBConnectButton.tsx +1 -3
- package/src/components/LBKeyPicker.tsx +10 -10
- package/src/components/LBSigninModal.tsx +12 -9
- package/src/components/UsageToast.tsx +4 -4
- package/src/context/I18nContext.tsx +6 -2
- package/src/context/LBAuthProvider.tsx +12 -5
- package/src/examples/AiImageGenerator.tsx +1 -1
- package/src/hooks/useAiStatus.ts +47 -5
- package/src/hooks/useLoadingTimer.ts +14 -8
- package/src/styles.css +3 -3
- package/src/utils/errorHandler.ts +3 -3
- package/src/utils/modelManagement.ts +3 -3
|
@@ -207,14 +207,17 @@ export function AiStatusButton({
|
|
|
207
207
|
|
|
208
208
|
const canPortal = typeof document !== "undefined";
|
|
209
209
|
|
|
210
|
-
const effectiveStatus =
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
210
|
+
const effectiveStatus = useMemo(
|
|
211
|
+
() =>
|
|
212
|
+
lbStatus === "ready"
|
|
213
|
+
? {
|
|
214
|
+
...(lbApiStatus || {}),
|
|
215
|
+
...(lbBasicStatus || {}),
|
|
216
|
+
storage: lbStorageStatus?.storage || lbApiStatus?.storage,
|
|
217
|
+
}
|
|
218
|
+
: status || null,
|
|
219
|
+
[lbApiStatus, lbBasicStatus, lbStatus, lbStorageStatus, status]
|
|
220
|
+
);
|
|
218
221
|
|
|
219
222
|
const hasApiKeySelected = Boolean(
|
|
220
223
|
effectiveStatus?.apiKey?.id ||
|
|
@@ -229,8 +232,7 @@ export function AiStatusButton({
|
|
|
229
232
|
: undefined;
|
|
230
233
|
const requiresApiKeySelection =
|
|
231
234
|
lbStatus === "ready" && !hasApiKeySelected && apiKeys.length > 0;
|
|
232
|
-
const isApiKeyAuthMode =
|
|
233
|
-
authTypeValue === "api_key";
|
|
235
|
+
const isApiKeyAuthMode = authTypeValue === "api_key";
|
|
234
236
|
|
|
235
237
|
const [tooltipStyle, setTooltipStyle] = useState<Record<string, string>>({});
|
|
236
238
|
|
|
@@ -328,11 +330,11 @@ export function AiStatusButton({
|
|
|
328
330
|
|
|
329
331
|
const hasApiKeyMeta = Boolean(
|
|
330
332
|
effectiveStatus?.apiKey?.name ||
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
333
|
+
effectiveStatus?.api_key?.name ||
|
|
334
|
+
effectiveStatus?.apiKey?.env ||
|
|
335
|
+
effectiveStatus?.api_key?.env ||
|
|
336
|
+
effectiveStatus?.apiKey?.rate_limit_rpm ||
|
|
337
|
+
effectiveStatus?.api_key?.rate_limit_rpm
|
|
336
338
|
);
|
|
337
339
|
const hasBalanceMeta = Boolean(effectiveStatus?.balance);
|
|
338
340
|
const hasStorageMeta = Boolean(effectiveStatus?.storage);
|
|
@@ -422,10 +424,14 @@ export function AiStatusButton({
|
|
|
422
424
|
{lbStatus === "ready" && user ? (
|
|
423
425
|
<>
|
|
424
426
|
<div className="ai-popover-body">
|
|
425
|
-
<div className="ai-popover-header">
|
|
427
|
+
<div className="ai-popover-header">
|
|
428
|
+
{t("status.title", "API Status")}
|
|
429
|
+
</div>
|
|
426
430
|
<div className="ai-popover-section ai-popover-section--first">
|
|
427
431
|
<div className="ai-popover-row">
|
|
428
|
-
<span className="ai-popover-label">
|
|
432
|
+
<span className="ai-popover-label">
|
|
433
|
+
{t("status.user", "User")}
|
|
434
|
+
</span>
|
|
429
435
|
<span
|
|
430
436
|
className="ai-popover-value ai-truncate"
|
|
431
437
|
style={{ maxWidth: 200 }}
|
|
@@ -437,7 +443,9 @@ export function AiStatusButton({
|
|
|
437
443
|
|
|
438
444
|
<div className="ai-popover-section">
|
|
439
445
|
<div className="ai-popover-row">
|
|
440
|
-
<span className="ai-popover-label">
|
|
446
|
+
<span className="ai-popover-label">
|
|
447
|
+
{t("status.apiKey", "API Key")}
|
|
448
|
+
</span>
|
|
441
449
|
<div className="ai-row">
|
|
442
450
|
{lbIsLoadingStatus || isImplicitStatusLoading ? (
|
|
443
451
|
<div className="ai-kv-skeleton ai-kv-skeleton--110" />
|
|
@@ -468,7 +476,9 @@ export function AiStatusButton({
|
|
|
468
476
|
</div>
|
|
469
477
|
|
|
470
478
|
<div className="ai-popover-row">
|
|
471
|
-
<span className="ai-popover-label">
|
|
479
|
+
<span className="ai-popover-label">
|
|
480
|
+
{t("status.env", "Env")}
|
|
481
|
+
</span>
|
|
472
482
|
{(lbIsLoadingStatus || isImplicitStatusLoading) &&
|
|
473
483
|
!effectiveStatus?.apiKey?.env ? (
|
|
474
484
|
<div className="ai-kv-skeleton ai-kv-skeleton--48" />
|
|
@@ -482,7 +492,9 @@ export function AiStatusButton({
|
|
|
482
492
|
</div>
|
|
483
493
|
|
|
484
494
|
<div className="ai-popover-row">
|
|
485
|
-
<span className="ai-popover-label">
|
|
495
|
+
<span className="ai-popover-label">
|
|
496
|
+
{t("status.rateLimit", "Rate Limit")}
|
|
497
|
+
</span>
|
|
486
498
|
{(lbIsLoadingStatus || isImplicitStatusLoading) &&
|
|
487
499
|
!effectiveStatus?.apiKey?.rate_limit_rpm ? (
|
|
488
500
|
<div className="ai-kv-skeleton ai-kv-skeleton--92" />
|
|
@@ -490,14 +502,16 @@ export function AiStatusButton({
|
|
|
490
502
|
<span className="ai-popover-value">
|
|
491
503
|
{effectiveStatus?.apiKey?.rate_limit_rpm ||
|
|
492
504
|
effectiveStatus?.api_key?.rate_limit_rpm ||
|
|
493
|
-
|
|
505
|
+
0}{" "}
|
|
494
506
|
req/min
|
|
495
507
|
</span>
|
|
496
508
|
)}
|
|
497
509
|
</div>
|
|
498
510
|
|
|
499
511
|
<div className="ai-popover-row">
|
|
500
|
-
<span className="ai-popover-label">
|
|
512
|
+
<span className="ai-popover-label">
|
|
513
|
+
{t("status.auth", "Auth")}
|
|
514
|
+
</span>
|
|
501
515
|
<span className="ai-popover-value">
|
|
502
516
|
{lbIsLoadingStatus || isImplicitStatusLoading
|
|
503
517
|
? "..."
|
|
@@ -513,7 +527,9 @@ export function AiStatusButton({
|
|
|
513
527
|
{t("status.wallet", "Wallet")}
|
|
514
528
|
</div>
|
|
515
529
|
<div className="ai-popover-row">
|
|
516
|
-
<span className="ai-popover-label">
|
|
530
|
+
<span className="ai-popover-label">
|
|
531
|
+
{t("status.total", "Total")}
|
|
532
|
+
</span>
|
|
517
533
|
{(lbIsLoadingStatus || isImplicitStatusLoading) &&
|
|
518
534
|
!effectiveStatus?.balance ? (
|
|
519
535
|
<div className="ai-row">
|
|
@@ -536,7 +552,9 @@ export function AiStatusButton({
|
|
|
536
552
|
{t("status.storage", "Storage")}
|
|
537
553
|
</div>
|
|
538
554
|
<div className="ai-popover-row">
|
|
539
|
-
<span className="ai-popover-label">
|
|
555
|
+
<span className="ai-popover-label">
|
|
556
|
+
{t("status.total", "Total")}
|
|
557
|
+
</span>
|
|
540
558
|
{lbIsLoadingStorage || isImplicitStorageLoading ? (
|
|
541
559
|
<div className="ai-row">
|
|
542
560
|
<div className="ai-kv-skeleton ai-kv-skeleton--120" />
|
|
@@ -564,8 +582,8 @@ export function AiStatusButton({
|
|
|
564
582
|
type="button"
|
|
565
583
|
className="ai-status-action-btn"
|
|
566
584
|
onClick={() => window.open(item.href, "_blank")}
|
|
567
|
-
|
|
568
|
-
|
|
585
|
+
title={label}
|
|
586
|
+
data-ai-tip={label}
|
|
569
587
|
>
|
|
570
588
|
<Icon size={17} />
|
|
571
589
|
</button>
|
|
@@ -602,7 +620,10 @@ export function AiStatusButton({
|
|
|
602
620
|
{t("status.lastbrainAuth", "LastBrain Authentication")}
|
|
603
621
|
</div>
|
|
604
622
|
<p className="ai-signin-subtitle" style={{ marginTop: 0 }}>
|
|
605
|
-
{t(
|
|
623
|
+
{t(
|
|
624
|
+
"status.connectToAccess",
|
|
625
|
+
"Sign in to access AI features."
|
|
626
|
+
)}
|
|
606
627
|
</p>
|
|
607
628
|
<button
|
|
608
629
|
type="button"
|
|
@@ -27,6 +27,7 @@ export interface AiTextareaProps
|
|
|
27
27
|
uiMode?: "modal" | "drawer";
|
|
28
28
|
size?: AiSize;
|
|
29
29
|
radius?: AiRadius;
|
|
30
|
+
onPanelOpenChange?: (isOpen: boolean) => void;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export function AiTextarea({
|
|
@@ -35,10 +36,11 @@ export function AiTextarea({
|
|
|
35
36
|
uiMode = "modal",
|
|
36
37
|
size = "md",
|
|
37
38
|
radius = "lg",
|
|
39
|
+
onPanelOpenChange,
|
|
38
40
|
context,
|
|
39
41
|
model,
|
|
40
42
|
prompt,
|
|
41
|
-
editMode = false,
|
|
43
|
+
editMode: _editMode = false,
|
|
42
44
|
enableModelManagement,
|
|
43
45
|
storeOutputs,
|
|
44
46
|
artifactTitle,
|
|
@@ -61,12 +63,15 @@ export function AiTextarea({
|
|
|
61
63
|
|
|
62
64
|
// Rendre l'authentification optionnelle
|
|
63
65
|
let lbStatus: string | undefined;
|
|
66
|
+
let hasLBProvider = false;
|
|
64
67
|
try {
|
|
65
68
|
const lbContext = useLB();
|
|
66
69
|
lbStatus = lbContext.status;
|
|
70
|
+
hasLBProvider = true;
|
|
67
71
|
} catch {
|
|
68
72
|
// LBProvider n'est pas disponible, ignorer
|
|
69
73
|
lbStatus = undefined;
|
|
74
|
+
hasLBProvider = false;
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
let ctxBaseUrl: string | undefined;
|
|
@@ -82,8 +87,11 @@ export function AiTextarea({
|
|
|
82
87
|
|
|
83
88
|
const baseUrl = propBaseUrl ?? ctxBaseUrl;
|
|
84
89
|
const apiKeyId = propApiKeyId ?? ctxApiKeyId;
|
|
90
|
+
const supportsSessionAuth =
|
|
91
|
+
typeof baseUrl === "string" &&
|
|
92
|
+
(baseUrl.includes("/api/ai") || baseUrl.includes("/api/lastbrain"));
|
|
85
93
|
|
|
86
|
-
const { models } = useAiModels({
|
|
94
|
+
const { models: _models } = useAiModels({
|
|
87
95
|
baseUrl,
|
|
88
96
|
apiKeyId,
|
|
89
97
|
modelType: "text-or-language",
|
|
@@ -92,7 +100,10 @@ export function AiTextarea({
|
|
|
92
100
|
const { formatted: loadingElapsed } = useLoadingTimer(loading);
|
|
93
101
|
|
|
94
102
|
const hasConfiguration = Boolean(model && prompt);
|
|
95
|
-
const isAuthReady =
|
|
103
|
+
const isAuthReady =
|
|
104
|
+
supportsSessionAuth ||
|
|
105
|
+
lbStatus === "ready" ||
|
|
106
|
+
Boolean(process.env.LB_API_KEY);
|
|
96
107
|
const shouldShowSparkles = isAuthReady && !disabled;
|
|
97
108
|
|
|
98
109
|
const handleOpenPanel = () => {
|
|
@@ -101,16 +112,18 @@ export function AiTextarea({
|
|
|
101
112
|
return;
|
|
102
113
|
}
|
|
103
114
|
setIsOpen(true);
|
|
115
|
+
onPanelOpenChange?.(true);
|
|
104
116
|
};
|
|
105
117
|
|
|
106
118
|
const handleClosePanel = () => {
|
|
107
119
|
setIsOpen(false);
|
|
120
|
+
onPanelOpenChange?.(false);
|
|
108
121
|
};
|
|
109
122
|
|
|
110
123
|
const handleSubmit = async (
|
|
111
124
|
selectedModel: string,
|
|
112
125
|
selectedPrompt: string,
|
|
113
|
-
|
|
126
|
+
_promptId?: string
|
|
114
127
|
) => {
|
|
115
128
|
try {
|
|
116
129
|
const resolvedContext = textareaValue || context || undefined;
|
|
@@ -141,13 +154,14 @@ export function AiTextarea({
|
|
|
141
154
|
});
|
|
142
155
|
showUsageToast(result);
|
|
143
156
|
}
|
|
144
|
-
} catch
|
|
157
|
+
} catch {
|
|
145
158
|
onToast?.({
|
|
146
159
|
type: "error",
|
|
147
160
|
message: t("ai.generationError", "Failed to generate text"),
|
|
148
161
|
});
|
|
149
162
|
} finally {
|
|
150
163
|
setIsOpen(false);
|
|
164
|
+
onPanelOpenChange?.(false);
|
|
151
165
|
}
|
|
152
166
|
};
|
|
153
167
|
|
|
@@ -280,10 +294,12 @@ export function AiTextarea({
|
|
|
280
294
|
onComplete={clearToast}
|
|
281
295
|
/>
|
|
282
296
|
)}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
297
|
+
{hasLBProvider ? (
|
|
298
|
+
<LBSigninModal
|
|
299
|
+
isOpen={showAuthModal}
|
|
300
|
+
onClose={() => setShowAuthModal(false)}
|
|
301
|
+
/>
|
|
302
|
+
) : null}
|
|
287
303
|
</div>
|
|
288
304
|
);
|
|
289
305
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import "../styles/register";
|
|
4
|
-
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
5
|
import { X, AlertCircle } from "lucide-react";
|
|
6
6
|
import { useI18n } from "../context/I18nContext";
|
|
7
7
|
|
|
@@ -27,7 +27,7 @@ export function ErrorToast({
|
|
|
27
27
|
const fadeTimeoutRef = useRef<number | null>(null);
|
|
28
28
|
const autoCloseTimeoutRef = useRef<number | null>(null);
|
|
29
29
|
|
|
30
|
-
const handleClose = () => {
|
|
30
|
+
const handleClose = useCallback(() => {
|
|
31
31
|
if (isClosing) return;
|
|
32
32
|
|
|
33
33
|
// Clear auto-close timeout if user closes manually
|
|
@@ -40,7 +40,7 @@ export function ErrorToast({
|
|
|
40
40
|
setIsVisible(false);
|
|
41
41
|
onComplete?.();
|
|
42
42
|
}, 200);
|
|
43
|
-
};
|
|
43
|
+
}, [isClosing, onComplete]);
|
|
44
44
|
|
|
45
45
|
useEffect(() => {
|
|
46
46
|
if (error) {
|
|
@@ -122,11 +122,11 @@ export function LBApiKeySelector({
|
|
|
122
122
|
{t("common.active", "Active")}
|
|
123
123
|
</span>
|
|
124
124
|
) : (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
<span className="ai-pill ai-pill--cost">
|
|
126
|
+
<XCircle size={12} />
|
|
127
|
+
{t("common.inactive", "Inactive")}
|
|
128
|
+
</span>
|
|
129
|
+
)}
|
|
130
130
|
</label>
|
|
131
131
|
);
|
|
132
132
|
})}
|
|
@@ -43,9 +43,7 @@ export function LBConnectButton({
|
|
|
43
43
|
|
|
44
44
|
const connectLabel = label || t("auth.signIn", "Sign in");
|
|
45
45
|
const buttonLabel =
|
|
46
|
-
status === "ready" && user
|
|
47
|
-
? t("auth.signOut", "Sign out")
|
|
48
|
-
: connectLabel;
|
|
46
|
+
status === "ready" && user ? t("auth.signOut", "Sign out") : connectLabel;
|
|
49
47
|
|
|
50
48
|
return (
|
|
51
49
|
<>
|
|
@@ -6,7 +6,7 @@ import "../styles/register";
|
|
|
6
6
|
* Permet de changer de clé API sans se reconnecter
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { useEffect, useState } from "react";
|
|
9
|
+
import { useCallback, useEffect, useState } from "react";
|
|
10
10
|
import { useLB } from "../hooks/useLB";
|
|
11
11
|
import type { LBApiKey } from "@lastbrain/ai-ui-core";
|
|
12
12
|
import { useI18n } from "../context/I18nContext";
|
|
@@ -30,25 +30,25 @@ export function LBKeyPicker({
|
|
|
30
30
|
const [error, setError] = useState("");
|
|
31
31
|
const [showDropdown, setShowDropdown] = useState(false);
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
if (status === "ready" && accessToken) {
|
|
35
|
-
loadKeys();
|
|
36
|
-
}
|
|
37
|
-
}, [status, accessToken]);
|
|
38
|
-
|
|
39
|
-
const loadKeys = async () => {
|
|
33
|
+
const loadKeys = useCallback(async () => {
|
|
40
34
|
if (!accessToken) return;
|
|
41
35
|
|
|
42
36
|
try {
|
|
43
37
|
setLoading(true);
|
|
44
38
|
const keys = await fetchApiKeys(accessToken);
|
|
45
39
|
setApiKeys(keys);
|
|
46
|
-
} catch
|
|
40
|
+
} catch {
|
|
47
41
|
setError(t("lb.keypicker.loadError", "Unable to load API keys"));
|
|
48
42
|
} finally {
|
|
49
43
|
setLoading(false);
|
|
50
44
|
}
|
|
51
|
-
};
|
|
45
|
+
}, [accessToken, fetchApiKeys, t]);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (status === "ready" && accessToken) {
|
|
49
|
+
loadKeys();
|
|
50
|
+
}
|
|
51
|
+
}, [accessToken, loadKeys, status]);
|
|
52
52
|
|
|
53
53
|
const handleSelectKey = async (keyId: string) => {
|
|
54
54
|
if (!accessToken) return;
|
|
@@ -7,6 +7,7 @@ import { AlertCircle, Loader2, Lock, Mail, Sparkles, X } from "lucide-react";
|
|
|
7
7
|
import { useLB } from "../context/LBAuthProvider";
|
|
8
8
|
import { LBApiKeySelector } from "./LBApiKeySelector";
|
|
9
9
|
import { useI18n } from "../context/I18nContext";
|
|
10
|
+
import type { LBApiKey } from "@lastbrain/ai-ui-core";
|
|
10
11
|
|
|
11
12
|
export interface LBSigninModalProps {
|
|
12
13
|
isOpen: boolean;
|
|
@@ -23,7 +24,7 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
|
|
|
23
24
|
const [loading, setLoading] = useState(false);
|
|
24
25
|
const [error, setError] = useState("");
|
|
25
26
|
const [showKeySelector, setShowKeySelector] = useState(false);
|
|
26
|
-
const [currentApiKeys, setCurrentApiKeys] = useState<
|
|
27
|
+
const [currentApiKeys, setCurrentApiKeys] = useState<LBApiKey[]>([]);
|
|
27
28
|
|
|
28
29
|
const { login, selectApiKeyWithToken, fetchApiKeys } = lbContext || {};
|
|
29
30
|
|
|
@@ -64,9 +65,7 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
if (!fetchApiKeys || !result.accessToken) {
|
|
67
|
-
setError(
|
|
68
|
-
t("auth.modal.tokenMissing", "Access token unavailable")
|
|
69
|
-
);
|
|
68
|
+
setError(t("auth.modal.tokenMissing", "Access token unavailable"));
|
|
70
69
|
return;
|
|
71
70
|
}
|
|
72
71
|
|
|
@@ -76,9 +75,7 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
|
|
|
76
75
|
setShowKeySelector(true);
|
|
77
76
|
} catch (keyError) {
|
|
78
77
|
console.error("Failed to fetch API keys:", keyError);
|
|
79
|
-
setError(
|
|
80
|
-
t("auth.modal.apiKeysFetchError", "Failed to fetch API keys")
|
|
81
|
-
);
|
|
78
|
+
setError(t("auth.modal.apiKeysFetchError", "Failed to fetch API keys"));
|
|
82
79
|
}
|
|
83
80
|
} catch (err) {
|
|
84
81
|
setError(
|
|
@@ -186,7 +183,10 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
|
|
|
186
183
|
required
|
|
187
184
|
autoFocus
|
|
188
185
|
autoComplete="email"
|
|
189
|
-
placeholder={t(
|
|
186
|
+
placeholder={t(
|
|
187
|
+
"auth.modal.emailPlaceholder",
|
|
188
|
+
"your@email.com"
|
|
189
|
+
)}
|
|
190
190
|
/>
|
|
191
191
|
</div>
|
|
192
192
|
</div>
|
|
@@ -210,7 +210,10 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
|
|
|
210
210
|
onChange={(e) => setPassword(e.target.value)}
|
|
211
211
|
required
|
|
212
212
|
autoComplete="current-password"
|
|
213
|
-
placeholder={t(
|
|
213
|
+
placeholder={t(
|
|
214
|
+
"auth.modal.passwordPlaceholder",
|
|
215
|
+
"••••••••"
|
|
216
|
+
)}
|
|
214
217
|
/>
|
|
215
218
|
</div>
|
|
216
219
|
</div>
|
|
@@ -6,7 +6,7 @@ import { X } from "lucide-react";
|
|
|
6
6
|
import { useI18n } from "../context/I18nContext";
|
|
7
7
|
|
|
8
8
|
interface UsageToastProps {
|
|
9
|
-
result:
|
|
9
|
+
result: any;
|
|
10
10
|
position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
|
|
11
11
|
onComplete?: () => void;
|
|
12
12
|
}
|
|
@@ -46,7 +46,7 @@ export function UsageToast({
|
|
|
46
46
|
}, 200);
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
const extractUsageMessage = (data:
|
|
49
|
+
const extractUsageMessage = (data: any) => {
|
|
50
50
|
const result = data as any;
|
|
51
51
|
|
|
52
52
|
// Extract cost from various possible locations
|
|
@@ -165,10 +165,10 @@ export function UsageToast({
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
export function useUsageToast() {
|
|
168
|
-
const [toastData, setToastData] = useState<
|
|
168
|
+
const [toastData, setToastData] = useState<any>(null);
|
|
169
169
|
const [toastKey, setToastKey] = useState(0);
|
|
170
170
|
|
|
171
|
-
const showUsageToast = (result:
|
|
171
|
+
const showUsageToast = (result: any) => {
|
|
172
172
|
// Replace any existing toast with new one
|
|
173
173
|
setToastKey((prev) => prev + 1);
|
|
174
174
|
setToastData(result);
|
|
@@ -46,14 +46,18 @@ export interface I18nProviderProps {
|
|
|
46
46
|
lang?: LBSupportedLang;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export function I18nProvider({
|
|
49
|
+
export function I18nProvider({
|
|
50
|
+
children,
|
|
51
|
+
lang = defaultLang,
|
|
52
|
+
}: I18nProviderProps) {
|
|
50
53
|
const safeLang: LBSupportedLang = dictionaries[lang] ? lang : defaultLang;
|
|
51
54
|
|
|
52
55
|
const value = useMemo<I18nContextValue>(() => {
|
|
53
56
|
const dict = dictionaries[safeLang] || dictionaries[defaultLang];
|
|
54
57
|
|
|
55
58
|
const t = (key: string, fallback?: string, params?: TranslateParams) => {
|
|
56
|
-
const template =
|
|
59
|
+
const template =
|
|
60
|
+
dict[key] || dictionaries[defaultLang][key] || fallback || key;
|
|
57
61
|
if (!params) return template;
|
|
58
62
|
return template.replace(/\{(\w+)\}/g, (_, p: string) =>
|
|
59
63
|
params[p] !== undefined ? String(params[p]) : `{${p}}`
|
|
@@ -416,7 +416,7 @@ export function LBProvider({
|
|
|
416
416
|
try {
|
|
417
417
|
let data: BasicStatus;
|
|
418
418
|
try {
|
|
419
|
-
data = await lbClient.getStatus();
|
|
419
|
+
data = (await lbClient.getStatus()) as BasicStatus;
|
|
420
420
|
} catch {
|
|
421
421
|
// Backward compatibility: older backends may not expose /auth/status
|
|
422
422
|
const userData = await lbClient.getUser();
|
|
@@ -483,15 +483,17 @@ export function LBProvider({
|
|
|
483
483
|
|
|
484
484
|
setIsLoadingStorage(true);
|
|
485
485
|
try {
|
|
486
|
-
const data = await lbClient.getStorageStatus();
|
|
487
|
-
const storageData = data?.storage
|
|
486
|
+
const data = (await lbClient.getStorageStatus()) as StorageStatus;
|
|
487
|
+
const storageData: StorageStatus = data?.storage
|
|
488
|
+
? { storage: data.storage }
|
|
489
|
+
: data;
|
|
488
490
|
setStorageStatus(storageData);
|
|
489
491
|
setStorageLastFetch(now);
|
|
490
492
|
|
|
491
493
|
// Combiner avec le basic status
|
|
492
494
|
const combinedStatus = {
|
|
493
495
|
...basicStatus,
|
|
494
|
-
storage: storageData
|
|
496
|
+
storage: storageData.storage,
|
|
495
497
|
};
|
|
496
498
|
setApiStatus(combinedStatus as AiStatus);
|
|
497
499
|
} catch (error) {
|
|
@@ -624,7 +626,12 @@ export function LBProvider({
|
|
|
624
626
|
setStorageStatus(null);
|
|
625
627
|
setApiKeys([]);
|
|
626
628
|
}
|
|
627
|
-
}, [
|
|
629
|
+
}, [
|
|
630
|
+
fetchApiKeysWithSession,
|
|
631
|
+
refreshBasicStatus,
|
|
632
|
+
refreshStorageStatus,
|
|
633
|
+
state.status,
|
|
634
|
+
]);
|
|
628
635
|
|
|
629
636
|
const value: LBContextValue = {
|
|
630
637
|
...state,
|
package/src/hooks/useAiStatus.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback } from "react";
|
|
3
|
+
import { useState, useEffect, useCallback, useContext, useMemo } from "react";
|
|
4
4
|
import type { AiStatus } from "@lastbrain/ai-ui-core";
|
|
5
5
|
import { useAiClient } from "./useAiClient";
|
|
6
|
+
import { LBContext } from "../context/LBAuthProvider";
|
|
6
7
|
|
|
7
8
|
export interface UseAiStatusOptions {
|
|
8
9
|
baseUrl?: string;
|
|
@@ -18,11 +19,40 @@ export interface UseAiStatusResult {
|
|
|
18
19
|
|
|
19
20
|
export function useAiStatus(options?: UseAiStatusOptions): UseAiStatusResult {
|
|
20
21
|
const client = useAiClient(options);
|
|
22
|
+
const lbContext = useContext(LBContext);
|
|
21
23
|
const [status, setStatus] = useState<AiStatus | null>(null);
|
|
22
24
|
const [loading, setLoading] = useState(false);
|
|
23
25
|
const [error, setError] = useState<Error | null>(null);
|
|
24
26
|
|
|
27
|
+
const statusFromLB = useMemo<AiStatus | null>(() => {
|
|
28
|
+
if (!lbContext || lbContext.status !== "ready") {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (lbContext.apiStatus) {
|
|
32
|
+
return lbContext.apiStatus;
|
|
33
|
+
}
|
|
34
|
+
if (lbContext.basicStatus) {
|
|
35
|
+
return {
|
|
36
|
+
...(lbContext.basicStatus as AiStatus),
|
|
37
|
+
storage:
|
|
38
|
+
lbContext.storageStatus?.storage ||
|
|
39
|
+
(lbContext.basicStatus as AiStatus)?.storage,
|
|
40
|
+
} as AiStatus;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}, [lbContext]);
|
|
44
|
+
|
|
25
45
|
const fetchStatus = useCallback(async () => {
|
|
46
|
+
// If LBProvider is available, it already handles fast status + storage split.
|
|
47
|
+
if (lbContext && lbContext.status === "ready") {
|
|
48
|
+
try {
|
|
49
|
+
await lbContext.refreshBasicStatus();
|
|
50
|
+
} catch {
|
|
51
|
+
// Ignore: provider already tracks errors/status
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
console.log("[useAiStatus] Starting status fetch");
|
|
27
57
|
|
|
28
58
|
setLoading(true);
|
|
@@ -39,15 +69,27 @@ export function useAiStatus(options?: UseAiStatusOptions): UseAiStatusResult {
|
|
|
39
69
|
} finally {
|
|
40
70
|
setLoading(false);
|
|
41
71
|
}
|
|
42
|
-
}, [client]);
|
|
72
|
+
}, [client, lbContext]);
|
|
43
73
|
|
|
44
74
|
useEffect(() => {
|
|
75
|
+
if (statusFromLB) {
|
|
76
|
+
setStatus(statusFromLB);
|
|
77
|
+
setLoading(false);
|
|
78
|
+
setError(null);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
45
81
|
fetchStatus();
|
|
46
|
-
}, [fetchStatus]);
|
|
82
|
+
}, [fetchStatus, statusFromLB]);
|
|
83
|
+
|
|
84
|
+
const isLoadingFromLB =
|
|
85
|
+
!!lbContext &&
|
|
86
|
+
lbContext.status === "ready" &&
|
|
87
|
+
!statusFromLB &&
|
|
88
|
+
(lbContext.isLoadingStatus || lbContext.isLoadingStorage);
|
|
47
89
|
|
|
48
90
|
return {
|
|
49
|
-
status,
|
|
50
|
-
loading,
|
|
91
|
+
status: statusFromLB || status,
|
|
92
|
+
loading: isLoadingFromLB || loading,
|
|
51
93
|
error,
|
|
52
94
|
refetch: fetchStatus,
|
|
53
95
|
};
|
|
@@ -14,19 +14,25 @@ export function useLoadingTimer(active: boolean) {
|
|
|
14
14
|
|
|
15
15
|
useEffect(() => {
|
|
16
16
|
if (!active) {
|
|
17
|
-
setSeconds(0);
|
|
18
|
-
return;
|
|
17
|
+
const timeoutId = window.setTimeout(() => setSeconds(0), 0);
|
|
18
|
+
return () => window.clearTimeout(timeoutId);
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
const
|
|
20
|
+
|
|
21
|
+
const resetId = window.setTimeout(() => setSeconds(0), 0);
|
|
22
|
+
const intervalId = window.setInterval(() => {
|
|
22
23
|
setSeconds((prev) => prev + 1);
|
|
23
24
|
}, 1000);
|
|
24
|
-
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
window.clearTimeout(resetId);
|
|
28
|
+
window.clearInterval(intervalId);
|
|
29
|
+
};
|
|
25
30
|
}, [active]);
|
|
26
31
|
|
|
32
|
+
const displaySeconds = active ? seconds : 0;
|
|
33
|
+
|
|
27
34
|
return {
|
|
28
|
-
seconds,
|
|
29
|
-
formatted: formatElapsed(
|
|
35
|
+
seconds: displaySeconds,
|
|
36
|
+
formatted: formatElapsed(displaySeconds),
|
|
30
37
|
};
|
|
31
38
|
}
|
|
32
|
-
|