data-primals-engine 1.5.2 → 1.6.1

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 (70) hide show
  1. package/README.md +938 -915
  2. package/client/README.md +136 -136
  3. package/client/index.js +0 -1
  4. package/client/public/doc/API.postman_collection.json +213 -213
  5. package/client/public/react.svg +9 -9
  6. package/client/src/AddWidgetTypeModal.jsx +47 -47
  7. package/client/src/App.css +42 -42
  8. package/client/src/App.jsx +92 -150
  9. package/client/src/App.scss +6 -0
  10. package/client/src/AssistantChat.jsx +362 -363
  11. package/client/src/Button.jsx +12 -12
  12. package/client/src/Dashboard.jsx +480 -480
  13. package/client/src/DashboardHtmlViewItem.jsx +146 -146
  14. package/client/src/DashboardView.jsx +654 -654
  15. package/client/src/DataEditor.jsx +383 -383
  16. package/client/src/DataLayout.jsx +779 -808
  17. package/client/src/DataTable.jsx +782 -822
  18. package/client/src/DataTable.scss +186 -186
  19. package/client/src/GeolocationField.jsx +93 -93
  20. package/client/src/HistoryDialog.jsx +307 -307
  21. package/client/src/HistoryDialog.scss +319 -319
  22. package/client/src/HtmlViewBuilderModal.jsx +90 -90
  23. package/client/src/HtmlViewBuilderModal.scss +17 -17
  24. package/client/src/HtmlViewCard.jsx +43 -43
  25. package/client/src/ModelCreator.jsx +683 -686
  26. package/client/src/ModelCreator.scss +1 -1
  27. package/client/src/ModelCreatorField.jsx +950 -950
  28. package/client/src/ModelImporter.jsx +3 -0
  29. package/client/src/ModelList.jsx +280 -280
  30. package/client/src/Notification.jsx +136 -136
  31. package/client/src/PackGallery.jsx +391 -391
  32. package/client/src/PackGallery.scss +231 -231
  33. package/client/src/Pagination.jsx +143 -143
  34. package/client/src/RelationField.jsx +354 -354
  35. package/client/src/RelationSelectorWidget.jsx +172 -172
  36. package/client/src/RelationValue.jsx +2 -1
  37. package/client/src/contexts/CommandContext.jsx +260 -0
  38. package/client/src/contexts/ModelContext.jsx +4 -1
  39. package/client/src/contexts/UIContext.jsx +72 -72
  40. package/client/src/filter.js +263 -263
  41. package/client/src/index.css +90 -90
  42. package/package.json +6 -5
  43. package/perf/artillery-hooks.js +37 -37
  44. package/perf/setup.yml +25 -25
  45. package/src/constants.js +4 -0
  46. package/src/defaultModels.js +7 -0
  47. package/src/engine.js +335 -335
  48. package/src/events.js +232 -137
  49. package/src/filter.js +274 -274
  50. package/src/index.js +1 -0
  51. package/src/modules/assistant/assistant.js +225 -83
  52. package/src/modules/auth-google/index.js +50 -50
  53. package/src/modules/auth-microsoft/index.js +81 -81
  54. package/src/modules/data/data.core.js +118 -118
  55. package/src/modules/data/data.history.js +555 -555
  56. package/src/modules/data/data.operations.js +112 -9
  57. package/src/modules/data/data.relations.js +686 -686
  58. package/src/modules/data/data.routes.js +1879 -1879
  59. package/src/modules/data/data.validation.js +2 -2
  60. package/src/modules/file.js +247 -247
  61. package/src/modules/user.js +14 -9
  62. package/src/packs.js +2 -2
  63. package/src/providers.js +2 -2
  64. package/src/services/stripe.js +196 -196
  65. package/src/sso.js +193 -193
  66. package/test/data.history.integration.test.js +264 -264
  67. package/test/data.integration.test.js +1281 -1206
  68. package/test/events.test.js +108 -10
  69. package/test/model.integration.test.js +377 -377
  70. package/test/user.test.js +106 -1
