data-primals-engine 1.6.5 → 1.7.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.
- package/README.md +160 -113
- package/client/index.js +3 -0
- package/client/package-lock.json +8121 -8824
- package/client/package.json +10 -3
- package/client/src/AssistantChat.jsx +369 -362
- package/client/src/DataEditor.jsx +383 -383
- package/client/src/DataLayout.jsx +54 -20
- package/client/src/ModelList.jsx +280 -280
- package/client/src/ViewSwitcher.scss +0 -31
- package/client/src/constants.js +81 -100
- package/client/src/contexts/CommandContext.jsx +274 -259
- package/client/vite.config.js +30 -30
- package/doc/AI-assistance.md +93 -0
- package/doc/Advanced-workflows.md +90 -0
- package/doc/Event-system.md +79 -0
- package/doc/Packs-gallery.md +73 -0
- package/package.json +30 -16
- package/src/constants.js +1 -1
- package/src/core.js +487 -477
- package/src/defaultModels.js +1 -1
- package/src/email.js +0 -2
- package/src/engine.js +342 -335
- package/src/filter.js +348 -343
- package/src/migrate.js +1 -1
- package/src/modules/assistant/assistant.js +30 -20
- package/src/modules/data/data.backup.js +4 -4
- package/src/modules/data/data.js +311 -302
- package/src/modules/data/data.operations.js +79 -64
- package/src/modules/data/data.relations.js +2 -1
- package/src/modules/data/data.scheduling.js +1 -1
- package/src/modules/mongodb.js +16 -8
- package/src/modules/user.js +0 -1
- package/src/modules/workflow.js +1828 -1815
- package/src/packs.js +5701 -5697
- package/src/profiles.js +19 -0
- package/swagger-en.yml +3390 -3385
- package/swagger-fr.yml +3385 -3380
- package/test/assistant.test.js +2 -2
- package/test/data.backup.integration.test.js +3 -1
- package/test/data.history.integration.test.js +0 -1
|
@@ -1,363 +1,370 @@
|
|
|
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
|
-
//
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
</div>
|
|
310
|
-
)}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
)}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
)}
|
|
323
|
-
{msg.actionDetails.
|
|
324
|
-
<>
|
|
325
|
-
<p><strong>{t('
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
+
// Fonction pour créer un message bot à partir d'un résultat
|
|
86
|
+
const createBotMessage = (res) => {
|
|
87
|
+
const msg = {
|
|
88
|
+
from: 'bot',
|
|
89
|
+
text: null,
|
|
90
|
+
actionDetails: null,
|
|
91
|
+
chartConfig: null,
|
|
92
|
+
flexViewConfig: null,
|
|
93
|
+
htmlViewConfig: null,
|
|
94
|
+
dataResult: null
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (res.displayMessage) {
|
|
98
|
+
msg.text = res.displayMessage;
|
|
99
|
+
} else if (res.codeMessage) {
|
|
100
|
+
const code = typeof res.codeMessage === 'object'
|
|
101
|
+
? JSON.stringify(res.codeMessage, null, 2)
|
|
102
|
+
: res.codeMessage;
|
|
103
|
+
msg.text = `\`\`\`json\n${code}\n\`\`\``;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (res.confirmationRequest) {
|
|
107
|
+
msg.actionDetails = {
|
|
108
|
+
model: res.model,
|
|
109
|
+
filter: res.filter,
|
|
110
|
+
data: res.data
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (res.chartConfig) msg.chartConfig = res.chartConfig;
|
|
115
|
+
if (res.flexViewConfig) msg.flexViewConfig = res.flexViewConfig;
|
|
116
|
+
if (res.htmlViewConfig) msg.htmlViewConfig = res.htmlViewConfig;
|
|
117
|
+
if (res.dataResult) msg.dataResult = res.dataResult;
|
|
118
|
+
|
|
119
|
+
return msg;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const resultsToProcess = result.results || [result];
|
|
123
|
+
const newMessages = [];
|
|
124
|
+
let newConfirmation = null;
|
|
125
|
+
|
|
126
|
+
resultsToProcess.forEach(res => {
|
|
127
|
+
const msg = createBotMessage(res);
|
|
128
|
+
if (res.confirmationRequest) {
|
|
129
|
+
newConfirmation = res.confirmationRequest;
|
|
130
|
+
}
|
|
131
|
+
// On ajoute le message s'il a du contenu
|
|
132
|
+
if (msg.text || msg.chartConfig || msg.dataResult || msg.flexViewConfig || msg.htmlViewConfig || msg.actionDetails) {
|
|
133
|
+
newMessages.push(msg);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (newConfirmation) {
|
|
138
|
+
setPendingConfirmation(newConfirmation);
|
|
139
|
+
} else if (isConfirmation) {
|
|
140
|
+
setPendingConfirmation(null);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (newMessages.length > 0) {
|
|
144
|
+
setMessages(prev => [...prev, ...newMessages]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
} else {
|
|
148
|
+
const errorMessage = { from: 'bot', text: t('assistant.error', `Désolé, une erreur est survenue : {{message}}`, { message: result.message }) };
|
|
149
|
+
setMessages(prev => [...prev, errorMessage]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const errorMessage = { from: 'bot', text: t('assistant.contactError', `Désolé, impossible de contacter l'assistant. ({{message}})`, { message: error.message }) };
|
|
154
|
+
setMessages(prev => [...prev, errorMessage]);
|
|
155
|
+
} finally {
|
|
156
|
+
setIsLoading(false);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Gère la soumission du formulaire de chat
|
|
161
|
+
const handleSubmit = async (e) => {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
// On ne soumet rien si le champ est vide, si une requête est en cours ou si une confirmation est attendue
|
|
164
|
+
if (!input.trim() || isLoading || pendingConfirmation) return;
|
|
165
|
+
|
|
166
|
+
const userMessage = { from: 'user', text: input };
|
|
167
|
+
const currentInput = input;
|
|
168
|
+
|
|
169
|
+
setMessages(prev => [...prev, userMessage]);
|
|
170
|
+
setInput('');
|
|
171
|
+
|
|
172
|
+
gtag('event', "Assistant Prior - msg");
|
|
173
|
+
|
|
174
|
+
await handleApiCall({
|
|
175
|
+
message: currentInput,
|
|
176
|
+
history: messages,
|
|
177
|
+
provider: selectedProvider,
|
|
178
|
+
context: { modelName: selectedModel?.name }
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// NOUVELLE FONCTION : Gère la réponse de l'utilisateur à une demande de confirmation
|
|
183
|
+
const handleConfirmAction = async (isConfirmed) => {
|
|
184
|
+
if (!pendingConfirmation) return;
|
|
185
|
+
|
|
186
|
+
if (isConfirmed) {
|
|
187
|
+
// L'utilisateur a cliqué sur "Oui"
|
|
188
|
+
const confirmationMessage = { from: 'user', text: t('yes', 'Oui') };
|
|
189
|
+
setMessages(prev => [...prev, confirmationMessage]);
|
|
190
|
+
|
|
191
|
+
// On rappelle l'API en envoyant l'action à confirmer
|
|
192
|
+
await handleApiCall({
|
|
193
|
+
message: "Action confirmée par l'utilisateur.",
|
|
194
|
+
history: messages,
|
|
195
|
+
provider: selectedProvider,
|
|
196
|
+
context: { modelName: selectedModel?.name },
|
|
197
|
+
confirmedAction: pendingConfirmation
|
|
198
|
+
});
|
|
199
|
+
} else {
|
|
200
|
+
// L'utilisateur a cliqué sur "Non"
|
|
201
|
+
const cancelMessage = { from: 'user', text: t('no', 'Non') };
|
|
202
|
+
const botResponseMessage = { from: 'bot', text: t('assistant.actionCancelled', "Action annulée.") };
|
|
203
|
+
setMessages(prev => [...prev, cancelMessage, botResponseMessage]);
|
|
204
|
+
setPendingConfirmation(null); // On annule la demande de confirmation
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Logique pour le sélecteur de fournisseur (OpenAI/Google)
|
|
209
|
+
const availableProviders = useMemo(() => {
|
|
210
|
+
const prs = Object.keys(providers).map(p => {
|
|
211
|
+
return config?.[p] ? ({value: p, label: p}) : null;
|
|
212
|
+
})
|
|
213
|
+
return prs.filter(Boolean);
|
|
214
|
+
}, [config]);
|
|
215
|
+
|
|
216
|
+
const [selectedProvider, setSelectedProvider] = useState(null);
|
|
217
|
+
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (availableProviders.length > 0 && !selectedProvider) {
|
|
220
|
+
setSelectedProvider(availableProviders[0].value);
|
|
221
|
+
}
|
|
222
|
+
}, [availableProviders, selectedProvider]);
|
|
223
|
+
|
|
224
|
+
// Si le chat est fermé, on affiche juste le bouton flottant
|
|
225
|
+
if (!isOpen) {
|
|
226
|
+
return (
|
|
227
|
+
<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")}>
|
|
228
|
+
<FaRobot />
|
|
229
|
+
</button>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Si le chat est ouvert, on affiche la fenêtre complète
|
|
234
|
+
return (
|
|
235
|
+
<div className={`assistant-chat-window ${isMaximized ? 'maximized' : ''}`}>
|
|
236
|
+
<div className="chat-header">
|
|
237
|
+
<h3><FaRobot style={{ marginRight: '8px' }} /> <Trans i18nKey="assistant.named" values={{ named: 'Prior' }} /></h3>
|
|
238
|
+
<div className="header-actions">
|
|
239
|
+
{availableProviders.length > 1 && (
|
|
240
|
+
<select
|
|
241
|
+
className="provider-selector"
|
|
242
|
+
value={selectedProvider}
|
|
243
|
+
onChange={(e) => setSelectedProvider(e.target.value)}
|
|
244
|
+
>
|
|
245
|
+
{availableProviders.map(provider => (
|
|
246
|
+
<option key={provider.value} value={provider.value}>
|
|
247
|
+
{provider.label}
|
|
248
|
+
</option>
|
|
249
|
+
))}
|
|
250
|
+
</select>
|
|
251
|
+
)}
|
|
252
|
+
<button onClick={() => setIsMaximized(!isMaximized)} title={isMaximized ? t('collapse', "Réduire") : t('expand', "Agrandir")}>
|
|
253
|
+
{isMaximized ? <FaCompress /> : <FaExpand />}
|
|
254
|
+
</button>
|
|
255
|
+
<button onClick={() => setIsOpen(false)} title={t('close', "Fermer")}>
|
|
256
|
+
<FaTimes />
|
|
257
|
+
</button>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div className="chat-messages">
|
|
262
|
+
{messages.map((msg, index) => (
|
|
263
|
+
<div key={index} className={`message ${msg.from}`}>
|
|
264
|
+
{msg.text && <Markdown>{msg.text}</Markdown>}
|
|
265
|
+
{msg.chartConfig && (
|
|
266
|
+
<div className="chart-container">
|
|
267
|
+
<DashboardChart config={msg.chartConfig} />
|
|
268
|
+
<div className="chart-actions" style={{ marginTop: '8px', textAlign: 'right' }}>
|
|
269
|
+
<Button onClick={() => {
|
|
270
|
+
nav('/user/'+getUserHash(me)+'/dashboards');
|
|
271
|
+
setChartToAdd(msg.chartConfig);
|
|
272
|
+
}} title={t('assistant.addToDashboard', 'Ajouter au tableau de bord')}>
|
|
273
|
+
<FaPlus />
|
|
274
|
+
<span style={{ marginLeft: '8px' }}>
|
|
275
|
+
{t('assistant.addToDashboard', 'Ajouter au tableau de bord')}
|
|
276
|
+
</span>
|
|
277
|
+
</Button>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
|
|
282
|
+
{/* NOUVEAU : Affichage de la Flex View */}
|
|
283
|
+
{msg.flexViewConfig && (
|
|
284
|
+
<div className="flex-view-container">
|
|
285
|
+
<FlexViewCard config={msg.flexViewConfig} />
|
|
286
|
+
<div className="chart-actions" style={{ marginTop: '8px', textAlign: 'right' }}>
|
|
287
|
+
<Button onClick={() => {
|
|
288
|
+
nav('/user/'+getUserHash(me)+'/dashboards');
|
|
289
|
+
setFlexViewToAdd(msg.flexViewConfig);
|
|
290
|
+
}} title={t('assistant.addToDashboard', 'Ajouter au tableau de bord')}>
|
|
291
|
+
<FaPlus />
|
|
292
|
+
</Button>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
|
|
297
|
+
{/* NOUVEAU : Affichage de la vue HTML personnalisée */}
|
|
298
|
+
{msg.htmlViewConfig && (
|
|
299
|
+
<div className="html-view-container">
|
|
300
|
+
<HtmlViewCard config={msg.htmlViewConfig} />
|
|
301
|
+
<div className="chart-actions" style={{ marginTop: '8px', textAlign: 'right' }}>
|
|
302
|
+
<Button onClick={() => {
|
|
303
|
+
nav('/user/'+getUserHash(me)+'/dashboards');
|
|
304
|
+
setHtmlViewToAdd(msg.htmlViewConfig);
|
|
305
|
+
}} title={t('assistant.addToDashboard', 'Ajouter au tableau de bord')}>
|
|
306
|
+
<FaPlus />
|
|
307
|
+
</Button>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
|
|
312
|
+
{/* NOUVEAU : Affichage des données tabulaires */}
|
|
313
|
+
{msg.dataResult && (
|
|
314
|
+
<div className="data-table-container">
|
|
315
|
+
<DataTable model={models.find(f => f.name === msg.dataResult.model)} advanced={false} data={msg.dataResult.data} />
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
{msg.actionDetails && (
|
|
319
|
+
<div className="action-details">
|
|
320
|
+
{msg.actionDetails.model && (
|
|
321
|
+
<p><strong>{t('model', 'Modèle')}:</strong> <code>{msg.actionDetails.model}</code></p>
|
|
322
|
+
)}
|
|
323
|
+
{msg.actionDetails.filter && (
|
|
324
|
+
<>
|
|
325
|
+
<p><strong>{t('filter', 'Filtre')}:</strong></p>
|
|
326
|
+
{/* Utiliser Markdown pour afficher un bloc de code JSON formaté */}
|
|
327
|
+
<Markdown>{`\`\`\`json\n${JSON.stringify(msg.actionDetails.filter, null, 2)}\n\`\`\``}</Markdown>
|
|
328
|
+
</>
|
|
329
|
+
)}
|
|
330
|
+
{msg.actionDetails.data && (
|
|
331
|
+
<>
|
|
332
|
+
<p><strong>{t('data', 'Données')}:</strong></p>
|
|
333
|
+
<Markdown>{`\`\`\`json\n${JSON.stringify(msg.actionDetails.data, null, 2)}\n\`\`\``}</Markdown>
|
|
334
|
+
</>
|
|
335
|
+
)}
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
))}
|
|
340
|
+
{isLoading && <div className="message bot"><p>...</p></div>}
|
|
341
|
+
|
|
342
|
+
{/* NOUVEAU : Affichage des boutons de confirmation */}
|
|
343
|
+
{pendingConfirmation && !isLoading && (
|
|
344
|
+
<div className="message bot confirmation-prompt">
|
|
345
|
+
<button onClick={() => handleConfirmAction(true)}>{t('yes', 'Oui')}</button>
|
|
346
|
+
<button onClick={() => handleConfirmAction(false)} className="cancel">{t('no', 'Non')}</button>
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
<div ref={messagesEndRef} />
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<form className="chat-input-form" onSubmit={handleSubmit}>
|
|
353
|
+
<input
|
|
354
|
+
type="text"
|
|
355
|
+
value={input}
|
|
356
|
+
onChange={(e) => setInput(e.target.value)}
|
|
357
|
+
placeholder={t('assistant.type', "Écrivez votre message...")}
|
|
358
|
+
// On désactive l'input si une requête est en cours ou si une confirmation est attendue
|
|
359
|
+
disabled={isLoading || !!pendingConfirmation}
|
|
360
|
+
autoFocus
|
|
361
|
+
/>
|
|
362
|
+
<button type="submit" disabled={isLoading || !input.trim() || !!pendingConfirmation} title={t('send', "Envoyer")}>
|
|
363
|
+
<FaPaperPlane />
|
|
364
|
+
</button>
|
|
365
|
+
</form>
|
|
366
|
+
</div>
|
|
367
|
+
);
|
|
368
|
+
};
|
|
369
|
+
|
|
363
370
|
export default AssistantChat;
|