@lastbrain/ai-ui-react 1.0.37 → 1.0.39
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/AiPromptPanel.js +1 -6
- package/dist/components/ErrorToast.js +14 -14
- package/dist/components/LBConnectButton.d.ts +20 -0
- package/dist/components/LBConnectButton.d.ts.map +1 -0
- package/dist/components/LBConnectButton.js +87 -0
- package/dist/components/LBKeyPicker.d.ts +10 -0
- package/dist/components/LBKeyPicker.d.ts.map +1 -0
- package/dist/components/LBKeyPicker.js +61 -0
- package/dist/context/LBAuthProvider.d.ts +38 -0
- package/dist/context/LBAuthProvider.d.ts.map +1 -0
- package/dist/context/LBAuthProvider.js +194 -0
- package/dist/hooks/useAiModels.d.ts.map +1 -1
- package/dist/hooks/useAiModels.js +1 -2
- package/dist/hooks/useLB.d.ts +5 -0
- package/dist/hooks/useLB.d.ts.map +1 -0
- package/dist/hooks/useLB.js +5 -0
- package/dist/hooks/usePrompts.d.ts.map +1 -1
- package/dist/hooks/usePrompts.js +12 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/package.json +2 -2
- package/src/components/AiPromptPanel.tsx +5 -6
- package/src/components/ErrorToast.tsx +16 -16
- package/src/components/LBConnectButton.tsx +230 -0
- package/src/components/LBKeyPicker.tsx +145 -0
- package/src/context/LBAuthProvider.tsx +269 -0
- package/src/hooks/useAiModels.ts +1 -2
- package/src/hooks/useLB.ts +7 -0
- package/src/hooks/usePrompts.ts +13 -3
- package/src/index.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lastbrain/ai-ui-react",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.39",
|
|
4
4
|
"description": "Headless React components for LastBrain AI UI Kit",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"lucide-react": "^0.257.0",
|
|
51
|
-
"@lastbrain/ai-ui-core": "1.0.
|
|
51
|
+
"@lastbrain/ai-ui-core": "1.0.29"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/react": "^19.2.0",
|
|
@@ -424,7 +424,7 @@ function AiPromptPanelInternal({
|
|
|
424
424
|
position: "sticky",
|
|
425
425
|
top: 0,
|
|
426
426
|
zIndex: 5,
|
|
427
|
-
|
|
427
|
+
|
|
428
428
|
borderBottom: "1px solid var(--ai-border-primary, #374151)",
|
|
429
429
|
backdropFilter: "blur(8px)",
|
|
430
430
|
}}
|
|
@@ -1003,7 +1003,7 @@ function AiPromptPanelInternal({
|
|
|
1003
1003
|
position: "sticky",
|
|
1004
1004
|
bottom: 0,
|
|
1005
1005
|
zIndex: 5,
|
|
1006
|
-
|
|
1006
|
+
|
|
1007
1007
|
borderTop: "1px solid var(--ai-border-primary, #374151)",
|
|
1008
1008
|
backdropFilter: "blur(8px)",
|
|
1009
1009
|
}}
|
|
@@ -1066,7 +1066,7 @@ function AiPromptPanelInternal({
|
|
|
1066
1066
|
overflow: "hidden",
|
|
1067
1067
|
display: "flex",
|
|
1068
1068
|
flexDirection: "column",
|
|
1069
|
-
|
|
1069
|
+
|
|
1070
1070
|
boxShadow: "0 20px 40px rgba(0, 0, 0, 0.5)",
|
|
1071
1071
|
}}
|
|
1072
1072
|
>
|
|
@@ -1250,9 +1250,8 @@ function AiPromptPanelInternal({
|
|
|
1250
1250
|
style={{
|
|
1251
1251
|
fontWeight: "600",
|
|
1252
1252
|
fontSize: "15px",
|
|
1253
|
-
color:
|
|
1254
|
-
|
|
1255
|
-
: "var(--ai-text-primary, #f3f4f6)",
|
|
1253
|
+
color: "var(--ai-text-secondary, #6b7280)",
|
|
1254
|
+
|
|
1256
1255
|
letterSpacing: "-0.01em",
|
|
1257
1256
|
}}
|
|
1258
1257
|
>
|
|
@@ -24,6 +24,21 @@ export function ErrorToast({
|
|
|
24
24
|
const fadeTimeoutRef = useRef<number | null>(null);
|
|
25
25
|
const autoCloseTimeoutRef = useRef<number | null>(null);
|
|
26
26
|
|
|
27
|
+
const handleClose = () => {
|
|
28
|
+
if (isClosing) return;
|
|
29
|
+
|
|
30
|
+
// Clear auto-close timeout if user closes manually
|
|
31
|
+
if (autoCloseTimeoutRef.current) {
|
|
32
|
+
window.clearTimeout(autoCloseTimeoutRef.current);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setIsClosing(true);
|
|
36
|
+
fadeTimeoutRef.current = window.setTimeout(() => {
|
|
37
|
+
setIsVisible(false);
|
|
38
|
+
onComplete?.();
|
|
39
|
+
}, 200);
|
|
40
|
+
};
|
|
41
|
+
|
|
27
42
|
useEffect(() => {
|
|
28
43
|
if (error) {
|
|
29
44
|
// Show toast immediately
|
|
@@ -46,22 +61,7 @@ export function ErrorToast({
|
|
|
46
61
|
window.clearTimeout(autoCloseTimeoutRef.current);
|
|
47
62
|
}
|
|
48
63
|
};
|
|
49
|
-
}, [error]);
|
|
50
|
-
|
|
51
|
-
const handleClose = () => {
|
|
52
|
-
if (isClosing) return;
|
|
53
|
-
|
|
54
|
-
// Clear auto-close timeout if user closes manually
|
|
55
|
-
if (autoCloseTimeoutRef.current) {
|
|
56
|
-
window.clearTimeout(autoCloseTimeoutRef.current);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
setIsClosing(true);
|
|
60
|
-
fadeTimeoutRef.current = window.setTimeout(() => {
|
|
61
|
-
setIsVisible(false);
|
|
62
|
-
onComplete?.();
|
|
63
|
-
}, 200);
|
|
64
|
-
};
|
|
64
|
+
}, [error, handleClose]);
|
|
65
65
|
|
|
66
66
|
if (!error) return null;
|
|
67
67
|
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bouton de connexion LastBrain
|
|
5
|
+
* Ouvre la modal d'authentification
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { useLB } from "../context/LBAuthProvider";
|
|
10
|
+
|
|
11
|
+
interface LBConnectButtonProps {
|
|
12
|
+
/** Texte du bouton */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Classe CSS personnalisée */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** Callback après connexion réussie */
|
|
17
|
+
onConnected?: () => void;
|
|
18
|
+
/** Callback à l'ouverture de la modal */
|
|
19
|
+
onOpenModal?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function LBConnectButton({
|
|
23
|
+
label = "Se connecter à LastBrain",
|
|
24
|
+
className = "",
|
|
25
|
+
onConnected,
|
|
26
|
+
onOpenModal,
|
|
27
|
+
}: LBConnectButtonProps) {
|
|
28
|
+
const { status, user, logout } = useLB();
|
|
29
|
+
const [showModal, setShowModal] = React.useState(false);
|
|
30
|
+
|
|
31
|
+
const handleClick = () => {
|
|
32
|
+
if (status === "ready" && user) {
|
|
33
|
+
// Déjà connecté, proposer de se déconnecter
|
|
34
|
+
logout();
|
|
35
|
+
} else {
|
|
36
|
+
// Pas connecté, ouvrir la modal
|
|
37
|
+
setShowModal(true);
|
|
38
|
+
onOpenModal?.();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleModalClose = (success: boolean) => {
|
|
43
|
+
setShowModal(false);
|
|
44
|
+
if (success) {
|
|
45
|
+
onConnected?.();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<button
|
|
52
|
+
onClick={handleClick}
|
|
53
|
+
className={
|
|
54
|
+
className ||
|
|
55
|
+
"px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
56
|
+
}
|
|
57
|
+
disabled={status === "loading"}
|
|
58
|
+
>
|
|
59
|
+
{status === "loading"
|
|
60
|
+
? "Chargement..."
|
|
61
|
+
: status === "ready" && user
|
|
62
|
+
? `Connecté (${user.email})`
|
|
63
|
+
: label}
|
|
64
|
+
</button>
|
|
65
|
+
|
|
66
|
+
{showModal && <LBAuthModal onClose={handleModalClose} />}
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Modal d'authentification LastBrain
|
|
73
|
+
*/
|
|
74
|
+
interface LBAuthModalProps {
|
|
75
|
+
onClose: (success: boolean) => void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function LBAuthModal({ onClose }: LBAuthModalProps) {
|
|
79
|
+
const { login, fetchApiKeys, selectApiKey, status } = useLB();
|
|
80
|
+
const [step, setStep] = React.useState<"login" | "select-key">("login");
|
|
81
|
+
const [email, setEmail] = React.useState("");
|
|
82
|
+
const [password, setPassword] = React.useState("");
|
|
83
|
+
const [error, setError] = React.useState("");
|
|
84
|
+
const [loading, setLoading] = React.useState(false);
|
|
85
|
+
const [accessToken, setAccessToken] = React.useState("");
|
|
86
|
+
const [apiKeys, setApiKeys] = React.useState<any[]>([]);
|
|
87
|
+
|
|
88
|
+
const handleLogin = async (e: React.FormEvent) => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
setError("");
|
|
91
|
+
setLoading(true);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const result = await login(email, password);
|
|
95
|
+
setAccessToken(result.accessToken);
|
|
96
|
+
|
|
97
|
+
// Récupérer les clés API
|
|
98
|
+
const keys = await fetchApiKeys(result.accessToken);
|
|
99
|
+
setApiKeys(keys);
|
|
100
|
+
|
|
101
|
+
if (keys.length === 0) {
|
|
102
|
+
setError(
|
|
103
|
+
"Aucune clé API active trouvée. Créez-en une dans votre dashboard LastBrain."
|
|
104
|
+
);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Passer à l'étape de sélection
|
|
109
|
+
setStep("select-key");
|
|
110
|
+
} catch (err) {
|
|
111
|
+
setError(err instanceof Error ? err.message : "Échec de la connexion");
|
|
112
|
+
} finally {
|
|
113
|
+
setLoading(false);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handleSelectKey = async (apiKeyId: string) => {
|
|
118
|
+
setError("");
|
|
119
|
+
setLoading(true);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await selectApiKey(accessToken, apiKeyId);
|
|
123
|
+
onClose(true); // Succès
|
|
124
|
+
} catch (err) {
|
|
125
|
+
setError(err instanceof Error ? err.message : "Échec de la sélection");
|
|
126
|
+
} finally {
|
|
127
|
+
setLoading(false);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
133
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
|
|
134
|
+
<div className="flex justify-between items-center mb-4">
|
|
135
|
+
<h2 className="text-xl font-bold">
|
|
136
|
+
{step === "login"
|
|
137
|
+
? "Connexion LastBrain"
|
|
138
|
+
: "Sélectionner une clé API"}
|
|
139
|
+
</h2>
|
|
140
|
+
<button
|
|
141
|
+
onClick={() => onClose(false)}
|
|
142
|
+
className="text-gray-500 hover:text-gray-700"
|
|
143
|
+
>
|
|
144
|
+
✕
|
|
145
|
+
</button>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{step === "login" ? (
|
|
149
|
+
<form onSubmit={handleLogin} className="space-y-4">
|
|
150
|
+
<div>
|
|
151
|
+
<label className="block text-sm font-medium mb-1">Email</label>
|
|
152
|
+
<input
|
|
153
|
+
type="email"
|
|
154
|
+
value={email}
|
|
155
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
156
|
+
className="w-full px-3 py-2 border rounded dark:bg-gray-700"
|
|
157
|
+
required
|
|
158
|
+
autoFocus
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div>
|
|
163
|
+
<label className="block text-sm font-medium mb-1">
|
|
164
|
+
Mot de passe
|
|
165
|
+
</label>
|
|
166
|
+
<input
|
|
167
|
+
type="password"
|
|
168
|
+
value={password}
|
|
169
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
170
|
+
className="w-full px-3 py-2 border rounded dark:bg-gray-700"
|
|
171
|
+
required
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{error && <div className="text-red-600 text-sm">{error}</div>}
|
|
176
|
+
|
|
177
|
+
<button
|
|
178
|
+
type="submit"
|
|
179
|
+
disabled={loading}
|
|
180
|
+
className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
|
181
|
+
>
|
|
182
|
+
{loading ? "Connexion..." : "Se connecter"}
|
|
183
|
+
</button>
|
|
184
|
+
|
|
185
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 text-center">
|
|
186
|
+
Pas encore de compte ?{" "}
|
|
187
|
+
<a
|
|
188
|
+
href="https://lastbrain.io/signup"
|
|
189
|
+
target="_blank"
|
|
190
|
+
rel="noopener noreferrer"
|
|
191
|
+
className="text-blue-600 hover:underline"
|
|
192
|
+
>
|
|
193
|
+
Créer un compte
|
|
194
|
+
</a>
|
|
195
|
+
</p>
|
|
196
|
+
</form>
|
|
197
|
+
) : (
|
|
198
|
+
<div className="space-y-4">
|
|
199
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
200
|
+
Sélectionnez une clé API pour créer une session de 72h
|
|
201
|
+
</p>
|
|
202
|
+
|
|
203
|
+
<div className="space-y-2">
|
|
204
|
+
{apiKeys.map((key) => (
|
|
205
|
+
<button
|
|
206
|
+
key={key.id}
|
|
207
|
+
onClick={() => handleSelectKey(key.id)}
|
|
208
|
+
disabled={!key.isActive || loading}
|
|
209
|
+
className="w-full text-left px-4 py-3 border rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
210
|
+
>
|
|
211
|
+
<div className="font-medium">{key.name}</div>
|
|
212
|
+
<div className="text-sm text-gray-500">
|
|
213
|
+
{key.keyPrefix}...
|
|
214
|
+
</div>
|
|
215
|
+
{!key.isActive && (
|
|
216
|
+
<div className="text-xs text-red-600">Inactive</div>
|
|
217
|
+
)}
|
|
218
|
+
</button>
|
|
219
|
+
))}
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
{error && <div className="text-red-600 text-sm">{error}</div>}
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export { LBAuthModal };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Composant LBKeyPicker
|
|
5
|
+
* Permet de changer de clé API sans se reconnecter
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
|
+
import { useLB } from "../hooks/useLB";
|
|
10
|
+
import type { LBApiKey } from "@lastbrain/ai-ui-core";
|
|
11
|
+
|
|
12
|
+
interface LBKeyPickerProps {
|
|
13
|
+
/** Classe CSS personnalisée */
|
|
14
|
+
className?: string;
|
|
15
|
+
/** Callback après changement de clé */
|
|
16
|
+
onKeyChanged?: (key: LBApiKey) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function LBKeyPicker({
|
|
20
|
+
className = "",
|
|
21
|
+
onKeyChanged,
|
|
22
|
+
}: LBKeyPickerProps) {
|
|
23
|
+
const { status, selectedKey, fetchApiKeys, selectApiKey, accessToken } =
|
|
24
|
+
useLB();
|
|
25
|
+
const [apiKeys, setApiKeys] = useState<LBApiKey[]>([]);
|
|
26
|
+
const [loading, setLoading] = useState(false);
|
|
27
|
+
const [error, setError] = useState("");
|
|
28
|
+
const [showDropdown, setShowDropdown] = useState(false);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (status === "ready" && accessToken) {
|
|
32
|
+
loadKeys();
|
|
33
|
+
}
|
|
34
|
+
}, [status, accessToken]);
|
|
35
|
+
|
|
36
|
+
const loadKeys = async () => {
|
|
37
|
+
if (!accessToken) return;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
setLoading(true);
|
|
41
|
+
const keys = await fetchApiKeys(accessToken);
|
|
42
|
+
setApiKeys(keys);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
setError("Impossible de charger les clés API");
|
|
45
|
+
} finally {
|
|
46
|
+
setLoading(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleSelectKey = async (keyId: string) => {
|
|
51
|
+
if (!accessToken) return;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
setLoading(true);
|
|
55
|
+
setError("");
|
|
56
|
+
await selectApiKey(accessToken, keyId);
|
|
57
|
+
const selectedApiKey = apiKeys.find((k) => k.id === keyId);
|
|
58
|
+
if (selectedApiKey) {
|
|
59
|
+
onKeyChanged?.(selectedApiKey);
|
|
60
|
+
}
|
|
61
|
+
setShowDropdown(false);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
setError(
|
|
64
|
+
err instanceof Error ? err.message : "Échec du changement de clé"
|
|
65
|
+
);
|
|
66
|
+
} finally {
|
|
67
|
+
setLoading(false);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (status !== "ready" || !selectedKey) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className={`relative ${className}`}>
|
|
77
|
+
<button
|
|
78
|
+
onClick={() => setShowDropdown(!showDropdown)}
|
|
79
|
+
disabled={loading}
|
|
80
|
+
className="px-4 py-2 border rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 flex items-center gap-2"
|
|
81
|
+
>
|
|
82
|
+
<span className="font-mono text-sm">{selectedKey.keyPrefix}...</span>
|
|
83
|
+
<svg
|
|
84
|
+
className="w-4 h-4"
|
|
85
|
+
fill="none"
|
|
86
|
+
stroke="currentColor"
|
|
87
|
+
viewBox="0 0 24 24"
|
|
88
|
+
>
|
|
89
|
+
<path
|
|
90
|
+
strokeLinecap="round"
|
|
91
|
+
strokeLinejoin="round"
|
|
92
|
+
strokeWidth={2}
|
|
93
|
+
d="M19 9l-7 7-7-7"
|
|
94
|
+
/>
|
|
95
|
+
</svg>
|
|
96
|
+
</button>
|
|
97
|
+
|
|
98
|
+
{showDropdown && (
|
|
99
|
+
<>
|
|
100
|
+
<div
|
|
101
|
+
className="fixed inset-0 z-10"
|
|
102
|
+
onClick={() => setShowDropdown(false)}
|
|
103
|
+
/>
|
|
104
|
+
<div className="absolute right-0 mt-2 w-64 bg-white dark:bg-gray-800 border rounded-lg shadow-lg z-20">
|
|
105
|
+
<div className="p-2">
|
|
106
|
+
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 px-3 py-2">
|
|
107
|
+
Changer de clé API
|
|
108
|
+
</div>
|
|
109
|
+
<div className="space-y-1">
|
|
110
|
+
{apiKeys.map((key) => (
|
|
111
|
+
<button
|
|
112
|
+
key={key.id}
|
|
113
|
+
onClick={() => handleSelectKey(key.id)}
|
|
114
|
+
disabled={
|
|
115
|
+
!key.isActive || loading || key.id === selectedKey.id
|
|
116
|
+
}
|
|
117
|
+
className={`w-full text-left px-3 py-2 rounded text-sm ${
|
|
118
|
+
key.id === selectedKey.id
|
|
119
|
+
? "bg-blue-100 dark:bg-blue-900"
|
|
120
|
+
: "hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
121
|
+
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
122
|
+
>
|
|
123
|
+
<div className="font-medium">{key.name}</div>
|
|
124
|
+
<div className="text-xs text-gray-500 font-mono">
|
|
125
|
+
{key.keyPrefix}...
|
|
126
|
+
</div>
|
|
127
|
+
{!key.isActive && (
|
|
128
|
+
<div className="text-xs text-red-600">Inactive</div>
|
|
129
|
+
)}
|
|
130
|
+
</button>
|
|
131
|
+
))}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{error && (
|
|
136
|
+
<div className="px-3 py-2 text-xs text-red-600 border-t">
|
|
137
|
+
{error}
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|