@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
@@ -0,0 +1,553 @@
1
+ "use client";
2
+
3
+ import { useState, type ButtonHTMLAttributes } from "react";
4
+ import { Loader2, X, FileText, Sparkle, Download } from "lucide-react";
5
+ import type { BaseAiProps } from "../types";
6
+ import { useAiCallText } from "../hooks/useAiCallText";
7
+ import { useAiModels } from "../hooks/useAiModels";
8
+ import { AiPromptPanel } from "./AiPromptPanel";
9
+ import { useUsageToast } from "./UsageToast";
10
+ import { aiStyles } from "../styles/inline";
11
+ import { useAiContext } from "../context/AiProvider";
12
+
13
+ export interface AiContextButtonProps
14
+ extends
15
+ Omit<BaseAiProps, "onValue" | "type">,
16
+ Omit<ButtonHTMLAttributes<HTMLButtonElement>, "baseUrl" | "apiKeyId"> {
17
+ // Données de contexte à analyser
18
+ contextData: any;
19
+ contextDescription?: string;
20
+ onResult?: (
21
+ result: string,
22
+ metadata?: { requestId: string; tokens: number }
23
+ ) => void;
24
+ uiMode?: "modal" | "drawer";
25
+ resultModalTitle?: string;
26
+ storeOutputs?: boolean; // For external API calls - whether to store outputs
27
+ artifactTitle?: string; // Title for stored artifacts
28
+ // Props optionnelles pour override du contexte
29
+ baseUrl?: string;
30
+ apiKeyId?: string;
31
+ }
32
+
33
+ export function AiContextButton({
34
+ baseUrl: propBaseUrl,
35
+ apiKeyId: propApiKeyId,
36
+ uiMode = "modal",
37
+ contextData,
38
+ contextDescription = "Données à analyser",
39
+ onResult,
40
+ onToast,
41
+ disabled,
42
+ className,
43
+ children,
44
+ resultModalTitle = "Résultat de l'analyse",
45
+ storeOutputs,
46
+ artifactTitle,
47
+ context: _context,
48
+ model: _model,
49
+ prompt: _prompt,
50
+ ...buttonProps
51
+ }: AiContextButtonProps) {
52
+ const [isOpen, setIsOpen] = useState(false);
53
+ const [isResultOpen, setIsResultOpen] = useState(false);
54
+ const [analysisResult, setAnalysisResult] = useState<{
55
+ content: string;
56
+ prompt: string;
57
+ requestId: string;
58
+ tokens: number;
59
+ cost: number;
60
+ } | null>(null);
61
+ const { showUsageToast, toastData, toastKey, clearToast } = useUsageToast();
62
+
63
+ // Récupérer le contexte AiProvider avec fallback sur les props
64
+ const aiContext = useAiContext();
65
+ const baseUrl = propBaseUrl ?? aiContext.baseUrl;
66
+ const apiKeyId = propApiKeyId ?? aiContext.apiKeyId;
67
+
68
+ const { models, loading: modelsLoading } = useAiModels({ baseUrl, apiKeyId });
69
+ const { generateText: callText, loading } = useAiCallText({
70
+ baseUrl,
71
+ apiKeyId,
72
+ });
73
+
74
+ const handleOpenPanel = () => {
75
+ console.log("Opening panel, models:", models, "loading:", modelsLoading);
76
+ setIsOpen(true);
77
+ };
78
+
79
+ const handleClosePanel = () => {
80
+ setIsOpen(false);
81
+ };
82
+
83
+ const handleCloseResult = () => {
84
+ setIsResultOpen(false);
85
+ setAnalysisResult(null);
86
+ };
87
+
88
+ const saveToFile = () => {
89
+ if (!analysisResult) return;
90
+
91
+ const currentDate = new Date()
92
+ .toLocaleDateString("fr-FR")
93
+ .replace(/\//g, "-");
94
+ const defaultName = `analyse-${currentDate}.txt`;
95
+ const fileName = prompt("Nom du fichier :", defaultName) || defaultName;
96
+
97
+ const content = `ANALYSE DES DONNÉES - ${new Date().toLocaleString("fr-FR")}
98
+
99
+ PROMPT UTILISÉ :
100
+ ${analysisResult.prompt}
101
+
102
+ RÉSULTAT DE L'ANALYSE :
103
+ ${analysisResult.content}
104
+
105
+ --- MÉTADONNÉES ---
106
+ Tokens utilisés: ${analysisResult.tokens.toLocaleString()}
107
+ Coût: $${(apiKeyId?.includes("dev") ? 0 : analysisResult.cost).toFixed(6)}
108
+ ID de requête: ${analysisResult.requestId || "N/A"}
109
+ Date: ${new Date().toLocaleString("fr-FR")}`;
110
+
111
+ const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
112
+ const url = URL.createObjectURL(blob);
113
+ const a = document.createElement("a");
114
+ a.href = url;
115
+ a.download = fileName.endsWith(".txt") ? fileName : `${fileName}.txt`;
116
+ document.body.appendChild(a);
117
+ a.click();
118
+ document.body.removeChild(a);
119
+ URL.revokeObjectURL(url);
120
+ };
121
+
122
+ // Styles selon le thème
123
+ const getThemeStyles = () => {
124
+ const isDark =
125
+ typeof document !== "undefined" &&
126
+ (document.documentElement.classList.contains("dark") ||
127
+ (!document.documentElement.classList.contains("light") &&
128
+ window.matchMedia("(prefers-color-scheme: dark)").matches));
129
+
130
+ return {
131
+ modal: {
132
+ backgroundColor: isDark ? "#1f2937" : "white",
133
+ border: `1px solid ${isDark ? "#374151" : "#e5e7eb"}`,
134
+ color: isDark ? "#f3f4f6" : "#374151",
135
+ },
136
+ header: {
137
+ color: isDark ? "#f9fafb" : "#1f2937",
138
+ borderBottom: `1px solid ${isDark ? "#374151" : "#e5e7eb"}`,
139
+ },
140
+ content: {
141
+ backgroundColor: isDark ? "#111827" : "#f9fafb",
142
+ border: `1px solid ${isDark ? "#374151" : "#e5e7eb"}`,
143
+ },
144
+ closeButton: {
145
+ color: isDark ? "#9ca3af" : "#6b7280",
146
+ hoverColor: isDark ? "#d1d5db" : "#374151",
147
+ },
148
+ };
149
+ };
150
+
151
+ const formatContextData = (data: any): string => {
152
+ if (typeof data === "string") return data;
153
+ if (typeof data === "object" && data !== null) {
154
+ return JSON.stringify(data, null, 2);
155
+ }
156
+ return String(data);
157
+ };
158
+
159
+ const handleSubmit = async (
160
+ selectedModel: string,
161
+ selectedPrompt: string
162
+ ) => {
163
+ try {
164
+ // Construire le prompt avec le contexte
165
+ const contextString = formatContextData(contextData);
166
+ const fullPrompt = `${selectedPrompt}
167
+
168
+ CONTEXTE (${contextDescription}):
169
+ ${contextString}
170
+
171
+ Analyse ces données et réponds de manière structurée et claire.`;
172
+
173
+ const result = await callText({
174
+ prompt: fullPrompt,
175
+ model: selectedModel || "gpt-4o-mini",
176
+ context: _context || undefined,
177
+ maxTokens: 4000,
178
+ temperature: 0.7,
179
+ });
180
+
181
+ if (result.text) {
182
+ // Calculer le total des tokens depuis la réponse réelle
183
+ const resultAny = result as any;
184
+ const totalTokens =
185
+ (resultAny.inputTokens || 0) + (resultAny.outputTokens || 0) ||
186
+ result.debitTokens ||
187
+ 0;
188
+ const actualCost = resultAny.cost || 0;
189
+
190
+ const resultData = {
191
+ content: result.text,
192
+ prompt: selectedPrompt,
193
+ requestId: result.requestId,
194
+ tokens: totalTokens,
195
+ cost: actualCost,
196
+ };
197
+
198
+ setAnalysisResult(resultData);
199
+ setIsResultOpen(true);
200
+
201
+ onResult?.(result.text, {
202
+ requestId: result.requestId,
203
+ tokens: result.debitTokens || 0,
204
+ });
205
+
206
+ onToast?.({
207
+ type: "success",
208
+ message: `Analyse terminée - Coût: $${(apiKeyId?.includes("dev") ? 0 : actualCost).toFixed(6)}`,
209
+ });
210
+
211
+ // Afficher le toast de coût
212
+ showUsageToast({
213
+ requestId: result.requestId,
214
+ debitTokens: totalTokens,
215
+ usage: {
216
+ total_tokens: totalTokens,
217
+ prompt_tokens: resultAny.inputTokens || 0,
218
+ completion_tokens: resultAny.outputTokens || 0,
219
+ },
220
+ cost: apiKeyId?.includes("dev") ? 0 : actualCost,
221
+ });
222
+ }
223
+ } catch (error) {
224
+ onToast?.({
225
+ type: "error",
226
+ message: "Erreur lors de l'analyse",
227
+ code: error instanceof Error ? error.message : undefined,
228
+ });
229
+ } finally {
230
+ setIsOpen(false);
231
+ }
232
+ };
233
+
234
+ return (
235
+ <>
236
+ <div style={{ position: "relative", display: "inline-block" }}>
237
+ <button
238
+ {...buttonProps}
239
+ onClick={handleOpenPanel}
240
+ disabled={disabled || loading}
241
+ className={className}
242
+ style={{
243
+ ...aiStyles.button,
244
+ display: "flex",
245
+ alignItems: "center",
246
+ gap: "8px",
247
+ cursor: disabled || loading ? "not-allowed" : "pointer",
248
+ opacity: disabled || loading ? 0.6 : 1,
249
+ backgroundColor: loading ? "#8b5cf6" : "#7c3aed",
250
+ color: "white",
251
+ border: "none",
252
+ borderRadius: "12px",
253
+ // padding: "12px 20px",
254
+ margin: "0px 8px",
255
+ fontSize: "14px",
256
+ fontWeight: "600",
257
+ minWidth: "20px",
258
+ height: "44px",
259
+ transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
260
+ boxShadow: loading
261
+ ? "0 4px 12px rgba(139, 92, 246, 0.3)"
262
+ : "0 2px 8px rgba(124, 58, 237, 0.2)",
263
+ transform: "scale(1)",
264
+ ...(loading && {
265
+ background: "linear-gradient(135deg, #7c3aed, #8b5cf6)",
266
+ animation: "pulse 2s ease-in-out infinite",
267
+ }),
268
+ ...buttonProps.style,
269
+ }}
270
+ onMouseEnter={(e) => {
271
+ if (!disabled && !loading) {
272
+ e.currentTarget.style.transform = "scale(1.02)";
273
+ e.currentTarget.style.boxShadow =
274
+ "0 6px 16px rgba(124, 58, 237, 0.3)";
275
+ }
276
+ }}
277
+ onMouseLeave={(e) => {
278
+ if (!disabled && !loading) {
279
+ e.currentTarget.style.transform = "scale(1)";
280
+ e.currentTarget.style.boxShadow = loading
281
+ ? "0 4px 12px rgba(139, 92, 246, 0.3)"
282
+ : "0 2px 8px rgba(124, 58, 237, 0.2)";
283
+ }
284
+ }}
285
+ onMouseDown={(e) => {
286
+ if (!disabled && !loading) {
287
+ e.currentTarget.style.transform = "scale(0.98)";
288
+ }
289
+ }}
290
+ onMouseUp={(e) => {
291
+ if (!disabled && !loading) {
292
+ e.currentTarget.style.transform = "scale(1.02)";
293
+ }
294
+ }}
295
+ data-ai-context-button
296
+ >
297
+ {loading ? (
298
+ <>
299
+ <Loader2
300
+ size={18}
301
+ className="animate-spin"
302
+ style={{
303
+ color: "white",
304
+ filter: "drop-shadow(0 0 2px rgba(255,255,255,0.3))",
305
+ }}
306
+ />
307
+ <span style={{ letterSpacing: "0.025em" }}>Analyse...</span>
308
+ </>
309
+ ) : (
310
+ <>
311
+ <Sparkle
312
+ size={18}
313
+ style={{
314
+ color: "white",
315
+ filter: "drop-shadow(0 0 2px rgba(255,255,255,0.2))",
316
+ }}
317
+ />
318
+ {/* <span style={{ letterSpacing: "0.025em" }}>{children || ""}</span> */}
319
+ </>
320
+ )}
321
+ </button>
322
+
323
+ {isOpen && (
324
+ <AiPromptPanel
325
+ isOpen={isOpen}
326
+ onClose={handleClosePanel}
327
+ onSubmit={handleSubmit}
328
+ uiMode={uiMode}
329
+ models={models?.filter((m) => m.type === "language") || []}
330
+ enableModelManagement={true}
331
+ />
332
+ )}
333
+ </div>
334
+
335
+ {/* Modal de résultat */}
336
+ {isResultOpen && analysisResult && (
337
+ <div
338
+ style={{
339
+ position: "fixed",
340
+ top: 0,
341
+ left: 0,
342
+ right: 0,
343
+ bottom: 0,
344
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
345
+ backdropFilter: "blur(8px)",
346
+ display: "flex",
347
+ alignItems: "center",
348
+ justifyContent: "center",
349
+ zIndex: 1000,
350
+ padding: "20px",
351
+ }}
352
+ onClick={(e) => {
353
+ if (e.target === e.currentTarget) {
354
+ handleCloseResult();
355
+ }
356
+ }}
357
+ >
358
+ <div
359
+ style={{
360
+ maxWidth: "800px",
361
+ position: "relative",
362
+ width: "100%",
363
+ maxHeight: "90vh",
364
+ borderRadius: "16px",
365
+ boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1)",
366
+ overflow: "hidden",
367
+ ...getThemeStyles().modal,
368
+ }}
369
+ >
370
+ {/* Header */}
371
+ <div
372
+ style={{
373
+ padding: "20px 24px 16px",
374
+ marginBottom: "12px",
375
+ display: "flex",
376
+ alignItems: "center",
377
+ gap: "12px",
378
+ flexDirection: "column",
379
+ justifyContent: "space-between",
380
+ ...getThemeStyles().header,
381
+ }}
382
+ >
383
+ <div
384
+ style={{
385
+ display: "flex",
386
+
387
+ alignItems: "center",
388
+ gap: "12px",
389
+ }}
390
+ >
391
+ <FileText size={20} />
392
+ <h2
393
+ style={{
394
+ fontSize: "18px",
395
+ fontWeight: "600",
396
+ margin: 0,
397
+ }}
398
+ >
399
+ {resultModalTitle}
400
+ </h2>
401
+ </div>
402
+ <div style={{ display: "flex", gap: "8px" }}>
403
+ <button
404
+ onClick={saveToFile}
405
+ style={{
406
+ padding: "8px 12px",
407
+ borderRadius: "6px",
408
+ backgroundColor: "transparent",
409
+ border: `1px solid ${getThemeStyles().closeButton.color}30`,
410
+ cursor: "pointer",
411
+ color: getThemeStyles().closeButton.color,
412
+ transition: "all 0.2s",
413
+ display: "flex",
414
+ alignItems: "center",
415
+ gap: "6px",
416
+ fontSize: "12px",
417
+ }}
418
+ onMouseEnter={(e) => {
419
+ e.currentTarget.style.backgroundColor =
420
+ getThemeStyles().closeButton.hoverColor + "10";
421
+ e.currentTarget.style.color =
422
+ getThemeStyles().closeButton.hoverColor;
423
+ }}
424
+ onMouseLeave={(e) => {
425
+ e.currentTarget.style.backgroundColor = "transparent";
426
+ e.currentTarget.style.color =
427
+ getThemeStyles().closeButton.color;
428
+ }}
429
+ title="Télécharger l'analyse"
430
+ >
431
+ <Download size={14} />
432
+ Sauvegarder
433
+ </button>
434
+ <button
435
+ onClick={handleCloseResult}
436
+ style={{
437
+ position: "absolute",
438
+ top: "16px",
439
+ right: "16px",
440
+ padding: "4px",
441
+ borderRadius: "6px",
442
+ backgroundColor: "transparent",
443
+ border: "none",
444
+ cursor: "pointer",
445
+ color: getThemeStyles().closeButton.color,
446
+ transition: "color 0.2s",
447
+ }}
448
+ onMouseEnter={(e) => {
449
+ e.currentTarget.style.color =
450
+ getThemeStyles().closeButton.hoverColor;
451
+ }}
452
+ onMouseLeave={(e) => {
453
+ e.currentTarget.style.color =
454
+ getThemeStyles().closeButton.color;
455
+ }}
456
+ >
457
+ <X size={18} />
458
+ </button>
459
+ </div>
460
+
461
+ {/* Content */}
462
+ <div
463
+ style={{
464
+ padding: "0 24px 24px",
465
+ overflow: "auto",
466
+ maxHeight: "calc(90vh - 80px)",
467
+ }}
468
+ >
469
+ {/* Prompt utilisé */}
470
+ <div style={{ marginBottom: "20px" }}>
471
+ <h3
472
+ style={{
473
+ fontSize: "14px",
474
+ fontWeight: "600",
475
+ marginBottom: "8px",
476
+ color: getThemeStyles().modal.color,
477
+ }}
478
+ >
479
+ Prompt utilisé :
480
+ </h3>
481
+ <div
482
+ style={{
483
+ padding: "12px",
484
+ borderRadius: "8px",
485
+ fontSize: "13px",
486
+ fontFamily: "monospace",
487
+ ...getThemeStyles().content,
488
+ }}
489
+ >
490
+ {analysisResult.prompt}
491
+ </div>
492
+ </div>
493
+
494
+ {/* Résultat */}
495
+ <div>
496
+ <h3
497
+ style={{
498
+ fontSize: "14px",
499
+ fontWeight: "600",
500
+ marginBottom: "12px",
501
+ color: getThemeStyles().modal.color,
502
+ }}
503
+ >
504
+ Résultat de l'analyse :
505
+ </h3>
506
+ <div
507
+ style={{
508
+ padding: "16px",
509
+ borderRadius: "8px",
510
+ lineHeight: "1.6",
511
+ fontSize: "14px",
512
+ whiteSpace: "pre-wrap",
513
+ ...getThemeStyles().content,
514
+ }}
515
+ >
516
+ {analysisResult.content}
517
+ </div>
518
+ </div>
519
+
520
+ {/* Metadata */}
521
+ <div style={{ height: "120px" }}>
522
+ <div
523
+ style={{
524
+ marginTop: "20px",
525
+ padding: "12px",
526
+ fontSize: "12px",
527
+ borderRadius: "8px",
528
+ display: "flex",
529
+
530
+ justifyContent: "space-between",
531
+ ...getThemeStyles().content,
532
+ }}
533
+ >
534
+ <span>
535
+ Coût: $
536
+ {(apiKeyId?.includes("dev")
537
+ ? 0
538
+ : analysisResult.cost
539
+ ).toFixed(6)}
540
+ </span>
541
+ <span>
542
+ ID: {analysisResult.requestId?.slice(-8) || "N/A"}
543
+ </span>
544
+ </div>
545
+ </div>
546
+ </div>
547
+ </div>
548
+ </div>
549
+ </div>
550
+ )}
551
+ </>
552
+ );
553
+ }