@lastbrain/ai-ui-react 1.0.69 → 1.0.70
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 +3 -1
- package/dist/components/AiContextButton.d.ts +5 -1
- package/dist/components/AiContextButton.d.ts.map +1 -1
- package/dist/components/AiContextButton.js +10 -4
- package/dist/components/AiPromptPanel.js +12 -6
- package/dist/components/AiStatusButton.d.ts.map +1 -1
- package/dist/components/AiStatusButton.js +18 -13
- package/dist/components/AiTextarea.d.ts.map +1 -1
- package/dist/components/LBApiKeySelector.d.ts.map +1 -1
- package/dist/components/LBConnectButton.d.ts.map +1 -1
- package/dist/components/LBConnectButton.js +1 -1
- package/dist/components/LBSigninModal.d.ts.map +1 -1
- package/dist/components/LBSigninModal.js +1 -1
- package/dist/context/LBAuthProvider.d.ts +35 -3
- package/dist/context/LBAuthProvider.d.ts.map +1 -1
- package/dist/context/LBAuthProvider.js +2 -0
- package/dist/examples/AiUiPremiumShowcase.d.ts.map +1 -1
- package/dist/hooks/useAiModels.d.ts.map +1 -1
- package/dist/hooks/useModelManagement.d.ts.map +1 -1
- package/dist/utils/errorHandler.d.ts +2 -2
- package/dist/utils/errorHandler.d.ts.map +1 -1
- package/dist/utils/errorHandler.js +8 -1
- package/dist/utils/modelManagement.d.ts +13 -10
- package/dist/utils/modelManagement.d.ts.map +1 -1
- package/dist/utils/modelManagement.js +19 -2
- package/package.json +1 -1
- package/src/components/AiChipLabel.tsx +5 -1
- package/src/components/AiContextButton.tsx +44 -23
- package/src/components/AiPromptPanel.tsx +19 -15
- package/src/components/AiStatusButton.tsx +24 -19
- package/src/components/AiTextarea.tsx +3 -1
- package/src/components/LBApiKeySelector.tsx +13 -3
- package/src/components/LBConnectButton.tsx +3 -12
- package/src/components/LBSigninModal.tsx +20 -10
- package/src/context/LBAuthProvider.tsx +45 -6
- package/src/examples/AiUiPremiumShowcase.tsx +4 -1
- package/src/hooks/useAiModels.ts +2 -1
- package/src/hooks/useModelManagement.ts +2 -1
- package/src/utils/errorHandler.ts +16 -3
- package/src/utils/modelManagement.ts +53 -15
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState, type ButtonHTMLAttributes } from "react";
|
|
4
|
-
import {
|
|
5
|
-
Download,
|
|
6
|
-
FileText,
|
|
7
|
-
Loader2,
|
|
8
|
-
Lock,
|
|
9
|
-
Sparkles,
|
|
10
|
-
X,
|
|
11
|
-
} from "lucide-react";
|
|
4
|
+
import { Download, FileText, Loader2, Lock, Sparkles, X } from "lucide-react";
|
|
12
5
|
import type { BaseAiProps } from "../types";
|
|
13
6
|
import type { AiRadius, AiSize, AiVariant } from "../types";
|
|
14
7
|
import { useAiCallText } from "../hooks/useAiCallText";
|
|
@@ -20,10 +13,14 @@ import { handleAIError } from "../utils/errorHandler";
|
|
|
20
13
|
import { useLB } from "../context/LBAuthProvider";
|
|
21
14
|
import { LBSigninModal } from "./LBSigninModal";
|
|
22
15
|
|
|
16
|
+
// Types pour les données de contexte
|
|
17
|
+
type ContextData = string | number | boolean | object | unknown[] | { [key: string]: unknown };
|
|
18
|
+
|
|
23
19
|
export interface AiContextButtonProps
|
|
24
|
-
extends
|
|
20
|
+
extends
|
|
21
|
+
Omit<BaseAiProps, "onValue" | "type">,
|
|
25
22
|
Omit<ButtonHTMLAttributes<HTMLButtonElement>, "baseUrl" | "apiKeyId"> {
|
|
26
|
-
contextData:
|
|
23
|
+
contextData: ContextData;
|
|
27
24
|
contextDescription?: string;
|
|
28
25
|
onResult?: (
|
|
29
26
|
result: string,
|
|
@@ -99,7 +96,7 @@ export function AiContextButton({
|
|
|
99
96
|
setIsOpen(true);
|
|
100
97
|
};
|
|
101
98
|
|
|
102
|
-
const formatContextData = (data:
|
|
99
|
+
const formatContextData = (data: ContextData): string => {
|
|
103
100
|
if (typeof data === "string") {
|
|
104
101
|
return data;
|
|
105
102
|
}
|
|
@@ -109,7 +106,10 @@ export function AiContextButton({
|
|
|
109
106
|
return String(data);
|
|
110
107
|
};
|
|
111
108
|
|
|
112
|
-
const handleSubmit = async (
|
|
109
|
+
const handleSubmit = async (
|
|
110
|
+
selectedModel: string,
|
|
111
|
+
selectedPrompt: string
|
|
112
|
+
) => {
|
|
113
113
|
try {
|
|
114
114
|
const contextString = formatContextData(contextData);
|
|
115
115
|
const fullPrompt = `${selectedPrompt}\n\nCONTEXTE (${contextDescription}):\n${contextString}\n\nAnalyse ces données et réponds de manière structurée et claire.`;
|
|
@@ -175,12 +175,17 @@ export function AiContextButton({
|
|
|
175
175
|
return;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
const currentDate = new Date()
|
|
178
|
+
const currentDate = new Date()
|
|
179
|
+
.toLocaleDateString("fr-FR")
|
|
180
|
+
.replace(/\//g, "-");
|
|
179
181
|
const defaultName = `analyse-${currentDate}.txt`;
|
|
180
182
|
const fileName = prompt("Nom du fichier :", defaultName) || defaultName;
|
|
181
183
|
|
|
182
|
-
const content = `ANALYSE DES DONNÉES - ${new Date().toLocaleString("fr-FR")}\n\nPROMPT UTILISÉ :\n${analysisResult.prompt}\n\nRÉSULTAT DE L'ANALYSE :\n${analysisResult.content}\n\n--- MÉTADONNÉES ---\nTokens utilisés: ${analysisResult.tokens.toLocaleString()}\nCoût: $${(
|
|
183
|
-
|
|
184
|
+
const content = `ANALYSE DES DONNÉES - ${new Date().toLocaleString("fr-FR")}\n\nPROMPT UTILISÉ :\n${analysisResult.prompt}\n\nRÉSULTAT DE L'ANALYSE :\n${analysisResult.content}\n\n--- MÉTADONNÉES ---\nTokens utilisés: ${analysisResult.tokens.toLocaleString()}\nCoût: $${(apiKeyId?.includes(
|
|
185
|
+
"dev"
|
|
186
|
+
)
|
|
187
|
+
? 0
|
|
188
|
+
: analysisResult.cost
|
|
184
189
|
).toFixed(6)}\nID de requête: ${analysisResult.requestId || "N/A"}`;
|
|
185
190
|
|
|
186
191
|
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
|
|
@@ -206,7 +211,9 @@ export function AiContextButton({
|
|
|
206
211
|
onClick={handleOpenPanel}
|
|
207
212
|
disabled={disabled || loading || !isAuthReady}
|
|
208
213
|
className={`ai-btn ai-context-btn ${variantClass} ${sizeClass} ${radiusClass} ${className || ""}`}
|
|
209
|
-
title={
|
|
214
|
+
title={
|
|
215
|
+
!isAuthReady ? "Authentication required" : "Analyser avec l'IA"
|
|
216
|
+
}
|
|
210
217
|
>
|
|
211
218
|
{loading ? (
|
|
212
219
|
<>
|
|
@@ -251,14 +258,21 @@ export function AiContextButton({
|
|
|
251
258
|
}
|
|
252
259
|
}}
|
|
253
260
|
>
|
|
254
|
-
<div
|
|
261
|
+
<div
|
|
262
|
+
className="ai-popover ai-result-modal"
|
|
263
|
+
onClick={(e) => e.stopPropagation()}
|
|
264
|
+
>
|
|
255
265
|
<div className="ai-result-header">
|
|
256
266
|
<div className="ai-row">
|
|
257
267
|
<FileText size={18} />
|
|
258
268
|
<h2 className="ai-result-title">{resultModalTitle}</h2>
|
|
259
269
|
</div>
|
|
260
270
|
<div className="ai-row">
|
|
261
|
-
<button
|
|
271
|
+
<button
|
|
272
|
+
type="button"
|
|
273
|
+
className={`ai-btn ai-btn--ghost ai-btn--compact ${sizeClass} ${radiusClass}`}
|
|
274
|
+
onClick={saveToFile}
|
|
275
|
+
>
|
|
262
276
|
<Download size={14} />
|
|
263
277
|
Sauvegarder
|
|
264
278
|
</button>
|
|
@@ -284,14 +298,18 @@ export function AiContextButton({
|
|
|
284
298
|
|
|
285
299
|
<div className="ai-result-block">
|
|
286
300
|
<h3 className="ai-result-subtitle">Résultat</h3>
|
|
287
|
-
<div className="ai-result-content">
|
|
301
|
+
<div className="ai-result-content">
|
|
302
|
+
{analysisResult.content}
|
|
303
|
+
</div>
|
|
288
304
|
</div>
|
|
289
305
|
|
|
290
306
|
<div className="ai-result-meta ai-between">
|
|
291
307
|
<span>
|
|
292
|
-
Coût: $
|
|
293
|
-
|
|
294
|
-
|
|
308
|
+
Coût: $
|
|
309
|
+
{(apiKeyId?.includes("dev")
|
|
310
|
+
? 0
|
|
311
|
+
: analysisResult.cost
|
|
312
|
+
).toFixed(6)}
|
|
295
313
|
</span>
|
|
296
314
|
<span>ID: {analysisResult.requestId?.slice(-8) || "N/A"}</span>
|
|
297
315
|
</div>
|
|
@@ -300,7 +318,10 @@ export function AiContextButton({
|
|
|
300
318
|
</div>
|
|
301
319
|
) : null}
|
|
302
320
|
|
|
303
|
-
<LBSigninModal
|
|
321
|
+
<LBSigninModal
|
|
322
|
+
isOpen={showAuthModal}
|
|
323
|
+
onClose={() => setShowAuthModal(false)}
|
|
324
|
+
/>
|
|
304
325
|
<ErrorToast key={errorKey} error={errorData} onComplete={clearError} />
|
|
305
326
|
</>
|
|
306
327
|
);
|
|
@@ -284,7 +284,9 @@ function AiPromptPanelInternal({
|
|
|
284
284
|
setSelectedModel("");
|
|
285
285
|
return;
|
|
286
286
|
}
|
|
287
|
-
const hasSelected = modelOptions.some(
|
|
287
|
+
const hasSelected = modelOptions.some(
|
|
288
|
+
(model) => model.id === selectedModel
|
|
289
|
+
);
|
|
288
290
|
if (!hasSelected) {
|
|
289
291
|
setSelectedModel(modelOptions[0].id);
|
|
290
292
|
}
|
|
@@ -441,7 +443,8 @@ function AiPromptPanelInternal({
|
|
|
441
443
|
width: "64px",
|
|
442
444
|
height: "64px",
|
|
443
445
|
borderRadius: "999px",
|
|
444
|
-
border:
|
|
446
|
+
border:
|
|
447
|
+
"2px solid color-mix(in srgb, var(--ai-primary) 20%, transparent)",
|
|
445
448
|
borderTopColor: "var(--ai-primary)",
|
|
446
449
|
animation: "ai-spin 1.1s linear infinite",
|
|
447
450
|
display: "flex",
|
|
@@ -873,12 +876,15 @@ function AiPromptPanelInternal({
|
|
|
873
876
|
transition: "all 0.2s",
|
|
874
877
|
}}
|
|
875
878
|
onMouseEnter={(e) => {
|
|
876
|
-
e.currentTarget.style.background =
|
|
877
|
-
|
|
879
|
+
e.currentTarget.style.background =
|
|
880
|
+
"color-mix(in srgb, var(--ai-primary) 10%, transparent)";
|
|
881
|
+
e.currentTarget.style.borderColor =
|
|
882
|
+
"var(--ai-primary)";
|
|
878
883
|
}}
|
|
879
884
|
onMouseLeave={(e) => {
|
|
880
885
|
e.currentTarget.style.background = "transparent";
|
|
881
|
-
e.currentTarget.style.borderColor =
|
|
886
|
+
e.currentTarget.style.borderColor =
|
|
887
|
+
"var(--ai-border)";
|
|
882
888
|
}}
|
|
883
889
|
>
|
|
884
890
|
<div
|
|
@@ -958,12 +964,15 @@ function AiPromptPanelInternal({
|
|
|
958
964
|
transition: "all 0.2s",
|
|
959
965
|
}}
|
|
960
966
|
onMouseEnter={(e) => {
|
|
961
|
-
e.currentTarget.style.background =
|
|
962
|
-
|
|
967
|
+
e.currentTarget.style.background =
|
|
968
|
+
"color-mix(in srgb, var(--ai-primary) 10%, transparent)";
|
|
969
|
+
e.currentTarget.style.borderColor =
|
|
970
|
+
"var(--ai-primary)";
|
|
963
971
|
}}
|
|
964
972
|
onMouseLeave={(e) => {
|
|
965
973
|
e.currentTarget.style.background = "transparent";
|
|
966
|
-
e.currentTarget.style.borderColor =
|
|
974
|
+
e.currentTarget.style.borderColor =
|
|
975
|
+
"var(--ai-border)";
|
|
967
976
|
}}
|
|
968
977
|
>
|
|
969
978
|
<div
|
|
@@ -1020,10 +1029,7 @@ function AiPromptPanelInternal({
|
|
|
1020
1029
|
backdropFilter: "blur(8px)",
|
|
1021
1030
|
}}
|
|
1022
1031
|
>
|
|
1023
|
-
<button
|
|
1024
|
-
onClick={handleClose}
|
|
1025
|
-
className="ai-btn ai-btn--ghost"
|
|
1026
|
-
>
|
|
1032
|
+
<button onClick={handleClose} className="ai-btn ai-btn--ghost">
|
|
1027
1033
|
Cancel
|
|
1028
1034
|
</button>
|
|
1029
1035
|
<button
|
|
@@ -1179,9 +1185,7 @@ function AiPromptPanelInternal({
|
|
|
1179
1185
|
/>
|
|
1180
1186
|
<div style={{ flex: 1 }}>
|
|
1181
1187
|
<div className="ai-model-item-title">
|
|
1182
|
-
<span>
|
|
1183
|
-
{modelData.name}
|
|
1184
|
-
</span>
|
|
1188
|
+
<span>{modelData.name}</span>
|
|
1185
1189
|
{modelData.isPro && (
|
|
1186
1190
|
<span className="ai-pill ai-pill--pro">
|
|
1187
1191
|
PRO
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type { AiStatus } from "@lastbrain/ai-ui-core";
|
|
4
|
-
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import type { AiStatus, LBUser } from "@lastbrain/ai-ui-core";
|
|
4
|
+
import { useLayoutEffect, useMemo, useRef, useState, useContext } from "react";
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
6
6
|
import {
|
|
7
7
|
ArrowRightLeft,
|
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
Settings,
|
|
15
15
|
Shield,
|
|
16
16
|
} from "lucide-react";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { LBContext, type LBApiKey, type BasicStatus, type StorageStatus } from "../context/LBAuthProvider";
|
|
18
|
+
import { AiContext } from "../context/AiProvider";
|
|
19
19
|
import { LBApiKeySelector } from "./LBApiKeySelector";
|
|
20
20
|
import { LBSigninModal } from "./LBSigninModal";
|
|
21
21
|
import type { AiRadius, AiSize } from "../types";
|
|
@@ -149,23 +149,23 @@ export function AiStatusButton({
|
|
|
149
149
|
radius = "full",
|
|
150
150
|
}: AiStatusButtonProps) {
|
|
151
151
|
let lbStatus: string | undefined;
|
|
152
|
-
let user:
|
|
152
|
+
let user: LBUser | null = null;
|
|
153
153
|
let logout: (() => Promise<void>) | undefined;
|
|
154
|
-
let apiKeys:
|
|
154
|
+
let apiKeys: LBApiKey[] = [];
|
|
155
155
|
let switchApiKey: ((apiKeyId: string) => Promise<void>) | undefined;
|
|
156
|
-
let lbApiStatus:
|
|
157
|
-
let lbBasicStatus:
|
|
158
|
-
let lbStorageStatus:
|
|
156
|
+
let lbApiStatus: AiStatus | null = null;
|
|
157
|
+
let lbBasicStatus: BasicStatus | null = null;
|
|
158
|
+
let lbStorageStatus: StorageStatus | null = null;
|
|
159
159
|
let lbIsLoadingStatus = false;
|
|
160
160
|
let lbIsLoadingStorage = false;
|
|
161
|
-
let lbSelectedKey:
|
|
161
|
+
let lbSelectedKey: LBApiKey | null = null;
|
|
162
162
|
let lbRefreshBasicStatus: (() => Promise<void>) | undefined;
|
|
163
163
|
let lbRefreshStorageStatus: ((force?: boolean) => Promise<void>) | undefined;
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
const lbContext = useContext(LBContext);
|
|
166
|
+
if (lbContext) {
|
|
167
167
|
lbStatus = lbContext.status;
|
|
168
|
-
user = lbContext.user;
|
|
168
|
+
user = lbContext.user ?? null;
|
|
169
169
|
logout = lbContext.logout;
|
|
170
170
|
apiKeys = lbContext.apiKeys || [];
|
|
171
171
|
switchApiKey = lbContext.switchApiKey;
|
|
@@ -177,14 +177,15 @@ export function AiStatusButton({
|
|
|
177
177
|
lbSelectedKey = lbContext.selectedKey || null;
|
|
178
178
|
lbRefreshBasicStatus = lbContext.refreshBasicStatus;
|
|
179
179
|
lbRefreshStorageStatus = lbContext.refreshStorageStatus;
|
|
180
|
-
}
|
|
180
|
+
} else {
|
|
181
181
|
lbStatus = undefined;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
const aiContext = useContext(AiContext);
|
|
184
185
|
let refetchProviders: (() => Promise<void>) | undefined;
|
|
185
|
-
|
|
186
|
-
refetchProviders =
|
|
187
|
-
}
|
|
186
|
+
if (aiContext) {
|
|
187
|
+
refetchProviders = aiContext.refetchProviders;
|
|
188
|
+
} else {
|
|
188
189
|
refetchProviders = undefined;
|
|
189
190
|
}
|
|
190
191
|
|
|
@@ -214,7 +215,7 @@ export function AiStatusButton({
|
|
|
214
215
|
);
|
|
215
216
|
const requiresApiKeySelection =
|
|
216
217
|
lbStatus === "ready" && !hasApiKeySelected && apiKeys.length > 0;
|
|
217
|
-
const isApiKeyAuthMode = effectiveStatus
|
|
218
|
+
const isApiKeyAuthMode = effectiveStatus && 'authType' in effectiveStatus && effectiveStatus.authType === "api_key";
|
|
218
219
|
|
|
219
220
|
const [tooltipStyle, setTooltipStyle] = useState<Record<string, string>>({});
|
|
220
221
|
|
|
@@ -453,7 +454,11 @@ export function AiStatusButton({
|
|
|
453
454
|
<span className="ai-popover-value">
|
|
454
455
|
{lbIsLoadingStatus
|
|
455
456
|
? "..."
|
|
456
|
-
: effectiveStatus
|
|
457
|
+
: (effectiveStatus &&
|
|
458
|
+
"authType" in effectiveStatus &&
|
|
459
|
+
effectiveStatus.authType) ||
|
|
460
|
+
lbStatus ||
|
|
461
|
+
"unknown"}
|
|
457
462
|
</span>
|
|
458
463
|
</div>
|
|
459
464
|
</div>
|
|
@@ -197,7 +197,9 @@ export function AiTextarea({
|
|
|
197
197
|
|
|
198
198
|
return (
|
|
199
199
|
<div className={`ai-control-group ai-glow ${className || ""}`}>
|
|
200
|
-
<div
|
|
200
|
+
<div
|
|
201
|
+
className={`ai-shell ai-shell--textarea ${sizeClass} ${radiusClass}`}
|
|
202
|
+
>
|
|
201
203
|
<textarea
|
|
202
204
|
ref={textareaRef}
|
|
203
205
|
{...textareaProps}
|
|
@@ -45,7 +45,10 @@ export function LBApiKeySelector({
|
|
|
45
45
|
|
|
46
46
|
return (
|
|
47
47
|
<div className="ai-signin-overlay" onClick={onCancel}>
|
|
48
|
-
<div
|
|
48
|
+
<div
|
|
49
|
+
className="ai-signin-panel ai-key-modal-panel"
|
|
50
|
+
onClick={(e) => e.stopPropagation()}
|
|
51
|
+
>
|
|
49
52
|
<div className="ai-signin-header">
|
|
50
53
|
<div className="ai-center mb-3">
|
|
51
54
|
<span className="ai-icon-badge">
|
|
@@ -87,7 +90,9 @@ export function LBApiKeySelector({
|
|
|
87
90
|
<div>
|
|
88
91
|
<div className="ai-model-item-title">{key.name}</div>
|
|
89
92
|
<div className="ai-model-item-meta">
|
|
90
|
-
<span>
|
|
93
|
+
<span>
|
|
94
|
+
{key.keyPrefix || key.id.substring(0, 12) + "..."}
|
|
95
|
+
</span>
|
|
91
96
|
</div>
|
|
92
97
|
</div>
|
|
93
98
|
</div>
|
|
@@ -115,7 +120,12 @@ export function LBApiKeySelector({
|
|
|
115
120
|
) : null}
|
|
116
121
|
|
|
117
122
|
<div className="ai-signin-actions">
|
|
118
|
-
<button
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={onCancel}
|
|
126
|
+
disabled={loading}
|
|
127
|
+
className="ai-btn ai-btn--ghost"
|
|
128
|
+
>
|
|
119
129
|
Annuler
|
|
120
130
|
</button>
|
|
121
131
|
<button
|
|
@@ -38,8 +38,7 @@ export function LBConnectButton({
|
|
|
38
38
|
onOpenModal?.();
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
const buttonLabel =
|
|
42
|
-
status === "ready" && user ? "Déconnexion" : label;
|
|
41
|
+
const buttonLabel = status === "ready" && user ? "Déconnexion" : label;
|
|
43
42
|
|
|
44
43
|
return (
|
|
45
44
|
<>
|
|
@@ -67,10 +66,7 @@ export function LBConnectButton({
|
|
|
67
66
|
)}
|
|
68
67
|
</button>
|
|
69
68
|
|
|
70
|
-
<LBSigninModal
|
|
71
|
-
isOpen={showModal}
|
|
72
|
-
onClose={() => setShowModal(false)}
|
|
73
|
-
/>
|
|
69
|
+
<LBSigninModal isOpen={showModal} onClose={() => setShowModal(false)} />
|
|
74
70
|
</>
|
|
75
71
|
);
|
|
76
72
|
}
|
|
@@ -84,10 +80,5 @@ interface LBAuthModalProps {
|
|
|
84
80
|
}
|
|
85
81
|
|
|
86
82
|
export function LBAuthModal({ onClose }: LBAuthModalProps) {
|
|
87
|
-
return (
|
|
88
|
-
<LBSigninModal
|
|
89
|
-
isOpen
|
|
90
|
-
onClose={() => onClose(false)}
|
|
91
|
-
/>
|
|
92
|
-
);
|
|
83
|
+
return <LBSigninModal isOpen onClose={() => onClose(false)} />;
|
|
93
84
|
}
|
|
@@ -22,11 +22,7 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
|
|
|
22
22
|
const [showKeySelector, setShowKeySelector] = useState(false);
|
|
23
23
|
const [currentApiKeys, setCurrentApiKeys] = useState<any[]>([]);
|
|
24
24
|
|
|
25
|
-
const {
|
|
26
|
-
login,
|
|
27
|
-
selectApiKeyWithToken,
|
|
28
|
-
fetchApiKeys,
|
|
29
|
-
} = lbContext || {};
|
|
25
|
+
const { login, selectApiKeyWithToken, fetchApiKeys } = lbContext || {};
|
|
30
26
|
|
|
31
27
|
const canRender = Boolean(isOpen && lbContext && login);
|
|
32
28
|
|
|
@@ -75,7 +71,9 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
|
|
|
75
71
|
setError("Erreur lors de la récupération des clés API");
|
|
76
72
|
}
|
|
77
73
|
} catch (err) {
|
|
78
|
-
setError(
|
|
74
|
+
setError(
|
|
75
|
+
err instanceof Error ? err.message : "Une erreur s'est produite"
|
|
76
|
+
);
|
|
79
77
|
} finally {
|
|
80
78
|
setLoading(false);
|
|
81
79
|
}
|
|
@@ -91,7 +89,9 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
|
|
|
91
89
|
setShowKeySelector(false);
|
|
92
90
|
onClose();
|
|
93
91
|
} catch (err) {
|
|
94
|
-
setError(
|
|
92
|
+
setError(
|
|
93
|
+
err instanceof Error ? err.message : "Erreur lors de la sélection"
|
|
94
|
+
);
|
|
95
95
|
setShowKeySelector(false);
|
|
96
96
|
}
|
|
97
97
|
};
|
|
@@ -151,7 +151,10 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
|
|
|
151
151
|
<div className="ai-signin-content">
|
|
152
152
|
<form onSubmit={handleSubmit}>
|
|
153
153
|
<div className="ai-input-row">
|
|
154
|
-
<label
|
|
154
|
+
<label
|
|
155
|
+
htmlFor="lb-signin-email"
|
|
156
|
+
className="ai-input-label ai-row"
|
|
157
|
+
>
|
|
155
158
|
<Mail size={14} className="ai-inline-icon" />
|
|
156
159
|
Email
|
|
157
160
|
</label>
|
|
@@ -173,7 +176,10 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
|
|
|
173
176
|
</div>
|
|
174
177
|
|
|
175
178
|
<div className="ai-input-row">
|
|
176
|
-
<label
|
|
179
|
+
<label
|
|
180
|
+
htmlFor="lb-signin-password"
|
|
181
|
+
className="ai-input-label ai-row"
|
|
182
|
+
>
|
|
177
183
|
<Lock size={14} className="ai-inline-icon" />
|
|
178
184
|
Mot de passe
|
|
179
185
|
</label>
|
|
@@ -201,7 +207,11 @@ export function LBSigninModal({ isOpen, onClose }: LBSigninModalProps) {
|
|
|
201
207
|
) : null}
|
|
202
208
|
|
|
203
209
|
<div className="ai-signin-actions">
|
|
204
|
-
<button
|
|
210
|
+
<button
|
|
211
|
+
type="submit"
|
|
212
|
+
className="ai-btn ai-btn--auth"
|
|
213
|
+
disabled={loading}
|
|
214
|
+
>
|
|
205
215
|
{loading ? (
|
|
206
216
|
<>
|
|
207
217
|
<Loader2 size={16} className="ai-spinner" />
|
|
@@ -24,6 +24,39 @@ import type {
|
|
|
24
24
|
} from "@lastbrain/ai-ui-core";
|
|
25
25
|
import { createLBClient } from "@lastbrain/ai-ui-core";
|
|
26
26
|
|
|
27
|
+
export interface ApiKeyUser {
|
|
28
|
+
id?: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
prefix?: string;
|
|
31
|
+
env?: string;
|
|
32
|
+
rate_limit_rpm?: number;
|
|
33
|
+
scopes?: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BasicStatus {
|
|
37
|
+
authType?: string;
|
|
38
|
+
user?: {
|
|
39
|
+
id?: string;
|
|
40
|
+
email?: string;
|
|
41
|
+
} | null;
|
|
42
|
+
apiKey?: ApiKeyUser | null;
|
|
43
|
+
api_key?: ApiKeyUser | null;
|
|
44
|
+
balance?: {
|
|
45
|
+
used?: number;
|
|
46
|
+
total?: number;
|
|
47
|
+
percentage?: number;
|
|
48
|
+
};
|
|
49
|
+
storage?: StorageStatus["storage"];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface StorageStatus {
|
|
53
|
+
storage?: {
|
|
54
|
+
used_mb?: number;
|
|
55
|
+
allocated_mb?: number;
|
|
56
|
+
percentage?: number;
|
|
57
|
+
} | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
interface LBProviderProps {
|
|
28
61
|
children: ReactNode;
|
|
29
62
|
/** URL de l'API LastBrain (ex: https://api.lastbrain.io) */
|
|
@@ -66,9 +99,9 @@ interface LBContextValue extends LBAuthState {
|
|
|
66
99
|
/** Status API (balance, storage, API key info) */
|
|
67
100
|
apiStatus: AiStatus | null;
|
|
68
101
|
/** Status basique (rapide) - balance, API key info sans storage */
|
|
69
|
-
basicStatus:
|
|
102
|
+
basicStatus: BasicStatus | null;
|
|
70
103
|
/** Status storage (lent) avec cache */
|
|
71
|
-
storageStatus:
|
|
104
|
+
storageStatus: StorageStatus | null;
|
|
72
105
|
/** Fonction pour rafraîchir le status rapide */
|
|
73
106
|
refreshBasicStatus: () => Promise<void>;
|
|
74
107
|
/** Fonction pour rafraîchir le storage (avec cache optionnel) */
|
|
@@ -83,6 +116,9 @@ interface LBContextValue extends LBAuthState {
|
|
|
83
116
|
|
|
84
117
|
const LBContext = createContext<LBContextValue | undefined>(undefined);
|
|
85
118
|
|
|
119
|
+
// Export pour usage dans d'autres composants
|
|
120
|
+
export { LBContext };
|
|
121
|
+
|
|
86
122
|
export function LBProvider({
|
|
87
123
|
children,
|
|
88
124
|
baseUrl: _baseUrl = "/api/lastbrain",
|
|
@@ -96,8 +132,8 @@ export function LBProvider({
|
|
|
96
132
|
const [apiKeys, setApiKeys] = useState<LBApiKey[]>([]);
|
|
97
133
|
const [accessToken, setAccessToken] = useState<string>();
|
|
98
134
|
const [apiStatus, setApiStatus] = useState<AiStatus | null>(null);
|
|
99
|
-
const [basicStatus, setBasicStatus] = useState<
|
|
100
|
-
const [storageStatus, setStorageStatus] = useState<
|
|
135
|
+
const [basicStatus, setBasicStatus] = useState<BasicStatus | null>(null);
|
|
136
|
+
const [storageStatus, setStorageStatus] = useState<StorageStatus | null>(null);
|
|
101
137
|
const [isLoadingStatus, setIsLoadingStatus] = useState(false);
|
|
102
138
|
const [isLoadingStorage, setIsLoadingStorage] = useState(false);
|
|
103
139
|
const [storageLastFetch, setStorageLastFetch] = useState<number>(0);
|
|
@@ -373,7 +409,7 @@ export function LBProvider({
|
|
|
373
409
|
|
|
374
410
|
setIsLoadingStatus(true);
|
|
375
411
|
try {
|
|
376
|
-
let data:
|
|
412
|
+
let data: BasicStatus;
|
|
377
413
|
try {
|
|
378
414
|
data = await lbClient.getStatus();
|
|
379
415
|
} catch {
|
|
@@ -452,7 +488,7 @@ export function LBProvider({
|
|
|
452
488
|
...basicStatus,
|
|
453
489
|
storage: storageData?.storage,
|
|
454
490
|
};
|
|
455
|
-
setApiStatus(combinedStatus);
|
|
491
|
+
setApiStatus(combinedStatus as AiStatus);
|
|
456
492
|
} catch (error) {
|
|
457
493
|
console.error("[LBProvider] Failed to fetch storage status:", error);
|
|
458
494
|
// Arrêter les tentatives répétées si erreur persistante
|
|
@@ -619,3 +655,6 @@ export function useLB(): LBContextValue {
|
|
|
619
655
|
}
|
|
620
656
|
return context;
|
|
621
657
|
}
|
|
658
|
+
|
|
659
|
+
// Re-export des types pour usage externe
|
|
660
|
+
export type { LBApiKey };
|
|
@@ -56,7 +56,10 @@ function ShowcasePanel({
|
|
|
56
56
|
<Sparkles size={14} className="text-[var(--ai-primary)]" />
|
|
57
57
|
</div>
|
|
58
58
|
<div className="mt-3 space-y-2">
|
|
59
|
-
<input
|
|
59
|
+
<input
|
|
60
|
+
className="ai-control ai-control-input"
|
|
61
|
+
placeholder="Default state"
|
|
62
|
+
/>
|
|
60
63
|
<input
|
|
61
64
|
className="ai-control ai-control-input ai-control--focused"
|
|
62
65
|
placeholder="Focused style"
|
package/src/hooks/useAiModels.ts
CHANGED
|
@@ -26,7 +26,8 @@ export function useAiModels(options?: UseAiModelsOptions): UseAiModelsResult {
|
|
|
26
26
|
|
|
27
27
|
// Utiliser le contexte dès que la base URL correspond.
|
|
28
28
|
// L'apiKeyId peut varier (session/api-key sélectionnée) sans invalider le contexte.
|
|
29
|
-
const useContextData =
|
|
29
|
+
const useContextData =
|
|
30
|
+
!options?.baseUrl || options.baseUrl === context.baseUrl;
|
|
30
31
|
|
|
31
32
|
// Filtrer les modèles selon le type demandé
|
|
32
33
|
const filteredModels = useMemo(() => {
|
|
@@ -44,7 +44,8 @@ export function useModelManagement(
|
|
|
44
44
|
// Utiliser les données du contexte si la base URL correspond.
|
|
45
45
|
// L'apiKey peut changer selon le mode d'auth (lb_session/supabase/api_key) sans
|
|
46
46
|
// devoir casser le cache/provider de modèles déjà présent.
|
|
47
|
-
const useContextData =
|
|
47
|
+
const useContextData =
|
|
48
|
+
!options.baseUrl || options.baseUrl === context.baseUrl;
|
|
48
49
|
|
|
49
50
|
// Filtrer par catégorie si nécessaire
|
|
50
51
|
const filteredModels = useMemo(() => {
|
|
@@ -8,12 +8,25 @@ export interface ParsedError {
|
|
|
8
8
|
isUserFriendly: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
type NormalizedErrorLike = {
|
|
12
|
+
code: string;
|
|
13
|
+
message: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function isNormalizedErrorLike(value: unknown): value is NormalizedErrorLike {
|
|
17
|
+
if (!value || typeof value !== "object") {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const v = value as Record<string, unknown>;
|
|
21
|
+
return typeof v.code === "string" && typeof v.message === "string";
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
/**
|
|
12
25
|
* Parse et uniformise la gestion des erreurs des composants AI
|
|
13
26
|
*/
|
|
14
|
-
export function parseAIError(error:
|
|
27
|
+
export function parseAIError(error: unknown): ParsedError {
|
|
15
28
|
// Si l'erreur est déjà un objet normalisé
|
|
16
|
-
if (error
|
|
29
|
+
if (isNormalizedErrorLike(error)) {
|
|
17
30
|
return {
|
|
18
31
|
message: getUserFriendlyMessage(error.code, error.message),
|
|
19
32
|
code: error.code,
|
|
@@ -160,7 +173,7 @@ export interface ErrorToastCallback {
|
|
|
160
173
|
* @param showInternalToast Callback interne optionnelle pour afficher un toast dans le composant
|
|
161
174
|
*/
|
|
162
175
|
export function handleAIError(
|
|
163
|
-
error:
|
|
176
|
+
error: unknown,
|
|
164
177
|
onToast?: ErrorToastCallback,
|
|
165
178
|
showInternalToast?: (error: { message: string; code?: string }) => void
|
|
166
179
|
): void {
|