data-primals-engine 1.5.0 → 1.5.2

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