data-primals-engine 1.0.11 → 1.1.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.
Files changed (166) hide show
  1. package/client/README.md +8 -0
  2. package/client/index.html +31 -0
  3. package/client/index.js +25 -0
  4. package/client/package-lock.json +11664 -0
  5. package/client/package.json +70 -0
  6. package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
  7. package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
  8. package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
  9. package/client/public/doc/API.postman_collection.json +214 -0
  10. package/client/public/github.svg +1 -0
  11. package/client/public/profilCompany.jpg +0 -0
  12. package/client/public/profilDeveloper.jpg +0 -0
  13. package/client/public/profilEngineer.jpg +0 -0
  14. package/client/public/profilPersonal.jpg +0 -0
  15. package/client/public/profils .psd +0 -0
  16. package/client/public/react.svg +9 -0
  17. package/client/src/APIInfo.jsx +328 -0
  18. package/client/src/AddWidgetTypeModal.jsx +44 -0
  19. package/client/src/AddWidgetTypeModal.scss +87 -0
  20. package/client/src/App.css +42 -0
  21. package/client/src/App.jsx +586 -0
  22. package/client/src/App.scss +1466 -0
  23. package/client/src/AssistantChat.jsx +280 -0
  24. package/client/src/AssistantChat.scss +229 -0
  25. package/client/src/Button.jsx +12 -0
  26. package/client/src/Button.scss +197 -0
  27. package/client/src/CalculationBuilder.jsx +338 -0
  28. package/client/src/CalculationStepRow.jsx +199 -0
  29. package/client/src/CalendarConfigModal.jsx +49 -0
  30. package/client/src/ChartConfigModal.jsx +402 -0
  31. package/client/src/ConditionBuilder.jsx +622 -0
  32. package/client/src/ConditionBuilder.scss +672 -0
  33. package/client/src/ConditionBuilder2.jsx +757 -0
  34. package/client/src/ContentView.jsx +274 -0
  35. package/client/src/CronBuilder.jsx +126 -0
  36. package/client/src/CronBuilder.scss +128 -0
  37. package/client/src/CronPartBuilder.jsx +150 -0
  38. package/client/src/Dashboard.jsx +210 -0
  39. package/client/src/Dashboard.scss +445 -0
  40. package/client/src/DashboardChart.jsx +362 -0
  41. package/client/src/DashboardFlexViewItem.jsx +129 -0
  42. package/client/src/DashboardView.jsx +554 -0
  43. package/client/src/DataChart.jsx +349 -0
  44. package/client/src/DataEditor.jsx +556 -0
  45. package/client/src/DataLayout.jsx +709 -0
  46. package/client/src/DataTable.jsx +996 -0
  47. package/client/src/DataTable.scss +73 -0
  48. package/client/src/Dialog.jsx +90 -0
  49. package/client/src/Dialog.scss +117 -0
  50. package/client/src/DisplayFlexNodeRenderer.jsx +115 -0
  51. package/client/src/DocumentationPageLayout.jsx +93 -0
  52. package/client/src/DocumentationPageLayout.scss +262 -0
  53. package/client/src/Draggable.jsx +222 -0
  54. package/client/src/Draggable.scss +49 -0
  55. package/client/src/ExportDialog.jsx +172 -0
  56. package/client/src/Field.jsx +1558 -0
  57. package/client/src/FlexBuilder.jsx +269 -0
  58. package/client/src/FlexBuilder.scss +226 -0
  59. package/client/src/FlexBuilderControls.jsx +170 -0
  60. package/client/src/FlexBuilderModal.jsx +75 -0
  61. package/client/src/FlexBuilderModal.scss +109 -0
  62. package/client/src/FlexBuilderPreview.jsx +48 -0
  63. package/client/src/FlexBuilderUtils.js +84 -0
  64. package/client/src/FlexDataRenderer.jsx +170 -0
  65. package/client/src/FlexDataRenderer.scss +80 -0
  66. package/client/src/FlexNode.jsx +290 -0
  67. package/client/src/FlexTreeUtils.js +129 -0
  68. package/client/src/HiddenableCell.jsx +46 -0
  69. package/client/src/HiddenableCell.scss +36 -0
  70. package/client/src/KPIDialog.jsx +39 -0
  71. package/client/src/KPIWidget.jsx +139 -0
  72. package/client/src/KanbanCard.jsx +92 -0
  73. package/client/src/KanbanColumn.jsx +29 -0
  74. package/client/src/KanbanConfigModal.jsx +115 -0
  75. package/client/src/KanbanView.jsx +113 -0
  76. package/client/src/KanbanView.scss +107 -0
  77. package/client/src/MessageRotator.jsx +77 -0
  78. package/client/src/MessageRotator.scss +14 -0
  79. package/client/src/ModelCreator.jsx +640 -0
  80. package/client/src/ModelCreator.scss +154 -0
  81. package/client/src/ModelCreatorField.jsx +810 -0
  82. package/client/src/ModelImporter.jsx +120 -0
  83. package/client/src/ModelList.jsx +205 -0
  84. package/client/src/Notification.jsx +137 -0
  85. package/client/src/Notification.scss +126 -0
  86. package/client/src/NotificationProvider.jsx +73 -0
  87. package/client/src/PackGallery.jsx +211 -0
  88. package/client/src/PackGallery.scss +156 -0
  89. package/client/src/Pagination.jsx +141 -0
  90. package/client/src/RTE.jsx +415 -0
  91. package/client/src/RTETrans.jsx +103 -0
  92. package/client/src/RelationField.jsx +247 -0
  93. package/client/src/RelationValue.jsx +230 -0
  94. package/client/src/RestoreConfirmationModal.jsx +183 -0
  95. package/client/src/RestoreDialog.jsx +134 -0
  96. package/client/src/RestoreDialog.scss +67 -0
  97. package/client/src/RichTextEditorModal.jsx +34 -0
  98. package/client/src/RichTextEditorModal.scss +54 -0
  99. package/client/src/TourSpotlight.jsx +234 -0
  100. package/client/src/TourSpotlight.scss +100 -0
  101. package/client/src/TutorialsMenu.jsx +83 -0
  102. package/client/src/TutorialsMenu.scss +217 -0
  103. package/client/src/TutorialsRewardModal.jsx +65 -0
  104. package/client/src/Utils.jsx +78 -0
  105. package/client/src/ViewSwitcher.jsx +54 -0
  106. package/client/src/ViewSwitcher.scss +74 -0
  107. package/client/src/Webpage.jsx +69 -0
  108. package/client/src/_variables.scss +21 -0
  109. package/client/src/assets/react.svg +1 -0
  110. package/client/src/constants.js +68 -0
  111. package/client/src/contexts/AuthContext.jsx +28 -0
  112. package/client/src/contexts/ModelContext.jsx +328 -0
  113. package/client/src/contexts/UIContext.jsx +52 -0
  114. package/client/src/core/data.js +35 -0
  115. package/client/src/entry-client.jsx +15 -0
  116. package/client/src/entry-server.jsx +13 -0
  117. package/client/src/filter.js +222 -0
  118. package/client/src/hooks/data.js +101 -0
  119. package/client/src/hooks/useDebounce.js +20 -0
  120. package/client/src/hooks/useDragAndDrop.js +59 -0
  121. package/client/src/hooks/useLocalStorage.js +100 -0
  122. package/client/src/hooks/useMount.js +42 -0
  123. package/client/src/hooks/useTutorials.jsx +227 -0
  124. package/client/src/hooks/useWindowSize.js +24 -0
  125. package/client/src/i18n.js +47 -0
  126. package/client/src/index.css +90 -0
  127. package/client/src/index.js +10 -0
  128. package/client/src/main.jsx +11 -0
  129. package/client/src/translations.js +15940 -0
  130. package/client/src/tutorials.js +112 -0
  131. package/client/vite.config.js +30 -0
  132. package/eslint.config.js +50 -0
  133. package/package.json +48 -27
  134. package/server.js +9 -0
  135. package/src/constants.js +12 -23
  136. package/src/core.js +9 -8
  137. package/src/data.js +81 -80
  138. package/src/defaultModels.js +114 -115
  139. package/src/email.js +8 -9
  140. package/src/engine.js +5 -5
  141. package/src/events.js +1 -2
  142. package/src/i18n.js +212 -211
  143. package/src/middlewares/middleware-mongodb.js +94 -93
  144. package/src/migrate.js +24 -24
  145. package/src/modules/assistant.js +55 -55
  146. package/src/modules/bucket.js +10 -9
  147. package/src/modules/data.js +387 -357
  148. package/src/modules/file.js +3 -6
  149. package/src/modules/mongodb.js +6 -7
  150. package/src/modules/user.js +24 -47
  151. package/src/modules/workflow.js +68 -67
  152. package/src/openai.jobs.js +2 -2
  153. package/src/packs.js +30 -31
  154. package/src/providers.js +101 -3
  155. package/src/setenv.js +1 -1
  156. package/src/user.js +0 -17
  157. package/src/workers/crypto-worker.js +21 -20
  158. package/src/workers/import-export-worker.js +18 -18
  159. package/test/data.backup.integration.test.js +7 -12
  160. package/test/data.integration.test.js +27 -25
  161. package/test/globalSetup.js +2 -2
  162. package/test/import_export.integration.test.js +6 -6
  163. package/test/model.integration.test.js +21 -22
  164. package/test/workflow.integration.test.js +2 -5
  165. package/test/workflow.robustness.test.js +6 -13
  166. package/vitest.config.js +2 -2
