@lastbrain/ai-ui-react 1.0.24 → 1.0.26
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 +12 -0
- package/dist/components/AiChipLabel.d.ts.map +1 -1
- package/dist/components/AiChipLabel.js +129 -1
- package/dist/components/AiContextButton.d.ts +18 -0
- package/dist/components/AiContextButton.d.ts.map +1 -0
- package/dist/components/AiContextButton.js +339 -0
- package/dist/components/AiImageButton.d.ts +12 -3
- package/dist/components/AiImageButton.d.ts.map +1 -1
- package/dist/components/AiImageButton.js +218 -8
- package/dist/components/AiStatusButton.d.ts.map +1 -1
- package/dist/components/AiStatusButton.js +1 -1
- package/dist/components/UsageToast.d.ts.map +1 -1
- package/dist/components/UsageToast.js +5 -3
- package/dist/examples/AiChipInputExample.d.ts +2 -0
- package/dist/examples/AiChipInputExample.d.ts.map +1 -0
- package/dist/examples/AiChipInputExample.js +14 -0
- package/dist/examples/AiContextButtonExample.d.ts +2 -0
- package/dist/examples/AiContextButtonExample.d.ts.map +1 -0
- package/dist/examples/AiContextButtonExample.js +88 -0
- package/dist/examples/AiImageButtonExample.d.ts +2 -0
- package/dist/examples/AiImageButtonExample.d.ts.map +1 -0
- package/dist/examples/AiImageButtonExample.js +26 -0
- package/dist/hooks/useAiCallImage.d.ts.map +1 -1
- package/dist/hooks/useAiCallImage.js +107 -1
- package/dist/hooks/useAiCallText.d.ts.map +1 -1
- package/dist/hooks/useAiCallText.js +25 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/styles/inline.d.ts.map +1 -1
- package/dist/styles/inline.js +3 -1
- package/package.json +2 -2
- package/src/components/AiChipLabel.tsx +218 -1
- package/src/components/AiContextButton.tsx +553 -0
- package/src/components/AiImageButton.tsx +386 -38
- package/src/components/AiStatusButton.tsx +7 -3
- package/src/components/UsageToast.tsx +5 -3
- package/src/examples/AiChipInputExample.tsx +81 -0
- package/src/examples/AiContextButtonExample.tsx +338 -0
- package/src/examples/AiImageButtonExample.tsx +72 -0
- package/src/hooks/useAiCallImage.ts +149 -1
- package/src/hooks/useAiCallText.ts +30 -1
- package/src/index.ts +4 -0
- package/src/styles/inline.ts +3 -1
|
@@ -1,37 +1,72 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState, type ButtonHTMLAttributes } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ImageIcon,
|
|
6
|
+
Loader2,
|
|
7
|
+
Download,
|
|
8
|
+
Copy,
|
|
9
|
+
ExternalLink,
|
|
10
|
+
X,
|
|
11
|
+
} from "lucide-react";
|
|
4
12
|
import type { BaseAiProps } from "../types";
|
|
5
13
|
import { useAiCallImage } from "../hooks/useAiCallImage";
|
|
6
14
|
import { useAiModels } from "../hooks/useAiModels";
|
|
7
15
|
import { AiPromptPanel } from "./AiPromptPanel";
|
|
8
|
-
import {
|
|
16
|
+
import { useUsageToast } from "./UsageToast";
|
|
17
|
+
import { aiStyles } from "../styles/inline";
|
|
18
|
+
import { useAiContext } from "../context/AiProvider";
|
|
9
19
|
|
|
10
20
|
export interface AiImageButtonProps
|
|
11
21
|
extends
|
|
12
22
|
Omit<BaseAiProps, "onValue" | "type">,
|
|
13
|
-
ButtonHTMLAttributes<HTMLButtonElement> {
|
|
14
|
-
onImage?: (
|
|
23
|
+
Omit<ButtonHTMLAttributes<HTMLButtonElement>, "baseUrl" | "apiKeyId"> {
|
|
24
|
+
onImage?: (
|
|
25
|
+
imageUrl: string,
|
|
26
|
+
metadata?: { requestId: string; tokens: number }
|
|
27
|
+
) => void;
|
|
15
28
|
uiMode?: "modal" | "drawer";
|
|
29
|
+
showImageCard?: boolean;
|
|
30
|
+
onImageSave?: (url: string) => Promise<void>;
|
|
31
|
+
storeOutputs?: boolean; // For external API calls - whether to store outputs
|
|
32
|
+
artifactTitle?: string; // Title for stored artifacts
|
|
33
|
+
// Props optionnelles pour override du contexte
|
|
34
|
+
baseUrl?: string;
|
|
35
|
+
apiKeyId?: string;
|
|
16
36
|
}
|
|
17
37
|
|
|
18
38
|
export function AiImageButton({
|
|
19
|
-
baseUrl,
|
|
20
|
-
apiKeyId,
|
|
39
|
+
baseUrl: propBaseUrl,
|
|
40
|
+
apiKeyId: propApiKeyId,
|
|
21
41
|
uiMode = "modal",
|
|
22
|
-
context,
|
|
23
|
-
model,
|
|
24
|
-
prompt,
|
|
42
|
+
context: _context,
|
|
43
|
+
model: _model,
|
|
44
|
+
prompt: _prompt,
|
|
25
45
|
onImage,
|
|
26
46
|
onToast,
|
|
27
47
|
disabled,
|
|
28
48
|
className,
|
|
29
49
|
children,
|
|
50
|
+
showImageCard = true,
|
|
51
|
+
onImageSave,
|
|
52
|
+
storeOutputs,
|
|
53
|
+
artifactTitle,
|
|
30
54
|
...buttonProps
|
|
31
55
|
}: AiImageButtonProps) {
|
|
32
56
|
const [isOpen, setIsOpen] = useState(false);
|
|
57
|
+
const [generatedImage, setGeneratedImage] = useState<{
|
|
58
|
+
url: string;
|
|
59
|
+
prompt: string;
|
|
60
|
+
requestId: string;
|
|
61
|
+
tokens: number;
|
|
62
|
+
} | null>(null);
|
|
33
63
|
const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
|
|
34
64
|
|
|
65
|
+
// Récupérer le contexte AiProvider avec fallback sur les props
|
|
66
|
+
const aiContext = useAiContext();
|
|
67
|
+
const baseUrl = propBaseUrl ?? aiContext.baseUrl;
|
|
68
|
+
const apiKeyId = propApiKeyId ?? aiContext.apiKeyId;
|
|
69
|
+
|
|
35
70
|
const { models } = useAiModels({ baseUrl, apiKeyId });
|
|
36
71
|
const { generateImage, loading } = useAiCallImage({ baseUrl, apiKeyId });
|
|
37
72
|
|
|
@@ -43,6 +78,81 @@ export function AiImageButton({
|
|
|
43
78
|
setIsOpen(false);
|
|
44
79
|
};
|
|
45
80
|
|
|
81
|
+
// Actions sur l'image
|
|
82
|
+
const handleDownload = () => {
|
|
83
|
+
if (!generatedImage) return;
|
|
84
|
+
const link = document.createElement("a");
|
|
85
|
+
link.href = generatedImage.url;
|
|
86
|
+
link.download = `ai-image-${generatedImage.requestId}.png`;
|
|
87
|
+
link.click();
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleCopyUrl = async () => {
|
|
91
|
+
if (!generatedImage) return;
|
|
92
|
+
try {
|
|
93
|
+
await navigator.clipboard.writeText(generatedImage.url);
|
|
94
|
+
onToast?.({
|
|
95
|
+
type: "success",
|
|
96
|
+
message: "URL copiée dans le presse-papier",
|
|
97
|
+
});
|
|
98
|
+
} catch (_error) {
|
|
99
|
+
onToast?.({ type: "error", message: "Erreur lors de la copie" });
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handleSave = async () => {
|
|
104
|
+
if (!generatedImage || !onImageSave) return;
|
|
105
|
+
try {
|
|
106
|
+
await onImageSave(generatedImage.url);
|
|
107
|
+
onToast?.({ type: "success", message: "Image sauvegardée" });
|
|
108
|
+
} catch (_error) {
|
|
109
|
+
onToast?.({ type: "error", message: "Erreur lors de la sauvegarde" });
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleCloseImage = () => {
|
|
114
|
+
setGeneratedImage(null);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Styles selon le thème
|
|
118
|
+
const getThemeStyles = () => {
|
|
119
|
+
// Détection automatique du thème comme dans les autres composants
|
|
120
|
+
const isDark =
|
|
121
|
+
typeof document !== "undefined" &&
|
|
122
|
+
(document.documentElement.classList.contains("dark") ||
|
|
123
|
+
(!document.documentElement.classList.contains("light") &&
|
|
124
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches));
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
card: {
|
|
128
|
+
backgroundColor: isDark ? "#1f2937" : "white",
|
|
129
|
+
border: `1px solid ${isDark ? "#374151" : "#e5e7eb"}`,
|
|
130
|
+
color: isDark ? "#f3f4f6" : "#374151",
|
|
131
|
+
},
|
|
132
|
+
header: {
|
|
133
|
+
color: isDark ? "#f9fafb" : "#1f2937",
|
|
134
|
+
},
|
|
135
|
+
closeButton: {
|
|
136
|
+
color: isDark ? "#9ca3af" : "#6b7280",
|
|
137
|
+
hoverColor: isDark ? "#d1d5db" : "#374151",
|
|
138
|
+
},
|
|
139
|
+
imageContainer: {
|
|
140
|
+
backgroundColor: isDark ? "#111827" : "#f9fafb",
|
|
141
|
+
},
|
|
142
|
+
actionButton: {
|
|
143
|
+
backgroundColor: isDark ? "#374151" : "white",
|
|
144
|
+
border: `1px solid ${isDark ? "#4b5563" : "#e5e7eb"}`,
|
|
145
|
+
color: isDark ? "#d1d5db" : "#6b7280",
|
|
146
|
+
hoverBackground: isDark ? "#4b5563" : "#f3f4f6",
|
|
147
|
+
hoverColor: isDark ? "#f3f4f6" : "#1f2937",
|
|
148
|
+
},
|
|
149
|
+
metadata: {
|
|
150
|
+
borderTop: `1px solid ${isDark ? "#374151" : "#f3f4f6"}`,
|
|
151
|
+
color: isDark ? "#9ca3af" : "#6b7280",
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
46
156
|
const handleSubmit = async (
|
|
47
157
|
selectedModel: string,
|
|
48
158
|
selectedPrompt: string
|
|
@@ -51,47 +161,285 @@ export function AiImageButton({
|
|
|
51
161
|
const result = await generateImage({
|
|
52
162
|
model: selectedModel,
|
|
53
163
|
prompt: selectedPrompt,
|
|
164
|
+
size: "1024x1024", // Taille par défaut
|
|
54
165
|
});
|
|
55
166
|
|
|
56
167
|
if (result.url) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
168
|
+
// Stocker l'image générée
|
|
169
|
+
const imageData = {
|
|
170
|
+
url: result.url,
|
|
171
|
+
prompt: selectedPrompt,
|
|
172
|
+
requestId: result.requestId,
|
|
173
|
+
tokens: result.debitTokens,
|
|
174
|
+
};
|
|
175
|
+
setGeneratedImage(imageData);
|
|
176
|
+
|
|
177
|
+
onImage?.(result.url, {
|
|
178
|
+
requestId: result.requestId,
|
|
179
|
+
tokens: result.debitTokens,
|
|
180
|
+
});
|
|
181
|
+
onToast?.({ type: "success", message: "Image générée avec succès" });
|
|
182
|
+
|
|
183
|
+
// Afficher le toast de coût même en mode dev
|
|
184
|
+
showUsageToast({
|
|
185
|
+
requestId: result.requestId,
|
|
186
|
+
debitTokens: result.debitTokens,
|
|
187
|
+
usage: {
|
|
188
|
+
total_tokens: result.debitTokens,
|
|
189
|
+
prompt_tokens: Math.floor(result.debitTokens * 0.8),
|
|
190
|
+
completion_tokens: Math.floor(result.debitTokens * 0.2),
|
|
191
|
+
},
|
|
192
|
+
cost: apiKeyId?.includes("dev") ? 0 : result.debitTokens * 0.002, // Coût simulé
|
|
193
|
+
});
|
|
60
194
|
}
|
|
61
195
|
} catch (error) {
|
|
62
|
-
onToast?.({
|
|
196
|
+
onToast?.({
|
|
197
|
+
type: "error",
|
|
198
|
+
message: "Erreur lors de la génération de l'image",
|
|
199
|
+
code: error instanceof Error ? error.message : undefined,
|
|
200
|
+
});
|
|
63
201
|
} finally {
|
|
64
202
|
setIsOpen(false);
|
|
65
203
|
}
|
|
66
204
|
};
|
|
67
205
|
|
|
68
206
|
return (
|
|
69
|
-
<div
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
207
|
+
<div className="flex items-start gap-4">
|
|
208
|
+
<div style={{ position: "relative", display: "inline-block" }}>
|
|
209
|
+
<button
|
|
210
|
+
{...buttonProps}
|
|
211
|
+
onClick={handleOpenPanel}
|
|
212
|
+
disabled={disabled || loading}
|
|
213
|
+
className={className}
|
|
214
|
+
style={{
|
|
215
|
+
...aiStyles.button,
|
|
216
|
+
display: "flex",
|
|
217
|
+
alignItems: "center",
|
|
218
|
+
gap: "8px",
|
|
219
|
+
cursor: disabled || loading ? "not-allowed" : "pointer",
|
|
220
|
+
opacity: disabled || loading ? 0.6 : 1,
|
|
221
|
+
backgroundColor: loading ? "#8b5cf6" : "#6366f1",
|
|
222
|
+
color: "white",
|
|
223
|
+
border: "none",
|
|
224
|
+
borderRadius: "12px",
|
|
225
|
+
padding: "12px 20px",
|
|
226
|
+
fontSize: "14px",
|
|
227
|
+
fontWeight: "600",
|
|
228
|
+
minWidth: "140px",
|
|
229
|
+
height: "44px",
|
|
230
|
+
transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
|
|
231
|
+
boxShadow: loading
|
|
232
|
+
? "0 4px 12px rgba(139, 92, 246, 0.3)"
|
|
233
|
+
: "0 2px 8px rgba(99, 102, 241, 0.2)",
|
|
234
|
+
transform: "scale(1)",
|
|
235
|
+
...(loading && {
|
|
236
|
+
background: "linear-gradient(135deg, #6366f1, #8b5cf6)",
|
|
237
|
+
animation: "pulse 2s ease-in-out infinite",
|
|
238
|
+
}),
|
|
239
|
+
...buttonProps.style,
|
|
240
|
+
}}
|
|
241
|
+
onMouseEnter={(e) => {
|
|
242
|
+
if (!disabled && !loading) {
|
|
243
|
+
e.currentTarget.style.transform = "scale(1.02)";
|
|
244
|
+
e.currentTarget.style.boxShadow =
|
|
245
|
+
"0 6px 16px rgba(99, 102, 241, 0.3)";
|
|
246
|
+
}
|
|
247
|
+
}}
|
|
248
|
+
onMouseLeave={(e) => {
|
|
249
|
+
if (!disabled && !loading) {
|
|
250
|
+
e.currentTarget.style.transform = "scale(1)";
|
|
251
|
+
e.currentTarget.style.boxShadow = loading
|
|
252
|
+
? "0 4px 12px rgba(139, 92, 246, 0.3)"
|
|
253
|
+
: "0 2px 8px rgba(99, 102, 241, 0.2)";
|
|
254
|
+
}
|
|
255
|
+
}}
|
|
256
|
+
onMouseDown={(e) => {
|
|
257
|
+
if (!disabled && !loading) {
|
|
258
|
+
e.currentTarget.style.transform = "scale(0.98)";
|
|
259
|
+
}
|
|
260
|
+
}}
|
|
261
|
+
onMouseUp={(e) => {
|
|
262
|
+
if (!disabled && !loading) {
|
|
263
|
+
e.currentTarget.style.transform = "scale(1.02)";
|
|
264
|
+
}
|
|
265
|
+
}}
|
|
266
|
+
data-ai-image-button
|
|
267
|
+
>
|
|
268
|
+
{loading ? (
|
|
269
|
+
<>
|
|
270
|
+
<Loader2
|
|
271
|
+
size={18}
|
|
272
|
+
className="animate-spin"
|
|
273
|
+
style={{
|
|
274
|
+
color: "white",
|
|
275
|
+
filter: "drop-shadow(0 0 2px rgba(255,255,255,0.3))",
|
|
276
|
+
}}
|
|
277
|
+
/>
|
|
278
|
+
<span style={{ letterSpacing: "0.025em" }}>Génération...</span>
|
|
279
|
+
</>
|
|
280
|
+
) : (
|
|
281
|
+
<>
|
|
282
|
+
<ImageIcon
|
|
283
|
+
size={18}
|
|
284
|
+
style={{
|
|
285
|
+
color: "white",
|
|
286
|
+
filter: "drop-shadow(0 0 2px rgba(255,255,255,0.2))",
|
|
287
|
+
}}
|
|
288
|
+
/>
|
|
289
|
+
<span style={{ letterSpacing: "0.025em" }}>
|
|
290
|
+
{children || "Générer une image"}
|
|
291
|
+
</span>
|
|
292
|
+
</>
|
|
293
|
+
)}
|
|
294
|
+
</button>
|
|
295
|
+
{isOpen && (
|
|
296
|
+
<AiPromptPanel
|
|
297
|
+
isOpen={isOpen}
|
|
298
|
+
onClose={handleClosePanel}
|
|
299
|
+
onSubmit={handleSubmit}
|
|
300
|
+
uiMode={uiMode}
|
|
301
|
+
models={models?.filter((m) => m.type === "image") || []}
|
|
302
|
+
/>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
{/* Card d'affichage de l'image générée */}
|
|
307
|
+
{showImageCard && generatedImage && (
|
|
308
|
+
<div
|
|
309
|
+
className="relative"
|
|
310
|
+
style={{
|
|
311
|
+
maxWidth: "320px",
|
|
312
|
+
borderRadius: "12px",
|
|
313
|
+
padding: "16px",
|
|
314
|
+
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)",
|
|
315
|
+
...getThemeStyles().card,
|
|
316
|
+
}}
|
|
317
|
+
>
|
|
318
|
+
{/* Header avec prompt et bouton fermer */}
|
|
319
|
+
<div className="flex items-start justify-between mb-3">
|
|
320
|
+
<h3
|
|
321
|
+
className="font-medium text-sm leading-tight"
|
|
322
|
+
style={{
|
|
323
|
+
maxWidth: "calc(100% - 32px)",
|
|
324
|
+
...getThemeStyles().header,
|
|
325
|
+
}}
|
|
326
|
+
title={generatedImage.prompt}
|
|
327
|
+
>
|
|
328
|
+
{generatedImage.prompt.length > 60
|
|
329
|
+
? `${generatedImage.prompt.substring(0, 60)}...`
|
|
330
|
+
: generatedImage.prompt}
|
|
331
|
+
</h3>
|
|
332
|
+
<button
|
|
333
|
+
onClick={handleCloseImage}
|
|
334
|
+
className="transition-colors flex-shrink-0"
|
|
335
|
+
style={{
|
|
336
|
+
padding: "2px",
|
|
337
|
+
borderRadius: "4px",
|
|
338
|
+
backgroundColor: "transparent",
|
|
339
|
+
border: "none",
|
|
340
|
+
cursor: "pointer",
|
|
341
|
+
color: getThemeStyles().closeButton.color,
|
|
342
|
+
}}
|
|
343
|
+
onMouseEnter={(e) => {
|
|
344
|
+
e.currentTarget.style.color =
|
|
345
|
+
getThemeStyles().closeButton.hoverColor;
|
|
346
|
+
}}
|
|
347
|
+
onMouseLeave={(e) => {
|
|
348
|
+
e.currentTarget.style.color =
|
|
349
|
+
getThemeStyles().closeButton.color;
|
|
350
|
+
}}
|
|
351
|
+
>
|
|
352
|
+
<X size={16} />
|
|
353
|
+
</button>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
{/* Image */}
|
|
357
|
+
<div
|
|
358
|
+
className="mb-4 rounded-lg overflow-hidden"
|
|
359
|
+
style={getThemeStyles().imageContainer}
|
|
360
|
+
>
|
|
361
|
+
<img
|
|
362
|
+
src={generatedImage.url}
|
|
363
|
+
alt={generatedImage.prompt}
|
|
364
|
+
className="w-full h-auto"
|
|
365
|
+
style={{
|
|
366
|
+
maxHeight: "200px",
|
|
367
|
+
objectFit: "contain",
|
|
368
|
+
display: "block",
|
|
369
|
+
}}
|
|
370
|
+
/>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
{/* Actions */}
|
|
374
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
375
|
+
<button
|
|
376
|
+
onClick={handleDownload}
|
|
377
|
+
className="flex items-center gap-1 px-3 py-2 text-xs font-medium rounded-lg transition-colors"
|
|
378
|
+
style={getThemeStyles().actionButton}
|
|
379
|
+
onMouseEnter={(e) => {
|
|
380
|
+
e.currentTarget.style.backgroundColor =
|
|
381
|
+
getThemeStyles().actionButton.hoverBackground;
|
|
382
|
+
e.currentTarget.style.color =
|
|
383
|
+
getThemeStyles().actionButton.hoverColor;
|
|
384
|
+
}}
|
|
385
|
+
onMouseLeave={(e) => {
|
|
386
|
+
e.currentTarget.style.backgroundColor =
|
|
387
|
+
getThemeStyles().actionButton.backgroundColor;
|
|
388
|
+
e.currentTarget.style.color =
|
|
389
|
+
getThemeStyles().actionButton.color;
|
|
390
|
+
}}
|
|
391
|
+
title="Télécharger l'image"
|
|
392
|
+
>
|
|
393
|
+
<Download size={14} />
|
|
394
|
+
Télécharger
|
|
395
|
+
</button>
|
|
396
|
+
|
|
397
|
+
<button
|
|
398
|
+
onClick={handleCopyUrl}
|
|
399
|
+
className="flex items-center gap-1 px-3 py-2 text-xs font-medium rounded-lg transition-colors"
|
|
400
|
+
style={getThemeStyles().actionButton}
|
|
401
|
+
onMouseEnter={(e) => {
|
|
402
|
+
e.currentTarget.style.backgroundColor =
|
|
403
|
+
getThemeStyles().actionButton.hoverBackground;
|
|
404
|
+
e.currentTarget.style.color =
|
|
405
|
+
getThemeStyles().actionButton.hoverColor;
|
|
406
|
+
}}
|
|
407
|
+
onMouseLeave={(e) => {
|
|
408
|
+
e.currentTarget.style.backgroundColor =
|
|
409
|
+
getThemeStyles().actionButton.backgroundColor;
|
|
410
|
+
e.currentTarget.style.color =
|
|
411
|
+
getThemeStyles().actionButton.color;
|
|
412
|
+
}}
|
|
413
|
+
title="Copier l'URL"
|
|
414
|
+
>
|
|
415
|
+
<Copy size={14} />
|
|
416
|
+
Copier URL
|
|
417
|
+
</button>
|
|
418
|
+
|
|
419
|
+
{onImageSave && (
|
|
420
|
+
<button
|
|
421
|
+
onClick={handleSave}
|
|
422
|
+
className="flex items-center gap-1 px-3 py-2 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
|
|
423
|
+
title="Sauvegarder en base"
|
|
424
|
+
>
|
|
425
|
+
<ExternalLink size={14} />
|
|
426
|
+
Sauvegarder
|
|
427
|
+
</button>
|
|
428
|
+
)}
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
{/* Metadata */}
|
|
432
|
+
<div
|
|
433
|
+
className="mt-3 pt-3 text-xs"
|
|
434
|
+
style={{
|
|
435
|
+
...getThemeStyles().metadata,
|
|
436
|
+
}}
|
|
437
|
+
>
|
|
438
|
+
<div className="flex justify-center">
|
|
439
|
+
<span>ID: {generatedImage.requestId.slice(-8)}</span>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
95
443
|
)}
|
|
96
444
|
</div>
|
|
97
445
|
);
|
|
@@ -348,16 +348,20 @@ export function AiStatusButton({
|
|
|
348
348
|
>
|
|
349
349
|
<div style={aiStyles.tooltipRow}>
|
|
350
350
|
<span style={aiStyles.tooltipLabel}>API Key:</span>
|
|
351
|
-
<span style={aiStyles.tooltipValue}>
|
|
351
|
+
<span style={aiStyles.tooltipValue}>
|
|
352
|
+
{status.api_key?.name || "N/A"}
|
|
353
|
+
</span>
|
|
352
354
|
</div>
|
|
353
355
|
<div style={aiStyles.tooltipRow}>
|
|
354
356
|
<span style={aiStyles.tooltipLabel}>Env:</span>
|
|
355
|
-
<span style={aiStyles.tooltipValue}>
|
|
357
|
+
<span style={aiStyles.tooltipValue}>
|
|
358
|
+
{status.api_key?.env || "N/A"}
|
|
359
|
+
</span>
|
|
356
360
|
</div>
|
|
357
361
|
<div style={aiStyles.tooltipRow}>
|
|
358
362
|
<span style={aiStyles.tooltipLabel}>Rate Limit:</span>
|
|
359
363
|
<span style={aiStyles.tooltipValue}>
|
|
360
|
-
{status.api_key
|
|
364
|
+
{status.api_key?.rate_limit_rpm || 0} req/min
|
|
361
365
|
</span>
|
|
362
366
|
</div>
|
|
363
367
|
</div>
|
|
@@ -20,9 +20,11 @@ export function UsageToast({
|
|
|
20
20
|
|
|
21
21
|
useEffect(() => {
|
|
22
22
|
if (result) {
|
|
23
|
-
// Show toast immediately
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// Show toast immediately - using startTransition to avoid cascading renders warning
|
|
24
|
+
queueMicrotask(() => {
|
|
25
|
+
setIsVisible(true);
|
|
26
|
+
setIsClosing(false);
|
|
27
|
+
});
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
return () => {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { AiChipInput } from "../components/AiChipLabel";
|
|
5
|
+
import { AiProvider } from "../context/AiProvider";
|
|
6
|
+
|
|
7
|
+
export function AiChipInputExample() {
|
|
8
|
+
const [tags, setTags] = useState<string[]>(["react", "typescript"]);
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<AiProvider baseUrl="/api/ai" apiKeyId="dev-key-example">
|
|
12
|
+
<div style={{ padding: "20px", maxWidth: "600px" }}>
|
|
13
|
+
<h2 style={{ marginBottom: "16px" }}>Exemple AiChipInput</h2>
|
|
14
|
+
|
|
15
|
+
<div style={{ marginBottom: "24px" }}>
|
|
16
|
+
<label
|
|
17
|
+
style={{ display: "block", marginBottom: "8px", fontWeight: "500" }}
|
|
18
|
+
>
|
|
19
|
+
Tags du projet
|
|
20
|
+
</label>
|
|
21
|
+
<AiChipInput
|
|
22
|
+
value={tags}
|
|
23
|
+
onChange={setTags}
|
|
24
|
+
placeholder="Ajoutez des tags séparés par des virgules ou générez avec l'IA..."
|
|
25
|
+
context="web development"
|
|
26
|
+
maxChips={10}
|
|
27
|
+
allowDuplicates={false}
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div
|
|
32
|
+
style={{
|
|
33
|
+
marginTop: "20px",
|
|
34
|
+
padding: "16px",
|
|
35
|
+
backgroundColor: "#f8f9fa",
|
|
36
|
+
borderRadius: "6px",
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<h3 style={{ margin: "0 0 8px 0", fontSize: "14px" }}>
|
|
40
|
+
Valeur actuelle :
|
|
41
|
+
</h3>
|
|
42
|
+
<pre style={{ margin: 0, fontSize: "12px" }}>
|
|
43
|
+
{JSON.stringify(tags, null, 2)}
|
|
44
|
+
</pre>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div style={{ marginTop: "16px", fontSize: "14px", color: "#6b7280" }}>
|
|
48
|
+
<p>
|
|
49
|
+
<strong>Mode développement :</strong>
|
|
50
|
+
</p>
|
|
51
|
+
<ul style={{ paddingLeft: "20px" }}>
|
|
52
|
+
<li>API Key contient "dev" → Simulation activée</li>
|
|
53
|
+
<li>Pas d'appel API réel</li>
|
|
54
|
+
<li>
|
|
55
|
+
Génération de tags simulés : "react, typescript, javascript..."
|
|
56
|
+
</li>
|
|
57
|
+
</ul>
|
|
58
|
+
|
|
59
|
+
<p>
|
|
60
|
+
<strong>Instructions :</strong>
|
|
61
|
+
</p>
|
|
62
|
+
<ul style={{ paddingLeft: "20px" }}>
|
|
63
|
+
<li>Tapez du texte et appuyez sur Entrée pour créer un chip</li>
|
|
64
|
+
<li>
|
|
65
|
+
Séparez plusieurs tags avec des virgules (,) ou des
|
|
66
|
+
points-virgules (;)
|
|
67
|
+
</li>
|
|
68
|
+
<li>
|
|
69
|
+
Cliquez sur l'icône ✨ pour générer des tags automatiquement
|
|
70
|
+
</li>
|
|
71
|
+
<li>Utilisez le ❌ sur chaque chip pour le supprimer</li>
|
|
72
|
+
<li>
|
|
73
|
+
Appuyez sur Backspace dans un champ vide pour supprimer le dernier
|
|
74
|
+
chip
|
|
75
|
+
</li>
|
|
76
|
+
</ul>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</AiProvider>
|
|
80
|
+
);
|
|
81
|
+
}
|