data-primals-engine 1.6.5 → 1.7.0

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.
@@ -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
- // 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
-
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;