@lastbrain/ai-ui-react 1.0.67 → 1.0.69

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