@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.
Files changed (44) hide show
  1. package/dist/components/AiChipLabel.d.ts +12 -0
  2. package/dist/components/AiChipLabel.d.ts.map +1 -1
  3. package/dist/components/AiChipLabel.js +129 -1
  4. package/dist/components/AiContextButton.d.ts +18 -0
  5. package/dist/components/AiContextButton.d.ts.map +1 -0
  6. package/dist/components/AiContextButton.js +339 -0
  7. package/dist/components/AiImageButton.d.ts +12 -3
  8. package/dist/components/AiImageButton.d.ts.map +1 -1
  9. package/dist/components/AiImageButton.js +218 -8
  10. package/dist/components/AiStatusButton.d.ts.map +1 -1
  11. package/dist/components/AiStatusButton.js +1 -1
  12. package/dist/components/UsageToast.d.ts.map +1 -1
  13. package/dist/components/UsageToast.js +5 -3
  14. package/dist/examples/AiChipInputExample.d.ts +2 -0
  15. package/dist/examples/AiChipInputExample.d.ts.map +1 -0
  16. package/dist/examples/AiChipInputExample.js +14 -0
  17. package/dist/examples/AiContextButtonExample.d.ts +2 -0
  18. package/dist/examples/AiContextButtonExample.d.ts.map +1 -0
  19. package/dist/examples/AiContextButtonExample.js +88 -0
  20. package/dist/examples/AiImageButtonExample.d.ts +2 -0
  21. package/dist/examples/AiImageButtonExample.d.ts.map +1 -0
  22. package/dist/examples/AiImageButtonExample.js +26 -0
  23. package/dist/hooks/useAiCallImage.d.ts.map +1 -1
  24. package/dist/hooks/useAiCallImage.js +107 -1
  25. package/dist/hooks/useAiCallText.d.ts.map +1 -1
  26. package/dist/hooks/useAiCallText.js +25 -1
  27. package/dist/index.d.ts +4 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4 -0
  30. package/dist/styles/inline.d.ts.map +1 -1
  31. package/dist/styles/inline.js +3 -1
  32. package/package.json +2 -2
  33. package/src/components/AiChipLabel.tsx +218 -1
  34. package/src/components/AiContextButton.tsx +553 -0
  35. package/src/components/AiImageButton.tsx +386 -38
  36. package/src/components/AiStatusButton.tsx +7 -3
  37. package/src/components/UsageToast.tsx +5 -3
  38. package/src/examples/AiChipInputExample.tsx +81 -0
  39. package/src/examples/AiContextButtonExample.tsx +338 -0
  40. package/src/examples/AiImageButtonExample.tsx +72 -0
  41. package/src/hooks/useAiCallImage.ts +149 -1
  42. package/src/hooks/useAiCallText.ts +30 -1
  43. package/src/index.ts +4 -0
  44. 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 { UsageToast, useUsageToast } from "./UsageToast";
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?: (imageUrl: string) => void;
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
- onImage?.(result.url);
58
- onToast?.({ type: "success", message: "Image generated successfully" });
59
- showUsageToast(result);
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?.({ type: "error", message: "Failed to generate image" });
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 style={{ position: "relative", display: "inline-block" }}>
70
- <button
71
- {...buttonProps}
72
- onClick={handleOpenPanel}
73
- disabled={disabled || loading}
74
- className={className}
75
- data-ai-image-button
76
- >
77
- {loading ? "Generating..." : children || "Generate Image"}
78
- </button>
79
- {isOpen && (
80
- <AiPromptPanel
81
- isOpen={isOpen}
82
- onClose={handleClosePanel}
83
- onSubmit={handleSubmit}
84
- uiMode={uiMode}
85
- models={models?.filter((m) => m.type === "image") || []}
86
- />
87
- )}
88
- {Boolean(toastData) && (
89
- <UsageToast
90
- key={toastKey}
91
- result={toastData}
92
- position="bottom-right"
93
- onComplete={clearToast}
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}>{status.api_key.name}</span>
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}>{status.api_key.env}</span>
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.rate_limit_rpm} req/min
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
- setIsVisible(true);
25
- setIsClosing(false);
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
+ }