@lastbrain/ai-ui-react 1.0.75 → 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/AiContextButton.d.ts +1 -1
- package/dist/components/AiContextButton.d.ts.map +1 -1
- package/dist/components/AiImageButton.js +2 -2
- 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/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/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/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/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/AiContextButton.tsx +1 -1
- package/src/components/AiImageButton.tsx +2 -2
- package/src/components/AiInput.tsx +9 -4
- package/src/components/AiPromptPanel.tsx +31 -6
- package/src/components/AiSelect.tsx +2 -2
- package/src/components/AiStatusButton.tsx +11 -8
- package/src/components/AiTextarea.tsx +25 -9
- package/src/components/ErrorToast.tsx +3 -3
- package/src/components/LBKeyPicker.tsx +10 -10
- package/src/components/LBSigninModal.tsx +2 -1
- package/src/components/UsageToast.tsx +4 -4
- package/src/context/LBAuthProvider.tsx +12 -5
- package/src/examples/AiImageGenerator.tsx +1 -1
- package/src/hooks/useAiStatus.ts +47 -5
- package/src/styles.css +3 -3
- package/src/utils/errorHandler.ts +3 -3
- package/src/utils/modelManagement.ts +3 -3
|
@@ -74,7 +74,7 @@ export function AiImageButton({
|
|
|
74
74
|
requestId: string;
|
|
75
75
|
tokens: number;
|
|
76
76
|
} | null>(null);
|
|
77
|
-
const { showUsageToast
|
|
77
|
+
const { showUsageToast } = useUsageToast();
|
|
78
78
|
const { showErrorToast, errorData, errorKey, clearError } = useErrorToast();
|
|
79
79
|
|
|
80
80
|
// Rendre l'authentification optionnelle
|
|
@@ -126,7 +126,7 @@ export function AiImageButton({
|
|
|
126
126
|
type: "success",
|
|
127
127
|
message: t("ai.image.savedSuccess", "Image saved"),
|
|
128
128
|
});
|
|
129
|
-
} catch
|
|
129
|
+
} catch {
|
|
130
130
|
onToast?.({
|
|
131
131
|
type: "error",
|
|
132
132
|
message: t("ai.image.saveError", "Error while saving"),
|
|
@@ -34,7 +34,7 @@ export function AiInput({
|
|
|
34
34
|
context,
|
|
35
35
|
model,
|
|
36
36
|
prompt,
|
|
37
|
-
editMode = false,
|
|
37
|
+
editMode: _editMode = false,
|
|
38
38
|
enableModelManagement = true,
|
|
39
39
|
storeOutputs,
|
|
40
40
|
artifactTitle,
|
|
@@ -51,7 +51,12 @@ export function AiInput({
|
|
|
51
51
|
inputProps.value?.toString() || inputProps.defaultValue?.toString() || ""
|
|
52
52
|
);
|
|
53
53
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
54
|
-
const {
|
|
54
|
+
const {
|
|
55
|
+
showUsageToast: _showUsageToast,
|
|
56
|
+
toastData,
|
|
57
|
+
toastKey,
|
|
58
|
+
clearToast,
|
|
59
|
+
} = useUsageToast();
|
|
55
60
|
|
|
56
61
|
// Rendre l'authentification optionnelle
|
|
57
62
|
let lbStatus: string | undefined;
|
|
@@ -77,7 +82,7 @@ export function AiInput({
|
|
|
77
82
|
const baseUrl = propBaseUrl ?? ctxBaseUrl;
|
|
78
83
|
const apiKeyId = propApiKeyId ?? ctxApiKeyId;
|
|
79
84
|
|
|
80
|
-
const { models } = useAiModels({
|
|
85
|
+
const { models: _models } = useAiModels({
|
|
81
86
|
baseUrl,
|
|
82
87
|
apiKeyId,
|
|
83
88
|
modelType: "text-or-language",
|
|
@@ -104,7 +109,7 @@ export function AiInput({
|
|
|
104
109
|
const handleSubmit = async (
|
|
105
110
|
selectedModel: string,
|
|
106
111
|
selectedPrompt: string,
|
|
107
|
-
|
|
112
|
+
_promptId?: string
|
|
108
113
|
) => {
|
|
109
114
|
try {
|
|
110
115
|
const resolvedContext = inputValue || context || undefined;
|
|
@@ -102,7 +102,7 @@ function AiPromptPanelInternal({
|
|
|
102
102
|
isOpen,
|
|
103
103
|
onClose,
|
|
104
104
|
onSubmit,
|
|
105
|
-
uiMode
|
|
105
|
+
uiMode = "modal",
|
|
106
106
|
models = [],
|
|
107
107
|
sourceText,
|
|
108
108
|
children,
|
|
@@ -405,9 +405,29 @@ function AiPromptPanelInternal({
|
|
|
405
405
|
return null;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
const isDrawer = uiMode === "drawer";
|
|
409
|
+
const panelContainerStyle = isDrawer
|
|
410
|
+
? {
|
|
411
|
+
...aiStyles.modal,
|
|
412
|
+
alignItems: "stretch",
|
|
413
|
+
justifyContent: "flex-end",
|
|
414
|
+
padding: "0",
|
|
415
|
+
}
|
|
416
|
+
: aiStyles.modal;
|
|
417
|
+
const panelContentStyle = isDrawer
|
|
418
|
+
? {
|
|
419
|
+
...aiStyles.modalContent,
|
|
420
|
+
width: "min(92vw, 640px)",
|
|
421
|
+
maxWidth: "640px",
|
|
422
|
+
maxHeight: "100vh",
|
|
423
|
+
height: "100vh",
|
|
424
|
+
borderRadius: "16px 0 0 16px",
|
|
425
|
+
}
|
|
426
|
+
: aiStyles.modalContent;
|
|
427
|
+
|
|
408
428
|
if (children) {
|
|
409
429
|
return createPortal(
|
|
410
|
-
<div style={
|
|
430
|
+
<div style={panelContainerStyle} onKeyDown={handleKeyDown}>
|
|
411
431
|
{children(renderProps)}
|
|
412
432
|
</div>,
|
|
413
433
|
portalRoot
|
|
@@ -415,7 +435,7 @@ function AiPromptPanelInternal({
|
|
|
415
435
|
}
|
|
416
436
|
|
|
417
437
|
return createPortal(
|
|
418
|
-
<div style={
|
|
438
|
+
<div style={panelContainerStyle} onKeyDown={handleKeyDown}>
|
|
419
439
|
<div
|
|
420
440
|
style={{
|
|
421
441
|
...aiStyles.modalOverlay,
|
|
@@ -426,13 +446,18 @@ function AiPromptPanelInternal({
|
|
|
426
446
|
/>
|
|
427
447
|
<div
|
|
428
448
|
style={{
|
|
429
|
-
...
|
|
449
|
+
...panelContentStyle,
|
|
430
450
|
opacity: isClosing ? 0 : 1,
|
|
431
|
-
transform: isClosing
|
|
451
|
+
transform: isClosing
|
|
452
|
+
? isDrawer
|
|
453
|
+
? "translateX(16px)"
|
|
454
|
+
: "translateY(12px)"
|
|
455
|
+
: isDrawer
|
|
456
|
+
? "translateX(0)"
|
|
457
|
+
: "translateY(0)",
|
|
432
458
|
transition: "opacity 200ms ease, transform 200ms ease",
|
|
433
459
|
display: "flex",
|
|
434
460
|
flexDirection: "column",
|
|
435
|
-
maxHeight: "85vh",
|
|
436
461
|
overflow: "hidden",
|
|
437
462
|
}}
|
|
438
463
|
>
|
|
@@ -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 ||
|
|
@@ -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) {
|
|
@@ -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
|
|
|
@@ -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);
|
|
@@ -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
|
};
|
package/src/styles.css
CHANGED
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
align-items: center;
|
|
196
196
|
gap: 10px;
|
|
197
197
|
min-height: var(--ai-control-h, var(--ai-size-md-h));
|
|
198
|
-
padding: 0
|
|
198
|
+
padding: 0 8px;
|
|
199
199
|
border-radius: var(--ai-radius-current, var(--ai-radius-full));
|
|
200
200
|
border: 1px solid var(--ai-border);
|
|
201
201
|
background:
|
|
@@ -362,8 +362,8 @@
|
|
|
362
362
|
.ai-shell--textarea .ai-control-action,
|
|
363
363
|
.ai-shell--textarea .ai-spark {
|
|
364
364
|
position: absolute;
|
|
365
|
-
right:
|
|
366
|
-
bottom:
|
|
365
|
+
right: 8px;
|
|
366
|
+
bottom: 8px;
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
/* Generic buttons */
|
|
@@ -13,7 +13,7 @@ type NormalizedErrorLike = {
|
|
|
13
13
|
message: string;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
function isNormalizedErrorLike(value:
|
|
16
|
+
function isNormalizedErrorLike(value: any): value is NormalizedErrorLike {
|
|
17
17
|
if (!value || typeof value !== "object") {
|
|
18
18
|
return false;
|
|
19
19
|
}
|
|
@@ -24,7 +24,7 @@ function isNormalizedErrorLike(value: unknown): value is NormalizedErrorLike {
|
|
|
24
24
|
/**
|
|
25
25
|
* Parse et uniformise la gestion des erreurs des composants AI
|
|
26
26
|
*/
|
|
27
|
-
export function parseAIError(error:
|
|
27
|
+
export function parseAIError(error: any): ParsedError {
|
|
28
28
|
// Si l'erreur est déjà un objet normalisé
|
|
29
29
|
if (isNormalizedErrorLike(error)) {
|
|
30
30
|
return {
|
|
@@ -173,7 +173,7 @@ export interface ErrorToastCallback {
|
|
|
173
173
|
* @param showInternalToast Callback interne optionnelle pour afficher un toast dans le composant
|
|
174
174
|
*/
|
|
175
175
|
export function handleAIError(
|
|
176
|
-
error:
|
|
176
|
+
error: any,
|
|
177
177
|
onToast?: ErrorToastCallback,
|
|
178
178
|
showInternalToast?: (error: { message: string; code?: string }) => void
|
|
179
179
|
): void {
|
|
@@ -15,7 +15,7 @@ interface ProvidersResponse {
|
|
|
15
15
|
providers?: Provider[];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
interface
|
|
18
|
+
interface _ModelsResponse {
|
|
19
19
|
models?: ModelInfo[];
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -32,7 +32,7 @@ interface ModelInfo {
|
|
|
32
32
|
costPer1M?: number;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function isModelCategory(value:
|
|
35
|
+
function isModelCategory(value: any): value is ModelCategory {
|
|
36
36
|
return (
|
|
37
37
|
value === "text" ||
|
|
38
38
|
value === "image" ||
|
|
@@ -41,7 +41,7 @@ function isModelCategory(value: unknown): value is ModelCategory {
|
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function isModelInfo(value:
|
|
44
|
+
function isModelInfo(value: any): value is ModelInfo {
|
|
45
45
|
if (!value || typeof value !== "object") return false;
|
|
46
46
|
const v = value as Record<string, unknown>;
|
|
47
47
|
return (
|