@@ -0,0 +1,14 @@
1
+ /* src/components/MessageRotator.css */
2
+ .message-rotator-container {
3
+ display: flex;
4
+ justify-content: center;
5
+ align-items: center;
6
+ min-height: 50px; /* Ajustez selon vos besoins */
7
+ overflow: hidden; /* Empêche le texte de déborder pendant la transition si les tailles varient */
8
+ position: relative; /* Utile si vous voulez positionner des éléments absolument à l'intérieur */
9
+ }
10
+
11
+ .message-rotator-text {
12
+ text-align: center;
13
+ /* Styles supplémentaires pour le texte si nécessaire */
14
+ }
@@ -0,0 +1,640 @@
1
+
2
+ import React, {forwardRef, useEffect, useRef, useState} from 'react';
3
+ import {useMutation, useQuery, useQueryClient} from 'react-query';
4
+ import {useModelContext} from "./contexts/ModelContext.jsx";
5
+ import {FaArrowDown, FaArrowUp, FaEdit, FaInfo, FaMinus, FaPlus, FaSave, FaTrash} from "react-icons/fa";
6
+
7
+ import "./ModelCreator.scss"
8
+ import {CheckboxField, CodeField, ColorField, NumberField, SelectField, TextField} from "./Field.jsx";
9
+ import Button from "./Button.jsx";
10
+ import {Trans, useTranslation} from "react-i18next";
11
+ import {
12
+ allowedFields,
13
+ defaultMaxRequestData,
14
+ mainFieldsTypes, maxFileSize,
15
+ maxModelNameLength,
16
+ maxRequestData
17
+ } from "data-primals-engine/constants";
18
+ import {useAuthContext} from "./contexts/AuthContext.jsx";
19
+ import {getUserHash, getUserId, isLocalUser} from "data-primals-engine/data";
20
+ import {useNotificationContext} from "./NotificationProvider.jsx";
21
+ import ConditionBuilder from './ConditionBuilder.jsx';
22
+ import './ConditionBuilder.scss';
23
+ import {Tooltip} from "react-tooltip";
24
+ import {FaCircleInfo} from "react-icons/fa6";
25
+ import i18n from "data-primals-engine/i18n";
26
+ import Draggable from "./Draggable.jsx";
27
+ import FlexBuilder from "./FlexBuilder.jsx";
28
+ import {NavLink} from "react-router";
29
+ import CalculationBuilder from "./CalculationBuilder.jsx";
30
+ import CronBuilder from "./CronBuilder.jsx";
31
+ import ModelCreatorField from "./ModelCreatorField.jsx";
32
+ import useLocalStorage from "./hooks/useLocalStorage.js";
33
+
34
+ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGenerate = false, initialModel, onModelSaved }, ref) => {
35
+
36
+ const queryClient = useQueryClient();
37
+ const {t, i18n} = useTranslation();
38
+ const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
39
+
40
+ const {models, elementsPerPage, page, setDatasToLoad, selectedModel, setSelectedModel, pagedFilters, relationIds, pagedSort, setGeneratedModels, generatedModels } = useModelContext();
41
+ const [modelName, setModelName] = useState(initialModel?.name || ''); // Utilisation de initialModel
42
+ const [modelMaxRequestData, setModelMaxRequestData] = useState(initialModel?.maxRequestData || ''); // Utilisation de initialModel
43
+ const [modelDescription, setModelDescription] = useState(initialModel?.name ? t(`model_description_${initialModel?.name}`, initialModel?.description) : '');
44
+ const [fields, setFields] = useState([]);
45
+ const [changed, setChanged] = useState(false);
46
+
47
+ const [selectedGeneratedModelIndex, setSelectedGeneratedModelIndex] = useState(0); // Pour l'index du modèle choisi
48
+
49
+ useEffect(() => {
50
+ if (initialModel) {
51
+ // Mode édition : on charge les données du modèle existant
52
+ setModelName(initialModel.name || '');
53
+ setModelMaxRequestData(initialModel.maxRequestData || defaultMaxRequestData);
54
+ setModelDescription(initialModel.name ? t(`model_description_${initialModel.name}`, initialModel.description) : '');
55
+ setFields([...(initialModel.fields || []).map(m => ({...m}))]);
56
+ } else {
57
+ // Mode création : on réinitialise tout pour une nouvelle génération
58
+ setModelName('');
59
+ setModelDescription('');
60
+ setFields([]);
61
+ setUseAI(true); // On active l'IA par défaut
62
+ setModelVisible(false);
63
+ }
64
+ }, [initialModel]);
65
+
66
+ useEffect(() => {
67
+ if (generatedModels.length > 0 && selectedGeneratedModelIndex >= 0) {
68
+ const selectedModel = generatedModels[selectedGeneratedModelIndex];
69
+ if (selectedModel) {
70
+ setModelName(selectedModel.name || '');
71
+ setModelDescription(selectedModel.description || '');
72
+ setFields(selectedModel.fields || []);
73
+ setModelMaxRequestData(selectedModel.maxRequestData || defaultMaxRequestData);
74
+ }
75
+ }
76
+ }, [generatedModels, selectedGeneratedModelIndex]);
77
+
78
+ useEffect(() => {
79
+ if( !changed)
80
+ setModelDescription(initialModel?.name ? t('model_description_'+initialModel?.name, initialModel?.description) : '');
81
+ }, [lang]);
82
+
83
+ const mutationRename = useMutation(
84
+ ({oldName, newName}) => {
85
+ const url = `/api/model/${initialModel._id}/renameField?_user=${encodeURIComponent(getUserId(me))}`;
86
+ return fetch(url, {
87
+ method: 'PATCH',
88
+ headers: { 'Content-Type': 'application/json' },
89
+ body: JSON.stringify({ oldFieldName: oldName, newFieldName: newName }),
90
+ }).then((res) => res.json());
91
+ },
92
+ {
93
+ onSuccess: (data) => {
94
+ setDatasToLoad(datas=>[...datas, modelName]);
95
+ queryClient.invalidateQueries('api/models');
96
+ queryClient.invalidateQueries(['api/data', modelName, 'page', page, elementsPerPage, pagedFilters[modelName], pagedSort[modelName]]);
97
+ },
98
+ onError: (error) => {
99
+ console.error('Erreur:', error);
100
+ },
101
+ }
102
+ );
103
+
104
+ const { me } = useAuthContext()
105
+
106
+ const {addNotification} = useNotificationContext()
107
+
108
+ const mutation = useMutation(
109
+ (modelData) => {
110
+ const url = initialModel?._id ? `/api/model/${initialModel._id}` : '/api/model'; // Utilisation de initialModel pour l'URL
111
+ const method = initialModel?._id ? 'PUT' : 'POST';
112
+ return fetch(url+'?_user='+me.username, {
113
+ method: method,
114
+ headers: { 'Content-Type': 'application/json' },
115
+ body: JSON.stringify(modelData),
116
+ }).then((res) => res.json());
117
+ },
118
+ {
119
+ onSuccess: (data) => {
120
+
121
+ const notificationData = {
122
+ title: data.success ? t('modelcreator.success', 'Modèle sauvegardé') : t(data.error, data.error),
123
+ icon: data.success ? <FaInfo /> : undefined,
124
+ status: data.success ? 'completed': 'error'
125
+ };
126
+ addNotification(notificationData);
127
+
128
+ if (!initialModel) {
129
+ setModelName('');
130
+ setModelDescription('');
131
+ setModelMaxRequestData(defaultMaxRequestData);
132
+ setFields([{ name: '', type: 'string' }]);
133
+ }
134
+ setDatasToLoad(datas=>[...datas, modelName]);
135
+
136
+ queryClient.invalidateQueries(['api/data', modelName, 'page', page, elementsPerPage, pagedFilters[modelName], pagedSort[modelName]]);
137
+ queryClient.invalidateQueries('api/models');
138
+
139
+
140
+ },
141
+ onError: (error) => {
142
+ const notificationData = {
143
+ title: t(error, error),
144
+ status: 'error'
145
+ };
146
+ addNotification(notificationData);
147
+ console.error('Erreur:', error);
148
+ },
149
+ }
150
+ );
151
+
152
+ const handleAddField = () => {
153
+ setFields([...fields, { name: '', type: 'string', _isNewField: true }]);
154
+ };
155
+
156
+ const handleRemoveField = (index) => {
157
+ const newFields = [...fields];
158
+ newFields.splice(index, 1);
159
+ setFields(newFields);
160
+ };
161
+ const deleteMutation = useMutation(
162
+ (modelName) =>
163
+ fetch(`/api/model?name=${modelName}&_user=${encodeURIComponent(getUserId(me))}`, {
164
+ method: 'DELETE',
165
+ }).then((res) => res.json()),
166
+ {
167
+ onSuccess: () => {
168
+ queryClient.invalidateQueries('api/models');
169
+ if (onModelSaved) {
170
+ onModelSaved(); // Vous pouvez passer un argument ici si nécessaire
171
+ }
172
+ // Réinitialiser le formulaire ou rediriger, selon vos besoins
173
+ setModelName('');
174
+ setModelDescription('');
175
+ setSelectedModel(null)
176
+ setFields([{ name: '', type: 'string', _isNewField: true }]);
177
+ setDatasToLoad(datas => datas.filter(f => f !== modelName));
178
+ setGeneratedModels(mods => {
179
+ return mods.filter(m => m.name !== initialModel.name);
180
+ });
181
+ const notificationData = {
182
+ title: t('modelcreator.deleteSuccess', 'Modèle supprimé'),
183
+ icon: <FaInfo />,
184
+ status: 'completed'
185
+ };
186
+ addNotification(notificationData);
187
+ },
188
+ onError: (error) => {
189
+ console.error('Erreur lors de la suppression:', error);
190
+
191
+ const notificationData = {
192
+ title: t(error, error),
193
+ status: 'error'
194
+ };
195
+ addNotification(notificationData);
196
+ },
197
+ }
198
+ );
199
+
200
+ const handleDelete = () => {
201
+ if (initialModel && window.confirm("Êtes-vous sûr de vouloir supprimer ce modèle ?")) {
202
+ deleteMutation.mutate(initialModel.name);
203
+ }
204
+ };
205
+ const handleSubmit = (e) => {
206
+ e.preventDefault();
207
+ const newModel = {
208
+ name: modelName,
209
+ description: modelDescription,
210
+ maxRequestData: modelMaxRequestData,
211
+ fields: (fields || []).map((field) => {
212
+ delete field['_isNewField'];
213
+ let otherFields = [];
214
+ // Check for specific field types
215
+ switch (field.type) {
216
+ case 'relation':
217
+ otherFields = ['relation', 'multiple', 'relationFilter'];
218
+ break;
219
+ case 'enum':
220
+ {
221
+ otherFields = ['items']
222
+ break;
223
+ }
224
+ case 'number':
225
+ otherFields = ['min', 'max', 'step', 'unit'];
226
+ break;
227
+ case 'string':
228
+ case 'string_t':
229
+ case 'richtext':
230
+ case 'url':
231
+ case 'email':
232
+ case 'phone':
233
+ case 'password':
234
+ case 'code':
235
+ if (field.type === 'code')
236
+ otherFields = ['maxlength', 'language', 'conditionBuilder'];
237
+ else if( ['string_t', 'string'].includes(field.type))
238
+ otherFields = ['maxlength', 'multiline'];
239
+ else
240
+ otherFields = ['maxlength'];
241
+ break;
242
+ case 'date':
243
+ case 'datetime':
244
+ {
245
+ otherFields = ['min','max'];
246
+ break;
247
+ }
248
+ case 'image':
249
+ case 'file':
250
+ otherFields = ['mimeTypes', 'maxSize'];
251
+ break;
252
+ case 'calculated':
253
+ otherFields = ['calculation'];
254
+ break;
255
+ case 'array':
256
+ otherFields = ['itemsType'];
257
+ break;
258
+ default:
259
+ }
260
+ Object.keys(field).forEach(f => {
261
+ if( ![...allowedFields, ...otherFields].includes(f))
262
+ delete field[f];
263
+ });
264
+ return field;
265
+ }),
266
+ };
267
+ setGeneratedModels(gen => gen.filter(m => m.name !== newModel.name));
268
+ mutation.mutateAsync(newModel).then(data => {
269
+ queryClient.invalidateQueries('api/models');
270
+ newModel.fields.forEach(field => {
271
+ if( field.type === "relation") {
272
+ queryClient.invalidateQueries(['api/data', field.relation, relationIds[field.relation]]);
273
+ }
274
+ })
275
+ if (onModelSaved) {
276
+ onModelSaved(data.data);
277
+ }
278
+ });
279
+
280
+ };
281
+ const swapElements = (array, index1, index2) => {
282
+ if( index1 < 0 || index2 < 0 || index1 >= array.length || index2 >= array.length)
283
+ return;
284
+ let temp = array[index1];
285
+ array[index1] = array[index2];
286
+ array[index2] = temp;
287
+ };
288
+
289
+ const handleUp = (index) => {
290
+ const nf = [...fields];
291
+ swapElements(nf, index, index -1);
292
+ setFields(nf);
293
+ };
294
+ const handleDown = (index) => {
295
+ const nf = [...fields];
296
+ swapElements(nf, index, index + 1);
297
+ setFields(nf);
298
+ };
299
+
300
+ const handleAddValue = (fi) => {
301
+ const newFields = [...fields];
302
+ if( !newFields[fi].items )
303
+ newFields[fi].items = [];
304
+ newFields[fi].items.push("");
305
+ setFields(newFields)
306
+ }
307
+ const handleRemoveValue = (fi, index) => {
308
+ const newFields = [...fields];
309
+ const field = newFields[fi];
310
+ field.items = field.items.filter((f,i) => i !== index);
311
+ setFields(newFields)
312
+ }
313
+
314
+ const handleRenameField = (fi, oldVal) => {
315
+ const prompted = prompt(t('core.renameFields', 'Renommer le champ'), oldVal);
316
+ if (prompted !== oldVal) {
317
+ mutationRename.mutateAsync({oldName: oldVal, newName: prompted}).then(e =>{
318
+ const newFields = [...fields];
319
+ const field = newFields[fi];
320
+ field.name = prompted;
321
+ setFields(newFields)
322
+ });
323
+ }
324
+ }
325
+
326
+
327
+ const modelLocked = !!initialModel && (!initialModel?._user ? true : isLocalUser(me) && initialModel?.locked);
328
+
329
+ const hint = (description) => t(description, '') && <div className="hint-icon"><FaCircleInfo data-tooltip-id={`tooltipHint`} data-tooltip-content={description} /></div>
330
+
331
+ const handleMoveDown = (field, index, i) => {
332
+ const newFields = [...fields];
333
+ if( i + 1 >= newFields[index].items.length )
334
+ return;
335
+ const r = newFields[index].items[i + 1];
336
+ newFields[index].items[i + 1] = newFields[index].items[i];
337
+ newFields[index].items[i] = r;
338
+ setFields(newFields);
339
+ }
340
+
341
+ const handleMoveUp = (field, index, i) => {
342
+ const newFields = [...fields];
343
+ if( i < 1 )
344
+ return;
345
+ const r = newFields[index].items[i - 1];
346
+ newFields[index].items[i - 1] = newFields[index].items[i];
347
+ newFields[index].items[i] = r;
348
+ setFields(newFields);
349
+ }
350
+
351
+ const [useAI, setUseAI] = useState(true);
352
+ const [showModel, setModelVisible] = useState(true);
353
+ const [prompt, setPrompt] = useState('');
354
+
355
+ const [homePrompt, setHomePrompt] = useLocalStorage("ai_model_prompt", null);
356
+ const [promptResult, setPromptResult] = useLocalStorage("ai_model_prompt_result", null);
357
+
358
+ // NOUVEAU : Créer une référence pour suivre le déclenchement
359
+ const hasTriggeredAutoGenerate = useRef(false);
360
+
361
+ const[generationIsLoading, setGenerationIsLoading] =useState(false);
362
+
363
+ const generateModelMutation = useMutation(
364
+ async ({userPrompt, modelToEdit}) => {
365
+ const bodyPayload = { prompt: userPrompt };
366
+ // Si on est en mode édition, on ajoute le modèle au corps de la requête
367
+ if (modelToEdit) {
368
+ bodyPayload.existingModel = modelToEdit;
369
+ }
370
+
371
+ const response = await fetch(`/api/model/generate?lang=${lang}`, {
372
+ method: 'POST',
373
+ headers: { 'Content-Type': 'application/json' },
374
+ body: JSON.stringify(bodyPayload),
375
+ });
376
+
377
+ if (!response.ok) {
378
+ const err = await response.json();
379
+ throw new Error(err.error || 'Failed to generate model');
380
+ }
381
+
382
+ return response.json();
383
+ },
384
+ {
385
+ onSuccess: (data) => {
386
+ const modelsList = data?.models || data || [];
387
+
388
+ if (modelsList && Array.isArray(modelsList) && modelsList.length > 0) {
389
+ const mds = modelsList.map(m => ({ ...m, _user: getUserId(me) }));
390
+ setGeneratedModels(g=>[...g, ...mds]);
391
+ onModelGenerated?.(mds);
392
+ modelsList.forEach(d => gtag('event', 'model generation "' + d.name + '"'));
393
+
394
+ setSelectedGeneratedModelIndex(0); // Sélectionne le premier par défaut
395
+ setModelVisible(true); // Affiche la section avec la liste et le formulaire
396
+ setHomePrompt(null);
397
+ addNotification({ title: t('modelcreator.generate.success'), status: 'completed' });
398
+ } else {
399
+ // GA ne retourne aucun modèle
400
+ setModelVisible(false);
401
+ setHomePrompt(null);
402
+ addNotification({
403
+ title: t('modelcreator.generate.error'),
404
+ description: t('modelcreator.generate.no_models', "Aucun modèle n'a pu être généré."),
405
+ status: 'error'
406
+ });
407
+ }
408
+ setGenerationIsLoading(false);
409
+ },
410
+ onError: (error) => {
411
+ setPromptResult(false);
412
+ addNotification({
413
+ title: t('modelcreator.generate.error'),
414
+ description: error.message,
415
+ status: 'error'
416
+ });
417
+ },
418
+ }
419
+ );
420
+
421
+ const handleGenerate = () => {
422
+ if( prompt?.trim() ){
423
+ gtag("event", "generate model by AI");
424
+ setPromptResult(null);
425
+ setGenerationIsLoading(true);
426
+ generateModelMutation.mutate({
427
+ userPrompt: prompt,
428
+ modelToEdit: initialModel
429
+ });
430
+ }
431
+ };
432
+
433
+ useEffect(() => {
434
+ // on ne veut la génération du prompt que si il y a un prompt en attente
435
+ if(homePrompt && !hasTriggeredAutoGenerate.current){
436
+ setPrompt(homePrompt);
437
+ hasTriggeredAutoGenerate.current = true;
438
+ setUseAI(true);
439
+ setGenerationIsLoading(true);
440
+ generateModelMutation.mutate({
441
+ userPrompt: homePrompt,
442
+ modelToEdit: initialModel
443
+ });
444
+ }
445
+
446
+ }, [initialModel]); // Le tableau vide est toujours correct
447
+ return (
448
+ <div className="model-creator" ref={ref}>
449
+
450
+
451
+ <Tooltip id={"tooltipHint"}
452
+ place={"top-end"}
453
+ globalCloseEvents={{ clickOutsideAnchor: true }}
454
+ render={({ content, activeAnchor }) => {
455
+ gtag('render hint ' + content);
456
+ const c = t(content, content);
457
+ return c && (
458
+ <><p className="ws-pre-line">{c}</p></>)
459
+ }} />
460
+ <h2>
461
+ {!initialModel && <Trans i18nKey={"btns.createModel"}>Créer un modèle</Trans>}
462
+ {!!initialModel && <><Trans i18nKey={"btns.editModel"}>Editer le modèle</Trans> &#34;{t(`model_${modelName}`, modelName)}&#34;</>}
463
+ </h2>
464
+ <form onSubmit={handleSubmit}>
465
+
466
+ {/* Section pour choisir le mode de création (IA ou Manuel) */}
467
+ {!initialModel && (
468
+ <CheckboxField
469
+ label={<Trans i18nKey="modelcreator.useAI" />}
470
+ checked={useAI}
471
+ onChange={() => setUseAI(!useAI)}
472
+ />
473
+ )}
474
+
475
+ {/* Section pour le prompt de l'IA (uniquement si création par IA et avant génération) */}
476
+ {useAI && !selectedModel && !showModel && (
477
+ <>
478
+ <div className="ai-prompt-container">
479
+ <>
480
+ <TextField
481
+ value={prompt}
482
+ onChange={(e) => setPrompt(e.target.value)}
483
+ multiline
484
+ help={t("modelcreator.generate.help")}
485
+ required
486
+ disabled={generationIsLoading}
487
+ />
488
+ <div className="examples" dangerouslySetInnerHTML={{ __html: t('modelcreator.generate.examples') }} />
489
+ <Button
490
+ role={"button"}
491
+ onClick={handleGenerate}
492
+ disabled={generationIsLoading}
493
+ >
494
+ {generationIsLoading
495
+ ? <Trans i18nKey="modelcreator.generating" />
496
+ : <Trans i18nKey="modelcreator.generate" />
497
+ }
498
+ </Button>
499
+ </>
500
+ </div>
501
+ {generationIsLoading && <p><Trans i18nKey="modelcreator.generating" /></p>}
502
+ </>
503
+ )}
504
+
505
+ {/* Layout principal pour afficher la liste et le formulaire côte à côte APRES génération */}
506
+ {showModel && useAI && (
507
+ <div className="flex model-generation-layout">
508
+ {/* Colonne de gauche: Liste des modèles générés */}
509
+ {generatedModels.some(g => models.find(f => f.name === g.name && f._user === g._user)) && (
510
+ <>
511
+ <div className="generated-models-list">
512
+ <h4>{t('modelcreator.generate.results_title', 'Suggestions de l\'IA')}</h4>
513
+ <ul>
514
+ {generatedModels.map((model, index) => (
515
+ <li
516
+ key={index}
517
+ className={index === selectedGeneratedModelIndex ? 'active' : ''}
518
+ onClick={() => setSelectedGeneratedModelIndex(index)}
519
+ >
520
+ {model.name}
521
+ </li>
522
+ ))}
523
+ </ul>
524
+ </div>
525
+
526
+ {/* Colonne de droite: Formulaire du modèle sélectionné */}
527
+ <div className="model-form-container">
528
+ {/* Le formulaire existant est placé ici */}
529
+ <div className="flex">
530
+ <label htmlFor="modelName"><Trans i18nKey={"modelcreator.name"}>Nom:</Trans></label>
531
+ </div>
532
+ <TextField
533
+ type="text"
534
+ id="modelName"
535
+ disabled={modelLocked}
536
+ value={modelName}
537
+ help={t('modelcreator.field.name.hint')}
538
+ onChange={(e) => setModelName(e.target.value)}
539
+ required
540
+ />
541
+
542
+ <div className="flex">
543
+ <label htmlFor="modelDescription"><Trans i18nKey={"modelcreator.description"}>Description:</Trans></label>
544
+ </div>
545
+ <TextField
546
+ multiline
547
+ help={t('modelcreator.field.description')}
548
+ id="modelDescription"
549
+ disabled={modelLocked}
550
+ value={modelDescription}
551
+ onChange={(e) => {
552
+ setModelDescription(e.target.value);
553
+ setChanged(true)
554
+ }}
555
+ />
556
+ <h3><Trans i18nKey={"modelcreator.fields"}>Champs du modèle :</Trans></h3>
557
+ {fields.map((field, index) => <ModelCreatorField
558
+ key={initialModel?.name + '_field_' + index}
559
+ handleRemoveValue={handleRemoveValue}
560
+ handleAddValue={handleAddValue}
561
+ handleRenameField={handleRenameField}
562
+ handleUp={handleUp}
563
+ handleDown={handleDown}
564
+ handleRemoveField={handleRemoveField}
565
+ index={index} fields={fields} field={field} model={initialModel} setFields={setFields} />)}
566
+ </div></>)}
567
+ </div>
568
+ )}
569
+
570
+ {/* Affichage du formulaire en mode manuel ou édition */}
571
+ {(!useAI || initialModel) && (
572
+ <div className="model-form-container">
573
+ <div className="flex">
574
+ <label htmlFor="modelName"><Trans i18nKey={"modelcreator.name"}>Nom:</Trans></label>
575
+ </div>
576
+ <TextField
577
+ type="text"
578
+ id="modelName"
579
+ disabled={modelLocked}
580
+ value={modelName}
581
+ help={t('modelcreator.field.name.hint')}
582
+ onChange={(e) => setModelName(e.target.value)}
583
+ required
584
+ />
585
+
586
+ <div className="flex">
587
+ <label htmlFor="modelDescription"><Trans i18nKey={"modelcreator.description"}>Description:</Trans></label>
588
+ </div>
589
+ <TextField
590
+ multiline
591
+ help={t('modelcreator.field.description')}
592
+ id="modelDescription"
593
+ disabled={modelLocked}
594
+ value={modelDescription}
595
+ onChange={(e) => {
596
+ setModelDescription(e.target.value);
597
+ setChanged(true)
598
+ }}
599
+ />
600
+ <h3><Trans i18nKey={"modelcreator.fields"}>Champs du modèle :</Trans></h3>
601
+ {fields.map((field, index) => <ModelCreatorField
602
+ key={initialModel?.name + '_field_' + index}
603
+ handleRemoveValue={handleRemoveValue}
604
+ handleAddValue={handleAddValue}
605
+ handleRenameField={handleRenameField}
606
+ handleUp={handleUp}
607
+ handleDown={handleDown}
608
+ handleRemoveField={handleRemoveField}
609
+ index={index} fields={fields} field={field} model={initialModel} setFields={setFields} />)}
610
+ </div>
611
+ )}
612
+
613
+ {/* Boutons d'action, visibles si un modèle est affiché ou en mode manuel */}
614
+ {(showModel || !useAI || initialModel) && (
615
+ <div className="actions flex">
616
+ {!useAI && (
617
+ <Button type="button" onClick={handleAddField}>
618
+ <FaPlus /> <Trans i18nKey={"btns.addField"}>Ajouter un champ</Trans>
619
+ </Button>
620
+ )}
621
+
622
+ <Button type="submit">
623
+ <FaSave />
624
+ <Trans i18nKey={"btns.saveModel"}>Enregistrer le modèle</Trans>
625
+ </Button>
626
+ {initialModel && (
627
+ <Button type="button" onClick={handleDelete}>
628
+ <FaTrash /><Trans i18nKey={"btns.del"}>Supprimer le modèle</Trans>
629
+ </Button>
630
+ )}
631
+ </div>
632
+ )}
633
+
634
+ </form>
635
+ </div>
636
+ );
637
+ });
638
+ ModelCreator.displayName = "ModelCreator";
639
+
640
+ export default ModelCreator;