@lastbrain/ai-ui-react 1.0.75 → 1.0.77
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/AiContextButton.js +17 -1
- package/dist/components/AiImageButton.d.ts.map +1 -1
- package/dist/components/AiImageButton.js +19 -3
- package/dist/components/AiInput.d.ts +1 -1
- package/dist/components/AiInput.d.ts.map +1 -1
- package/dist/components/AiInput.js +21 -5
- 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 +18 -2
- package/dist/components/AiStatusButton.d.ts.map +1 -1
- package/dist/components/AiStatusButton.js +18 -4
- package/dist/components/AiTextarea.d.ts +2 -1
- package/dist/components/AiTextarea.d.ts.map +1 -1
- package/dist/components/AiTextarea.js +25 -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 +2 -0
- package/dist/context/LBAuthProvider.d.ts.map +1 -1
- package/dist/context/LBAuthProvider.js +43 -10
- package/dist/examples/AiImageGenerator.js +1 -1
- package/dist/hooks/useAiStatus.d.ts.map +1 -1
- package/dist/hooks/useAiStatus.js +58 -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 +20 -2
- package/src/components/AiImageButton.tsx +21 -3
- package/src/components/AiInput.tsx +28 -5
- package/src/components/AiPromptPanel.tsx +31 -6
- package/src/components/AiSelect.tsx +21 -3
- package/src/components/AiStatusButton.tsx +35 -10
- package/src/components/AiTextarea.tsx +33 -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 +46 -9
- package/src/examples/AiImageGenerator.tsx +1 -1
- package/src/hooks/useAiStatus.ts +62 -5
- package/src/styles.css +3 -3
- package/src/utils/errorHandler.ts +3 -3
- package/src/utils/modelManagement.ts +3 -3
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import "../styles/register";
|
|
4
4
|
import type { AiStatus, LBUser } from "@lastbrain/ai-ui-core";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
useContext,
|
|
11
|
+
useEffect,
|
|
12
|
+
} from "react";
|
|
6
13
|
import { createPortal } from "react-dom";
|
|
7
14
|
import {
|
|
8
15
|
ArrowRightLeft,
|
|
@@ -169,6 +176,8 @@ export function AiStatusButton({
|
|
|
169
176
|
let lbSelectedKey: LBApiKey | null = null;
|
|
170
177
|
let lbRefreshBasicStatus: (() => Promise<void>) | undefined;
|
|
171
178
|
let lbRefreshStorageStatus: ((force?: boolean) => Promise<void>) | undefined;
|
|
179
|
+
let lbSessionToken: string | undefined;
|
|
180
|
+
let lbHasSelectedApiKeyCookie = false;
|
|
172
181
|
|
|
173
182
|
const lbContext = useContext(LBContext);
|
|
174
183
|
if (lbContext) {
|
|
@@ -185,6 +194,8 @@ export function AiStatusButton({
|
|
|
185
194
|
lbSelectedKey = lbContext.selectedKey || null;
|
|
186
195
|
lbRefreshBasicStatus = lbContext.refreshBasicStatus;
|
|
187
196
|
lbRefreshStorageStatus = lbContext.refreshStorageStatus;
|
|
197
|
+
lbSessionToken = lbContext.session?.sessionToken;
|
|
198
|
+
lbHasSelectedApiKeyCookie = lbContext.hasSelectedApiKeyCookie;
|
|
188
199
|
} else {
|
|
189
200
|
lbStatus = undefined;
|
|
190
201
|
}
|
|
@@ -207,14 +218,17 @@ export function AiStatusButton({
|
|
|
207
218
|
|
|
208
219
|
const canPortal = typeof document !== "undefined";
|
|
209
220
|
|
|
210
|
-
const effectiveStatus =
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
221
|
+
const effectiveStatus = useMemo(
|
|
222
|
+
() =>
|
|
223
|
+
lbStatus === "ready"
|
|
224
|
+
? {
|
|
225
|
+
...(lbApiStatus || {}),
|
|
226
|
+
...(lbBasicStatus || {}),
|
|
227
|
+
storage: lbStorageStatus?.storage || lbApiStatus?.storage,
|
|
228
|
+
}
|
|
229
|
+
: status || null,
|
|
230
|
+
[lbApiStatus, lbBasicStatus, lbStatus, lbStorageStatus, status]
|
|
231
|
+
);
|
|
218
232
|
|
|
219
233
|
const hasApiKeySelected = Boolean(
|
|
220
234
|
effectiveStatus?.apiKey?.id ||
|
|
@@ -227,12 +241,23 @@ export function AiStatusButton({
|
|
|
227
241
|
typeof effectiveStatus.authType === "string"
|
|
228
242
|
? effectiveStatus.authType
|
|
229
243
|
: undefined;
|
|
244
|
+
const hasLbSession = Boolean(lbSessionToken);
|
|
230
245
|
const requiresApiKeySelection =
|
|
231
|
-
lbStatus === "ready" &&
|
|
246
|
+
lbStatus === "ready" &&
|
|
247
|
+
hasLbSession &&
|
|
248
|
+
(!lbHasSelectedApiKeyCookie || !hasApiKeySelected) &&
|
|
249
|
+
apiKeys.length > 0;
|
|
232
250
|
const isApiKeyAuthMode = authTypeValue === "api_key";
|
|
233
251
|
|
|
234
252
|
const [tooltipStyle, setTooltipStyle] = useState<Record<string, string>>({});
|
|
235
253
|
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
if (requiresApiKeySelection) {
|
|
256
|
+
setShowApiKeySelector(true);
|
|
257
|
+
setShowTooltip(false);
|
|
258
|
+
}
|
|
259
|
+
}, [requiresApiKeySelection]);
|
|
260
|
+
|
|
236
261
|
useLayoutEffect(() => {
|
|
237
262
|
if (!showTooltip || !buttonRef.current || !canPortal) {
|
|
238
263
|
return;
|
|
@@ -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,21 @@ export function AiTextarea({
|
|
|
61
63
|
|
|
62
64
|
// Rendre l'authentification optionnelle
|
|
63
65
|
let lbStatus: string | undefined;
|
|
66
|
+
let lbHasSession = false;
|
|
67
|
+
let lbHasSelectedKey = false;
|
|
68
|
+
let lbHasSelectedApiKeyCookie = false;
|
|
69
|
+
let hasLBProvider = false;
|
|
64
70
|
try {
|
|
65
71
|
const lbContext = useLB();
|
|
66
72
|
lbStatus = lbContext.status;
|
|
73
|
+
lbHasSession = Boolean(lbContext.session?.sessionToken);
|
|
74
|
+
lbHasSelectedKey = Boolean(lbContext.selectedKey?.id);
|
|
75
|
+
lbHasSelectedApiKeyCookie = lbContext.hasSelectedApiKeyCookie;
|
|
76
|
+
hasLBProvider = true;
|
|
67
77
|
} catch {
|
|
68
78
|
// LBProvider n'est pas disponible, ignorer
|
|
69
79
|
lbStatus = undefined;
|
|
80
|
+
hasLBProvider = false;
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
let ctxBaseUrl: string | undefined;
|
|
@@ -82,8 +93,13 @@ export function AiTextarea({
|
|
|
82
93
|
|
|
83
94
|
const baseUrl = propBaseUrl ?? ctxBaseUrl;
|
|
84
95
|
const apiKeyId = propApiKeyId ?? ctxApiKeyId;
|
|
96
|
+
const needsApiKeySelection =
|
|
97
|
+
hasLBProvider &&
|
|
98
|
+
lbStatus === "ready" &&
|
|
99
|
+
lbHasSession &&
|
|
100
|
+
(!lbHasSelectedKey || !lbHasSelectedApiKeyCookie);
|
|
85
101
|
|
|
86
|
-
const { models } = useAiModels({
|
|
102
|
+
const { models: _models } = useAiModels({
|
|
87
103
|
baseUrl,
|
|
88
104
|
apiKeyId,
|
|
89
105
|
modelType: "text-or-language",
|
|
@@ -92,7 +108,10 @@ export function AiTextarea({
|
|
|
92
108
|
const { formatted: loadingElapsed } = useLoadingTimer(loading);
|
|
93
109
|
|
|
94
110
|
const hasConfiguration = Boolean(model && prompt);
|
|
95
|
-
const isAuthReady =
|
|
111
|
+
const isAuthReady = hasLBProvider
|
|
112
|
+
? !needsApiKeySelection &&
|
|
113
|
+
(lbStatus === "ready" || Boolean(process.env.LB_API_KEY))
|
|
114
|
+
: Boolean(process.env.LB_API_KEY);
|
|
96
115
|
const shouldShowSparkles = isAuthReady && !disabled;
|
|
97
116
|
|
|
98
117
|
const handleOpenPanel = () => {
|
|
@@ -101,16 +120,18 @@ export function AiTextarea({
|
|
|
101
120
|
return;
|
|
102
121
|
}
|
|
103
122
|
setIsOpen(true);
|
|
123
|
+
onPanelOpenChange?.(true);
|
|
104
124
|
};
|
|
105
125
|
|
|
106
126
|
const handleClosePanel = () => {
|
|
107
127
|
setIsOpen(false);
|
|
128
|
+
onPanelOpenChange?.(false);
|
|
108
129
|
};
|
|
109
130
|
|
|
110
131
|
const handleSubmit = async (
|
|
111
132
|
selectedModel: string,
|
|
112
133
|
selectedPrompt: string,
|
|
113
|
-
|
|
134
|
+
_promptId?: string
|
|
114
135
|
) => {
|
|
115
136
|
try {
|
|
116
137
|
const resolvedContext = textareaValue || context || undefined;
|
|
@@ -141,13 +162,14 @@ export function AiTextarea({
|
|
|
141
162
|
});
|
|
142
163
|
showUsageToast(result);
|
|
143
164
|
}
|
|
144
|
-
} catch
|
|
165
|
+
} catch {
|
|
145
166
|
onToast?.({
|
|
146
167
|
type: "error",
|
|
147
168
|
message: t("ai.generationError", "Failed to generate text"),
|
|
148
169
|
});
|
|
149
170
|
} finally {
|
|
150
171
|
setIsOpen(false);
|
|
172
|
+
onPanelOpenChange?.(false);
|
|
151
173
|
}
|
|
152
174
|
};
|
|
153
175
|
|
|
@@ -280,10 +302,12 @@ export function AiTextarea({
|
|
|
280
302
|
onComplete={clearToast}
|
|
281
303
|
/>
|
|
282
304
|
)}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
305
|
+
{hasLBProvider ? (
|
|
306
|
+
<LBSigninModal
|
|
307
|
+
isOpen={showAuthModal}
|
|
308
|
+
onClose={() => setShowAuthModal(false)}
|
|
309
|
+
/>
|
|
310
|
+
) : null}
|
|
287
311
|
</div>
|
|
288
312
|
);
|
|
289
313
|
}
|
|
@@ -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);
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
useEffect,
|
|
12
12
|
useCallback,
|
|
13
13
|
useMemo,
|
|
14
|
+
useRef,
|
|
14
15
|
useState,
|
|
15
16
|
type ReactNode,
|
|
16
17
|
} from "react";
|
|
@@ -114,6 +115,8 @@ interface LBContextValue extends LBAuthState {
|
|
|
114
115
|
isLoadingStatus: boolean;
|
|
115
116
|
/** Indique si le storage est en cours de chargement */
|
|
116
117
|
isLoadingStorage: boolean;
|
|
118
|
+
/** True si le cookie api_key_selected est présent */
|
|
119
|
+
hasSelectedApiKeyCookie: boolean;
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
const LBContext = createContext<LBContextValue | undefined>(undefined);
|
|
@@ -142,6 +145,17 @@ export function LBProvider({
|
|
|
142
145
|
const [isLoadingStatus, setIsLoadingStatus] = useState(false);
|
|
143
146
|
const [isLoadingStorage, setIsLoadingStorage] = useState(false);
|
|
144
147
|
const [storageLastFetch, setStorageLastFetch] = useState<number>(0);
|
|
148
|
+
const [hasSelectedApiKeyCookie, setHasSelectedApiKeyCookie] = useState(false);
|
|
149
|
+
const previousStatusRef = useRef<LBAuthState["status"]>("loading");
|
|
150
|
+
|
|
151
|
+
const syncSelectedApiKeyCookie = useCallback(() => {
|
|
152
|
+
if (typeof document === "undefined") return;
|
|
153
|
+
const hasCookie = document.cookie
|
|
154
|
+
.split(";")
|
|
155
|
+
.map((part) => part.trim())
|
|
156
|
+
.some((part) => part.startsWith("api_key_selected=") && part.length > 17);
|
|
157
|
+
setHasSelectedApiKeyCookie(hasCookie);
|
|
158
|
+
}, []);
|
|
145
159
|
|
|
146
160
|
const lbClient = useMemo(
|
|
147
161
|
() =>
|
|
@@ -223,6 +237,13 @@ export function LBProvider({
|
|
|
223
237
|
checkSession();
|
|
224
238
|
}, [checkSession]);
|
|
225
239
|
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
syncSelectedApiKeyCookie();
|
|
242
|
+
if (typeof window === "undefined") return;
|
|
243
|
+
const interval = window.setInterval(syncSelectedApiKeyCookie, 1000);
|
|
244
|
+
return () => window.clearInterval(interval);
|
|
245
|
+
}, [syncSelectedApiKeyCookie]);
|
|
246
|
+
|
|
226
247
|
/**
|
|
227
248
|
* Récupère les clés API de l'utilisateur
|
|
228
249
|
*/
|
|
@@ -272,6 +293,7 @@ export function LBProvider({
|
|
|
272
293
|
|
|
273
294
|
setAccessToken(undefined); // Nettoyer l'access token temporaire
|
|
274
295
|
setApiKeys([]); // Nettoyer les clés API temporaires
|
|
296
|
+
setTimeout(() => syncSelectedApiKeyCookie(), 100);
|
|
275
297
|
onStatusChange?.("ready");
|
|
276
298
|
onAuthChange?.(); // Refresh provider after signin
|
|
277
299
|
} catch (error) {
|
|
@@ -284,7 +306,7 @@ export function LBProvider({
|
|
|
284
306
|
throw error;
|
|
285
307
|
}
|
|
286
308
|
},
|
|
287
|
-
[lbClient, state.user,
|
|
309
|
+
[lbClient, onAuthChange, onStatusChange, state.user, syncSelectedApiKeyCookie]
|
|
288
310
|
);
|
|
289
311
|
|
|
290
312
|
/**
|
|
@@ -416,7 +438,7 @@ export function LBProvider({
|
|
|
416
438
|
try {
|
|
417
439
|
let data: BasicStatus;
|
|
418
440
|
try {
|
|
419
|
-
data = await lbClient.getStatus();
|
|
441
|
+
data = (await lbClient.getStatus()) as BasicStatus;
|
|
420
442
|
} catch {
|
|
421
443
|
// Backward compatibility: older backends may not expose /auth/status
|
|
422
444
|
const userData = await lbClient.getUser();
|
|
@@ -483,15 +505,17 @@ export function LBProvider({
|
|
|
483
505
|
|
|
484
506
|
setIsLoadingStorage(true);
|
|
485
507
|
try {
|
|
486
|
-
const data = await lbClient.getStorageStatus();
|
|
487
|
-
const storageData = data?.storage
|
|
508
|
+
const data = (await lbClient.getStorageStatus()) as StorageStatus;
|
|
509
|
+
const storageData: StorageStatus = data?.storage
|
|
510
|
+
? { storage: data.storage }
|
|
511
|
+
: data;
|
|
488
512
|
setStorageStatus(storageData);
|
|
489
513
|
setStorageLastFetch(now);
|
|
490
514
|
|
|
491
515
|
// Combiner avec le basic status
|
|
492
516
|
const combinedStatus = {
|
|
493
517
|
...basicStatus,
|
|
494
|
-
storage: storageData
|
|
518
|
+
storage: storageData.storage,
|
|
495
519
|
};
|
|
496
520
|
setApiStatus(combinedStatus as AiStatus);
|
|
497
521
|
} catch (error) {
|
|
@@ -548,6 +572,7 @@ export function LBProvider({
|
|
|
548
572
|
}
|
|
549
573
|
await refreshBasicStatus();
|
|
550
574
|
setTimeout(() => refreshStorageStatus(), 100);
|
|
575
|
+
setTimeout(() => syncSelectedApiKeyCookie(), 100);
|
|
551
576
|
} else {
|
|
552
577
|
throw new Error("No valid authentication method available");
|
|
553
578
|
}
|
|
@@ -560,6 +585,7 @@ export function LBProvider({
|
|
|
560
585
|
lbClient,
|
|
561
586
|
refreshBasicStatus,
|
|
562
587
|
refreshStorageStatus,
|
|
588
|
+
syncSelectedApiKeyCookie,
|
|
563
589
|
]
|
|
564
590
|
);
|
|
565
591
|
|
|
@@ -580,6 +606,7 @@ export function LBProvider({
|
|
|
580
606
|
setBasicStatus(null);
|
|
581
607
|
setStorageStatus(null);
|
|
582
608
|
setStorageLastFetch(0);
|
|
609
|
+
setHasSelectedApiKeyCookie(false);
|
|
583
610
|
onStatusChange?.("needs_auth");
|
|
584
611
|
onAuthChange?.(); // Refresh provider after logout
|
|
585
612
|
}
|
|
@@ -610,21 +637,30 @@ export function LBProvider({
|
|
|
610
637
|
}
|
|
611
638
|
}, [lbClient, state.status]);
|
|
612
639
|
|
|
613
|
-
// Refresh status
|
|
640
|
+
// Refresh status uniquement lors de la transition vers "ready"
|
|
641
|
+
// (évite les boucles status/user causées par des callbacks recréés)
|
|
614
642
|
useEffect(() => {
|
|
615
|
-
|
|
643
|
+
const wasReady = previousStatusRef.current === "ready";
|
|
644
|
+
previousStatusRef.current = state.status;
|
|
645
|
+
|
|
646
|
+
if (state.status === "ready" && !wasReady) {
|
|
616
647
|
// Appel rapide d'abord
|
|
617
648
|
refreshBasicStatus();
|
|
618
649
|
// Storage en arrière-plan après 100ms
|
|
619
650
|
setTimeout(() => refreshStorageStatus(), 100);
|
|
620
651
|
fetchApiKeysWithSession(); // Also fetch API keys list
|
|
621
|
-
} else {
|
|
652
|
+
} else if (state.status !== "ready") {
|
|
622
653
|
setApiStatus(null);
|
|
623
654
|
setBasicStatus(null);
|
|
624
655
|
setStorageStatus(null);
|
|
625
656
|
setApiKeys([]);
|
|
626
657
|
}
|
|
627
|
-
}, [
|
|
658
|
+
}, [
|
|
659
|
+
fetchApiKeysWithSession,
|
|
660
|
+
refreshBasicStatus,
|
|
661
|
+
refreshStorageStatus,
|
|
662
|
+
state.status,
|
|
663
|
+
]);
|
|
628
664
|
|
|
629
665
|
const value: LBContextValue = {
|
|
630
666
|
...state,
|
|
@@ -645,6 +681,7 @@ export function LBProvider({
|
|
|
645
681
|
refreshStorageStatus,
|
|
646
682
|
isLoadingStatus,
|
|
647
683
|
isLoadingStorage,
|
|
684
|
+
hasSelectedApiKeyCookie,
|
|
648
685
|
};
|
|
649
686
|
|
|
650
687
|
return (
|
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,48 @@ 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 exists, never call getStatus directly here:
|
|
47
|
+
// - ready: delegate refresh to provider
|
|
48
|
+
// - non-ready: avoid unauthorized polling loops
|
|
49
|
+
if (lbContext) {
|
|
50
|
+
if (lbContext.status === "ready") {
|
|
51
|
+
try {
|
|
52
|
+
await lbContext.refreshBasicStatus();
|
|
53
|
+
} catch {
|
|
54
|
+
// Ignore: provider already tracks errors/status
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
setStatus(null);
|
|
58
|
+
setLoading(false);
|
|
59
|
+
setError(null);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
26
64
|
console.log("[useAiStatus] Starting status fetch");
|
|
27
65
|
|
|
28
66
|
setLoading(true);
|
|
@@ -39,15 +77,34 @@ export function useAiStatus(options?: UseAiStatusOptions): UseAiStatusResult {
|
|
|
39
77
|
} finally {
|
|
40
78
|
setLoading(false);
|
|
41
79
|
}
|
|
42
|
-
}, [client]);
|
|
80
|
+
}, [client, lbContext]);
|
|
43
81
|
|
|
44
82
|
useEffect(() => {
|
|
83
|
+
if (lbContext && lbContext.status !== "ready") {
|
|
84
|
+
setStatus(null);
|
|
85
|
+
setLoading(false);
|
|
86
|
+
setError(null);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (statusFromLB) {
|
|
91
|
+
setStatus(statusFromLB);
|
|
92
|
+
setLoading(false);
|
|
93
|
+
setError(null);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
45
96
|
fetchStatus();
|
|
46
|
-
}, [fetchStatus]);
|
|
97
|
+
}, [fetchStatus, lbContext, statusFromLB]);
|
|
98
|
+
|
|
99
|
+
const isLoadingFromLB =
|
|
100
|
+
!!lbContext &&
|
|
101
|
+
lbContext.status === "ready" &&
|
|
102
|
+
!statusFromLB &&
|
|
103
|
+
(lbContext.isLoadingStatus || lbContext.isLoadingStorage);
|
|
47
104
|
|
|
48
105
|
return {
|
|
49
|
-
status,
|
|
50
|
-
loading,
|
|
106
|
+
status: statusFromLB || status,
|
|
107
|
+
loading: isLoadingFromLB || loading,
|
|
51
108
|
error,
|
|
52
109
|
refetch: fetchStatus,
|
|
53
110
|
};
|
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 (
|