@@ -1,364 +1,363 @@
1
- // client/src/components/AssistantChat.jsx
2
-
3
- import React, { useState, useMemo, useEffect, useRef } from 'react';
4
- import { FaRobot, FaPaperPlane, FaTimes, FaExpand, FaCompress, FaPlus } from 'react-icons/fa';
5
- import './AssistantChat.scss';
6
- import { useModelContext } from "./contexts/ModelContext.jsx";
7
- import { Trans, useTranslation } from "react-i18next";
8
- import Markdown from 'react-markdown';
9
- import {useQueryClient} from "react-query";
10
- import {providers} from "../../src/modules/assistant/constants.js";
11
- import DashboardChart from "./DashboardChart.jsx";
12
- import FlexViewCard from "./FlexViewCard.jsx";
13
- import HtmlViewCard from "./HtmlViewCard.jsx";
14
- import {useUI} from "./contexts/UIContext.jsx";
15
- import Button from "./Button.jsx";
16
- import {getUserHash, getUserId} from "../../src/data.js";
17
- import {useNavigation} from "react-router";
18
- import {useAuthContext} from "./contexts/AuthContext.jsx";
19
- import {useNavigate} from "react-router-dom";
20
- import {DataTable} from "./DataTable.jsx";
21
-
22
- const AssistantChat = ({ config }) => {
23
- const { selectedModel, models } = useModelContext();
24
- const { me } = useAuthContext();
25
- const nav = useNavigate();
26
- const { t } = useTranslation();
27
- const [isOpen, setIsOpen] = useState(false);
28
- const [isMaximized, setIsMaximized] = useState(false);
29
- const [messages, setMessages] = useState([
30
- { from: 'bot', text: t('assistant.welcome') }
31
- ]);
32
- const [input, setInput] = useState('');
33
- const [isLoading, setIsLoading] = useState(false);
34
- const { setChartToAdd, setFlexViewToAdd, setHtmlViewToAdd } = useUI();
35
-
36
- // NOUVEL ÉTAT : Stocke une action en attente de confirmation de l'utilisateur
37
- const [pendingConfirmation, setPendingConfirmation] = useState(null);
38
-
39
- // NOUVEAU : Référence pour le défilement automatique
40
- const messagesEndRef = useRef(null);
41
-
42
- // Fonction pour défiler vers le bas
43
- const scrollToBottom = () => {
44
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
45
- };
46
-
47
- const queryClient = useQueryClient();
48
-
49
- // Défiler vers le bas à chaque nouveau message ou changement de statut de chargement
50
- useEffect(scrollToBottom, [messages, isLoading]);
51
-
52
- // Fonction centralisée pour appeler l'API de l'assistant
53
- const handleApiCall = async (payload) => {
54
- setIsLoading(true);
55
- // On efface toute confirmation en attente dès qu'une nouvelle action est lancée
56
- setPendingConfirmation(null);
57
- const isConfirmation = !!payload.confirmedAction;
58
- if (!isConfirmation) {
59
- setPendingConfirmation(null);
60
- }
61
- try {
62
- const response = await fetch('/api/assistant/chat', {
63
- method: 'POST',
64
- headers: { 'Content-Type': 'application/json' },
65
- body: JSON.stringify(payload)
66
- });
67
-
68
- if (!response.ok) {
69
- throw new Error(`Erreur HTTP: ${response.status}`);
70
- }
71
-
72
- const result = await response.json();
73
-
74
- if (result.success) {
75
- if (isConfirmation) {
76
- const modelToInvalidate = payload.confirmedAction.params.model;
77
- if (modelToInvalidate) {
78
- console.log(`[Assistant] Action on model '${modelToInvalidate}' succeeded. Invalidating cache.`);
79
- // Invalide toutes les requêtes qui commencent par ce tableau.
80
- // C'est la mode la plus simple et la plus sûre pour s'assurer
81
- // que toutes les vues de ces données (paginées ou non) sont rafraîchies.
82
- queryClient.invalidateQueries(['api/data', modelToInvalidate]);
83
- }
84
- }
85
-
86
- // On crée un objet de message pour le bot qui peut contenir plus que du texte
87
- const botMessage = {
88
- from: 'bot',
89
- text: null,
90
- actionDetails: null, // Pour stocker les détails de l'action à afficher
91
- chartConfig: null,
92
- flexViewConfig: null,
93
- htmlViewConfig: null
94
- };
95
-
96
- // Gérer le texte à afficher
97
- if (result.displayMessage) {
98
- botMessage.text = result.displayMessage;
99
- } else if (result.codeMessage) {
100
- const code = typeof result.codeMessage === 'object'
101
- ? JSON.stringify(result.codeMessage, null, 2)
102
- : result.codeMessage;
103
- botMessage.text = `\`\`\`json\n${code}\n\`\`\``;
104
- }
105
-
106
- // NOUVEAU : Si une confirmation est demandée, on enrichit le message
107
- if (result.confirmationRequest) {
108
- // On stocke l'action complète dans l'état pour l'envoyer si l'utilisateur confirme
109
- setPendingConfirmation(result.confirmationRequest);
110
-
111
- // On ajoute les détails (modèle, filtre, données) au message pour l'affichage
112
- botMessage.actionDetails = {
113
- model: result.model,
114
- filter: result.filter,
115
- data: result.data // Sera undefined si non présent, ce qui est correct
116
- };
117
- } else if (isConfirmation) {
118
- // Si c'était un appel de confirmation et qu'il n'y a pas de nouvelle demande, on vide l'état
119
- setPendingConfirmation(null);
120
- }
121
-
122
- // On ajoute le message à la liste uniquement s'il a du contenu textuel
123
- if (result.chartConfig) {
124
- botMessage.chartConfig = result.chartConfig;
125
- }
126
- if (result.flexViewConfig) {
127
- botMessage.flexViewConfig = result.flexViewConfig;
128
- }
129
- if (result.htmlViewConfig) {
130
- botMessage.htmlViewConfig = result.htmlViewConfig;
131
- }
132
- // Si des données tabulaires sont retournées
133
- if (result.dataResult) {
134
- botMessage.dataResult = result.dataResult;
135
- }
136
- // On ajoute le message à la liste uniquement s'il a du contenu
137
- if (botMessage.text || botMessage.chartConfig || botMessage.dataResult || botMessage.flexViewConfig || botMessage.htmlViewConfig) {
138
- setMessages(prev => [...prev, botMessage]);
139
- }
140
-
141
- } else {
142
- const errorMessage = { from: 'bot', text: t('assistant.error', `Désolé, une erreur est survenue : {{message}}`, { message: result.message }) };
143
- setMessages(prev => [...prev, errorMessage]);
144
- }
145
-
146
- } catch (error) {
147
- const errorMessage = { from: 'bot', text: t('assistant.contactError', `Désolé, impossible de contacter l'assistant. ({{message}})`, { message: error.message }) };
148
- setMessages(prev => [...prev, errorMessage]);
149
- } finally {
150
- setIsLoading(false);
151
- }
152
- };
153
-
154
- // Gère la soumission du formulaire de chat
155
- const handleSubmit = async (e) => {
156
- e.preventDefault();
157
- // On ne soumet rien si le champ est vide, si une requête est en cours ou si une confirmation est attendue
158
- if (!input.trim() || isLoading || pendingConfirmation) return;
159
-
160
- const userMessage = { from: 'user', text: input };
161
- const currentInput = input;
162
-
163
- setMessages(prev => [...prev, userMessage]);
164
- setInput('');
165
-
166
- gtag('event', "Assistant Prior - msg");
167
-
168
- await handleApiCall({
169
- message: currentInput,
170
- history: messages,
171
- provider: selectedProvider,
172
- context: { modelName: selectedModel?.name }
173
- });
174
- };
175
-
176
- // NOUVELLE FONCTION : Gère la réponse de l'utilisateur à une demande de confirmation
177
- const handleConfirmAction = async (isConfirmed) => {
178
- if (!pendingConfirmation) return;
179
-
180
- if (isConfirmed) {
181
- // L'utilisateur a cliqué sur "Oui"
182
- const confirmationMessage = { from: 'user', text: t('yes', 'Oui') };
183
- setMessages(prev => [...prev, confirmationMessage]);
184
-
185
- // On rappelle l'API en envoyant l'action à confirmer
186
- await handleApiCall({
187
- message: "Action confirmée par l'utilisateur.",
188
- history: messages,
189
- provider: selectedProvider,
190
- context: { modelName: selectedModel?.name },
191
- confirmedAction: pendingConfirmation
192
- });
193
- } else {
194
- // L'utilisateur a cliqué sur "Non"
195
- const cancelMessage = { from: 'user', text: t('no', 'Non') };
196
- const botResponseMessage = { from: 'bot', text: t('assistant.actionCancelled', "Action annulée.") };
197
- setMessages(prev => [...prev, cancelMessage, botResponseMessage]);
198
- setPendingConfirmation(null); // On annule la demande de confirmation
199
- }
200
- };
201
-
202
- // Logique pour le sélecteur de fournisseur (OpenAI/Google)
203
- const availableProviders = useMemo(() => {
204
- const prs = Object.keys(providers).map(p => {
205
- return config?.[p] ? ({value: p, label: p}) : null;
206
- })
207
- return prs.filter(Boolean);
208
- }, [config]);
209
-
210
- const [selectedProvider, setSelectedProvider] = useState(null);
211
-
212
- useEffect(() => {
213
- if (availableProviders.length > 0 && !selectedProvider) {
214
- setSelectedProvider(availableProviders[0].value);
215
- }
216
- }, [availableProviders, selectedProvider]);
217
-
218
- // Si le chat est fermé, on affiche juste le bouton flottant
219
- if (!isOpen) {
220
- return (
221
- <button data-tooltip-id={"tooltipField"} data-tooltip-html={t("assistant.tooltip", "<b>Prior</b>, votre assistant en gestion des données")} className="fab assistant-fab" onClick={() => setIsOpen(true)} title={t('assistant.open', "Ouvrir l'assistant Prior")}>
222
- <FaRobot />
223
- </button>
224
- );
225
- }
226
-
227
- // Si le chat est ouvert, on affiche la fenêtre complète
228
- return (
229
- <div className={`assistant-chat-window ${isMaximized ? 'maximized' : ''}`}>
230
- <div className="chat-header">
231
- <h3><FaRobot style={{ marginRight: '8px' }} /> <Trans i18nKey="assistant.named" values={{ named: 'Prior' }} /></h3>
232
- <div className="header-actions">
233
- {availableProviders.length > 1 && (
234
- <select
235
- className="provider-selector"
236
- value={selectedProvider}
237
- onChange={(e) => setSelectedProvider(e.target.value)}
238
- >
239
- {availableProviders.map(provider => (
240
- <option key={provider.value} value={provider.value}>
241
- {provider.label}
242
- </option>
243
- ))}
244
- </select>
245
- )}
246
- <button onClick={() => setIsMaximized(!isMaximized)} title={isMaximized ? t('collapse', "Réduire") : t('expand', "Agrandir")}>
247
- {isMaximized ? <FaCompress /> : <FaExpand />}
248
- </button>
249
- <button onClick={() => setIsOpen(false)} title={t('close', "Fermer")}>
250
- <FaTimes />
251
- </button>
252
- </div>
253
- </div>
254
-
255
- <div className="chat-messages">
256
- {messages.map((msg, index) => (
257
- <div key={index} className={`message ${msg.from}`}>
258
- {msg.text && <Markdown>{msg.text}</Markdown>}
259
- {msg.chartConfig && (
260
- <div className="chart-container">
261
- <DashboardChart config={msg.chartConfig} />
262
- <div className="chart-actions" style={{ marginTop: '8px', textAlign: 'right' }}>
263
- <Button onClick={() => {
264
- nav('/user/'+getUserHash(me)+'/dashboards');
265
- setChartToAdd(msg.chartConfig);
266
- }} title={t('assistant.addToDashboard', 'Ajouter au tableau de bord')}>
267
- <FaPlus />
268
- <span style={{ marginLeft: '8px' }}>
269
- {t('assistant.addToDashboard', 'Ajouter au tableau de bord')}
270
- </span>
271
- </Button>
272
- </div>
273
- </div>
274
- )}
275
-
276
- {/* NOUVEAU : Affichage de la Flex View */}
277
- {msg.flexViewConfig && (
278
- <div className="flex-view-container">
279
- <FlexViewCard config={msg.flexViewConfig} />
280
- <div className="chart-actions" style={{ marginTop: '8px', textAlign: 'right' }}>
281
- <Button onClick={() => {
282
- nav('/user/'+getUserHash(me)+'/dashboards');
283
- setFlexViewToAdd(msg.flexViewConfig);
284
- }} title={t('assistant.addToDashboard', 'Ajouter au tableau de bord')}>
285
- <FaPlus />
286
- </Button>
287
- </div>
288
- </div>
289
- )}
290
-
291
- {/* NOUVEAU : Affichage de la vue HTML personnalisée */}
292
- {msg.htmlViewConfig && (
293
- <div className="html-view-container">
294
- <HtmlViewCard config={msg.htmlViewConfig} />
295
- <div className="chart-actions" style={{ marginTop: '8px', textAlign: 'right' }}>
296
- <Button onClick={() => {
297
- nav('/user/'+getUserHash(me)+'/dashboards');
298
- setHtmlViewToAdd(msg.htmlViewConfig);
299
- }} title={t('assistant.addToDashboard', 'Ajouter au tableau de bord')}>
300
- <FaPlus />
301
- </Button>
302
- </div>
303
- </div>
304
- )}
305
-
306
- {/* NOUVEAU : Affichage des données tabulaires */}
307
- {msg.dataResult && (
308
- <div className="data-table-container">
309
- <DataTable model={models.find(f => f.name === msg.dataResult.model)} advanced={false} data={msg.dataResult.data} />
310
- </div>
311
- )}
312
- {msg.actionDetails && (
313
- <div className="action-details">
314
- {msg.actionDetails.model && (
315
- <p><strong>{t('model', 'Modèle')}:</strong> <code>{msg.actionDetails.model}</code></p>
316
- )}
317
- {msg.actionDetails.filter && (
318
- <>
319
- <p><strong>{t('filter', 'Filtre')}:</strong></p>
320
- {/* Utiliser Markdown pour afficher un bloc de code JSON formaté */}
321
- <Markdown>{`\`\`\`json\n${JSON.stringify(msg.actionDetails.filter, null, 2)}\n\`\`\``}</Markdown>
322
- </>
323
- )}
324
- {msg.actionDetails.data && (
325
- <>
326
- <p><strong>{t('data', 'Données')}:</strong></p>
327
- <Markdown>{`\`\`\`json\n${JSON.stringify(msg.actionDetails.data, null, 2)}\n\`\`\``}</Markdown>
328
- </>
329
- )}
330
- </div>
331
- )}
332
- </div>
333
- ))}
334
- {isLoading && <div className="message bot"><p>...</p></div>}
335
-
336
- {/* NOUVEAU : Affichage des boutons de confirmation */}
337
- {pendingConfirmation && !isLoading && (
338
- <div className="message bot confirmation-prompt">
339
- <button onClick={() => handleConfirmAction(true)}>{t('yes', 'Oui')}</button>
340
- <button onClick={() => handleConfirmAction(false)} className="cancel">{t('no', 'Non')}</button>
341
- </div>
342
- )}
343
- <div ref={messagesEndRef} />
344
- </div>
345
-
346
- <form className="chat-input-form" onSubmit={handleSubmit}>
347
- <input
348
- type="text"
349
- value={input}
350
- onChange={(e) => setInput(e.target.value)}
351
- placeholder={t('assistant.type', "Écrivez votre message...")}
352
- // On désactive l'input si une requête est en cours ou si une confirmation est attendue
353
- disabled={isLoading || !!pendingConfirmation}
354
- autoFocus
355
- />
356
- <button type="submit" disabled={isLoading || !input.trim() || !!pendingConfirmation} title={t('send', "Envoyer")}>
357
- <FaPaperPlane />
358
- </button>
359
- </form>
360
- </div>
361
- );
362
- };
363
-
1
+ // client/src/components/AssistantChat.jsx
2
+
3
+ import React, { useState, useMemo, useEffect, useRef } from 'react';
4
+ import { FaRobot, FaPaperPlane, FaTimes, FaExpand, FaCompress, FaPlus } from 'react-icons/fa';
5
+ import './AssistantChat.scss';
6
+ import { useModelContext } from "./contexts/ModelContext.jsx";
7
+ import { Trans, useTranslation } from "react-i18next";
8
+ import Markdown from 'react-markdown';
9
+ import {useQueryClient} from "react-query";
10
+ import {providers} from "../../src/modules/assistant/constants.js";
11
+ import DashboardChart from "./DashboardChart.jsx";
12
+ import FlexViewCard from "./FlexViewCard.jsx";
13
+ import HtmlViewCard from "./HtmlViewCard.jsx";
14
+ import {useUI} from "./contexts/UIContext.jsx";
15
+ import Button from "./Button.jsx";
16
+ import {getUserHash, getUserId} from "../../src/data.js";
17
+ import {useAuthContext} from "./contexts/AuthContext.jsx";
18
+ import {useNavigate} from "react-router-dom";
19
+ import {DataTable} from "./DataTable.jsx";
20
+
21
+ const AssistantChat = ({ config }) => {
22
+ const { selectedModel, models } = useModelContext();
23
+ const { me } = useAuthContext();
24
+ const nav = useNavigate();
25
+ const { t } = useTranslation();
26
+ const [isOpen, setIsOpen] = useState(false);
27
+ const [isMaximized, setIsMaximized] = useState(false);
28
+ const [messages, setMessages] = useState([
29
+ { from: 'bot', text: t('assistant.welcome') }
30
+ ]);
31
+ const [input, setInput] = useState('');
32
+ const [isLoading, setIsLoading] = useState(false);
33
+ const { setChartToAdd, setFlexViewToAdd, setHtmlViewToAdd } = useUI();
34
+
35
+ // NOUVEL ÉTAT : Stocke une action en attente de confirmation de l'utilisateur
36
+ const [pendingConfirmation, setPendingConfirmation] = useState(null);
37
+
38
+ // NOUVEAU : Référence pour le défilement automatique
39
+ const messagesEndRef = useRef(null);
40
+
41
+ // Fonction pour défiler vers le bas
42
+ const scrollToBottom = () => {
43
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
44
+ };
45
+
46
+ const queryClient = useQueryClient();
47
+
48
+ // Défiler vers le bas à chaque nouveau message ou changement de statut de chargement
49
+ useEffect(scrollToBottom, [messages, isLoading]);
50
+
51
+ // Fonction centralisée pour appeler l'API de l'assistant
52
+ const handleApiCall = async (payload) => {
53
+ setIsLoading(true);
54
+ // On efface toute confirmation en attente dès qu'une nouvelle action est lancée
55
+ setPendingConfirmation(null);
56
+ const isConfirmation = !!payload.confirmedAction;
57
+ if (!isConfirmation) {
58
+ setPendingConfirmation(null);
59
+ }
60
+ try {
61
+ const response = await fetch('/api/assistant/chat', {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/json' },
64
+ body: JSON.stringify(payload)
65
+ });
66
+
67
+ if (!response.ok) {
68
+ throw new Error(`Erreur HTTP: ${response.status}`);
69
+ }
70
+
71
+ const result = await response.json();
72
+
73
+ if (result.success) {
74
+ if (isConfirmation) {
75
+ const modelToInvalidate = payload.confirmedAction.params.model;
76
+ if (modelToInvalidate) {
77
+ console.log(`[Assistant] Action on model '${modelToInvalidate}' succeeded. Invalidating cache.`);
78
+ // Invalide toutes les requêtes qui commencent par ce tableau.
79
+ // C'est la mode la plus simple et la plus sûre pour s'assurer
80
+ // que toutes les vues de ces données (paginées ou non) sont rafraîchies.
81
+ queryClient.invalidateQueries(['api/data', modelToInvalidate]);
82
+ }
83
+ }
84
+
85
+ // On crée un objet de message pour le bot qui peut contenir plus que du texte
86
+ const botMessage = {
87
+ from: 'bot',
88
+ text: null,
89
+ actionDetails: null, // Pour stocker les détails de l'action à afficher
90
+ chartConfig: null,
91
+ flexViewConfig: null,
92
+ htmlViewConfig: null
93
+ };
94
+
95
+ // Gérer le texte à afficher
96
+ if (result.displayMessage) {
97
+ botMessage.text = result.displayMessage;
98
+ } else if (result.codeMessage) {
99
+ const code = typeof result.codeMessage === 'object'
100
+ ? JSON.stringify(result.codeMessage, null, 2)
101
+ : result.codeMessage;
102
+ botMessage.text = `\`\`\`json\n${code}\n\`\`\``;
103
+ }
104
+
105
+ // NOUVEAU : Si une confirmation est demandée, on enrichit le message
106
+ if (result.confirmationRequest) {
107
+ // On stocke l'action complète dans l'état pour l'envoyer si l'utilisateur confirme
108
+ setPendingConfirmation(result.confirmationRequest);
109
+
110
+ // On ajoute les détails (modèle, filtre, données) au message pour l'affichage
111
+ botMessage.actionDetails = {
112
+ model: result.model,
113
+ filter: result.filter,
114
+ data: result.data // Sera undefined si non présent, ce qui est correct
115
+ };
116
+ } else if (isConfirmation) {
117
+ // Si c'était un appel de confirmation et qu'il n'y a pas de nouvelle demande, on vide l'état
118
+ setPendingConfirmation(null);
119
+ }
120
+
121
+ // On ajoute le message à la liste uniquement s'il a du contenu textuel
122
+ if (result.chartConfig) {
123
+ botMessage.chartConfig = result.chartConfig;
124
+ }
125
+ if (result.flexViewConfig) {
126
+ botMessage.flexViewConfig = result.flexViewConfig;
127
+ }
128
+ if (result.htmlViewConfig) {
129
+ botMessage.htmlViewConfig = result.htmlViewConfig;
130
+ }
131
+ // Si des données tabulaires sont retournées
132
+ if (result.dataResult) {
133
+ botMessage.dataResult = result.dataResult;
134
+ }
135
+ // On ajoute le message à la liste uniquement s'il a du contenu
136
+ if (botMessage.text || botMessage.chartConfig || botMessage.dataResult || botMessage.flexViewConfig || botMessage.htmlViewConfig) {
137
+ setMessages(prev => [...prev, botMessage]);
138
+ }
139
+
140
+ } else {
141
+ const errorMessage = { from: 'bot', text: t('assistant.error', `Désolé, une erreur est survenue : {{message}}`, { message: result.message }) };
142
+ setMessages(prev => [...prev, errorMessage]);
143
+ }
144
+
145
+ } catch (error) {
146
+ const errorMessage = { from: 'bot', text: t('assistant.contactError', `Désolé, impossible de contacter l'assistant. ({{message}})`, { message: error.message }) };
147
+ setMessages(prev => [...prev, errorMessage]);
148
+ } finally {
149
+ setIsLoading(false);
150
+ }
151
+ };
152
+
153
+ // Gère la soumission du formulaire de chat
154
+ const handleSubmit = async (e) => {
155
+ e.preventDefault();
156
+ // On ne soumet rien si le champ est vide, si une requête est en cours ou si une confirmation est attendue
157
+ if (!input.trim() || isLoading || pendingConfirmation) return;
158
+
159
+ const userMessage = { from: 'user', text: input };
160
+ const currentInput = input;
161
+
162
+ setMessages(prev => [...prev, userMessage]);
163
+ setInput('');
164
+
165
+ gtag('event', "Assistant Prior - msg");
166
+
167
+ await handleApiCall({
168
+ message: currentInput,
169
+ history: messages,
170
+ provider: selectedProvider,
171
+ context: { modelName: selectedModel?.name }
172
+ });
173
+ };
174
+
175
+ // NOUVELLE FONCTION : Gère la réponse de l'utilisateur à une demande de confirmation
176
+ const handleConfirmAction = async (isConfirmed) => {
177
+ if (!pendingConfirmation) return;
178
+
179
+ if (isConfirmed) {
180
+ // L'utilisateur a cliqué sur "Oui"
181
+ const confirmationMessage = { from: 'user', text: t('yes', 'Oui') };
182
+ setMessages(prev => [...prev, confirmationMessage]);
183
+
184
+ // On rappelle l'API en envoyant l'action à confirmer
185
+ await handleApiCall({
186
+ message: "Action confirmée par l'utilisateur.",
187
+ history: messages,
188
+ provider: selectedProvider,
189
+ context: { modelName: selectedModel?.name },
190
+ confirmedAction: pendingConfirmation
191
+ });
192
+ } else {
193
+ // L'utilisateur a cliqué sur "Non"
194
+ const cancelMessage = { from: 'user', text: t('no', 'Non') };
195
+ const botResponseMessage = { from: 'bot', text: t('assistant.actionCancelled', "Action annulée.") };
196
+ setMessages(prev => [...prev, cancelMessage, botResponseMessage]);
197
+ setPendingConfirmation(null); // On annule la demande de confirmation
198
+ }
199
+ };
200
+
201
+ // Logique pour le sélecteur de fournisseur (OpenAI/Google)
202
+ const availableProviders = useMemo(() => {
203
+ const prs = Object.keys(providers).map(p => {
204
+ return config?.[p] ? ({value: p, label: p}) : null;
205
+ })
206
+ return prs.filter(Boolean);
207
+ }, [config]);
208
+
209
+ const [selectedProvider, setSelectedProvider] = useState(null);
210
+
211
+ useEffect(() => {
212
+ if (availableProviders.length > 0 && !selectedProvider) {
213
+ setSelectedProvider(availableProviders[0].value);
214
+ }
215
+ }, [availableProviders, selectedProvider]);
216
+
217
+ // Si le chat est fermé, on affiche juste le bouton flottant
218
+ if (!isOpen) {
219
+ return (
220
+ <button data-tooltip-id={"tooltipField"} data-tooltip-html={t("assistant.tooltip", "<b>Prior</b>, votre assistant en gestion des données")} className="fab assistant-fab" onClick={() => setIsOpen(true)} title={t('assistant.open', "Ouvrir l'assistant Prior")}>
221
+ <FaRobot />
222
+ </button>
223
+ );
224
+ }
225
+
226
+ // Si le chat est ouvert, on affiche la fenêtre complète
227
+ return (
228
+ <div className={`assistant-chat-window ${isMaximized ? 'maximized' : ''}`}>
229
+ <div className="chat-header">
230
+ <h3><FaRobot style={{ marginRight: '8px' }} /> <Trans i18nKey="assistant.named" values={{ named: 'Prior' }} /></h3>
231
+ <div className="header-actions">
232
+ {availableProviders.length > 1 && (
233
+ <select
234
+ className="provider-selector"
235
+ value={selectedProvider}
236
+ onChange={(e) => setSelectedProvider(e.target.value)}
237
+ >
238
+ {availableProviders.map(provider => (
239
+ <option key={provider.value} value={provider.value}>
240
+ {provider.label}
241
+ </option>
242
+ ))}
243
+ </select>
244
+ )}
245
+ <button onClick={() => setIsMaximized(!isMaximized)} title={isMaximized ? t('collapse', "Réduire") : t('expand', "Agrandir")}>
246
+ {isMaximized ? <FaCompress /> : <FaExpand />}
247
+ </button>
248
+ <button onClick={() => setIsOpen(false)} title={t('close', "Fermer")}>
249
+ <FaTimes />
250
+ </button>
251
+ </div>
252
+ </div>
253
+
254
+ <div className="chat-messages">
255
+ {messages.map((msg, index) => (
256
+ <div key={index} className={`message ${msg.from}`}>
257
+ {msg.text && <Markdown>{msg.text}</Markdown>}
258
+ {msg.chartConfig && (
259
+ <div className="chart-container">
260
+ <DashboardChart config={msg.chartConfig} />
261
+ <div className="chart-actions" style={{ marginTop: '8px', textAlign: 'right' }}>
262
+ <Button onClick={() => {
263
+ nav('/user/'+getUserHash(me)+'/dashboards');
264
+ setChartToAdd(msg.chartConfig);
265
+ }} title={t('assistant.addToDashboard', 'Ajouter au tableau de bord')}>
266
+ <FaPlus />
267
+ <span style={{ marginLeft: '8px' }}>
268
+ {t('assistant.addToDashboard', 'Ajouter au tableau de bord')}
269
+ </span>
270
+ </Button>
271
+ </div>
272
+ </div>
273
+ )}
274
+
275
+ {/* NOUVEAU : Affichage de la Flex View */}
276
+ {msg.flexViewConfig && (
277
+ <div className="flex-view-container">
278
+ <FlexViewCard config={msg.flexViewConfig} />
279
+ <div className="chart-actions" style={{ marginTop: '8px', textAlign: 'right' }}>
280
+ <Button onClick={() => {
281
+ nav('/user/'+getUserHash(me)+'/dashboards');
282
+ setFlexViewToAdd(msg.flexViewConfig);
283
+ }} title={t('assistant.addToDashboard', 'Ajouter au tableau de bord')}>
284
+ <FaPlus />
285
+ </Button>
286
+ </div>
287
+ </div>
288
+ )}
289
+
290
+ {/* NOUVEAU : Affichage de la vue HTML personnalisée */}
291
+ {msg.htmlViewConfig && (
292
+ <div className="html-view-container">
293
+ <HtmlViewCard config={msg.htmlViewConfig} />
294
+ <div className="chart-actions" style={{ marginTop: '8px', textAlign: 'right' }}>
295
+ <Button onClick={() => {
296
+ nav('/user/'+getUserHash(me)+'/dashboards');
297
+ setHtmlViewToAdd(msg.htmlViewConfig);
298
+ }} title={t('assistant.addToDashboard', 'Ajouter au tableau de bord')}>
299
+ <FaPlus />
300
+ </Button>
301
+ </div>
302
+ </div>
303
+ )}
304
+
305
+ {/* NOUVEAU : Affichage des données tabulaires */}
306
+ {msg.dataResult && (
307
+ <div className="data-table-container">
308
+ <DataTable model={models.find(f => f.name === msg.dataResult.model)} advanced={false} data={msg.dataResult.data} />
309
+ </div>
310
+ )}
311
+ {msg.actionDetails && (
312
+ <div className="action-details">
313
+ {msg.actionDetails.model && (
314
+ <p><strong>{t('model', 'Modèle')}:</strong> <code>{msg.actionDetails.model}</code></p>
315
+ )}
316
+ {msg.actionDetails.filter && (
317
+ <>
318
+ <p><strong>{t('filter', 'Filtre')}:</strong></p>
319
+ {/* Utiliser Markdown pour afficher un bloc de code JSON formaté */}
320
+ <Markdown>{`\`\`\`json\n${JSON.stringify(msg.actionDetails.filter, null, 2)}\n\`\`\``}</Markdown>
321
+ </>
322
+ )}
323
+ {msg.actionDetails.data && (
324
+ <>
325
+ <p><strong>{t('data', 'Données')}:</strong></p>
326
+ <Markdown>{`\`\`\`json\n${JSON.stringify(msg.actionDetails.data, null, 2)}\n\`\`\``}</Markdown>
327
+ </>
328
+ )}
329
+ </div>
330
+ )}
331
+ </div>
332
+ ))}
333
+ {isLoading && <div className="message bot"><p>...</p></div>}
334
+
335
+ {/* NOUVEAU : Affichage des boutons de confirmation */}
336
+ {pendingConfirmation && !isLoading && (
337
+ <div className="message bot confirmation-prompt">
338
+ <button onClick={() => handleConfirmAction(true)}>{t('yes', 'Oui')}</button>
339
+ <button onClick={() => handleConfirmAction(false)} className="cancel">{t('no', 'Non')}</button>
340
+ </div>
341
+ )}
342
+ <div ref={messagesEndRef} />
343
+ </div>
344
+
345
+ <form className="chat-input-form" onSubmit={handleSubmit}>
346
+ <input
347
+ type="text"
348
+ value={input}
349
+ onChange={(e) => setInput(e.target.value)}
350
+ placeholder={t('assistant.type', "Écrivez votre message...")}
351
+ // On désactive l'input si une requête est en cours ou si une confirmation est attendue
352
+ disabled={isLoading || !!pendingConfirmation}
353
+ autoFocus
354
+ />
355
+ <button type="submit" disabled={isLoading || !input.trim() || !!pendingConfirmation} title={t('send', "Envoyer")}>
356
+ <FaPaperPlane />
357
+ </button>
358
+ </form>
359
+ </div>
360
+ );
361
+ };
362
+
364
363
  export default AssistantChat;