data-primals-engine 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,687 +1,684 @@
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, IconField, 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 "../../src/constants.js";
18
- import {useAuthContext} from "./contexts/AuthContext.jsx";
19
- import {getUserHash, getUserId, isLocalUser} from "../../src/data.js";
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 "../../src/i18n.js";
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 [modelHistory, setModelHistory] = useState(!!initialModel?.history || undefined);
45
- const [modelIcon, setModelIcon] = useState(!!initialModel?.icon || undefined);
46
- const [fields, setFields] = useState([]);
47
- const [changed, setChanged] = useState(false);
48
-
49
- const [selectedGeneratedModelIndex, setSelectedGeneratedModelIndex] = useState(0); // Pour l'index du modèle choisi
50
-
51
- useEffect(() => {
52
- if (initialModel) {
53
- // Mode édition : on charge les données du modèle existant
54
- setModelName(initialModel.name || '');
55
- setModelMaxRequestData(initialModel.maxRequestData || defaultMaxRequestData);
56
- setModelDescription(initialModel.name ? t(`model_description_${initialModel.name}`, initialModel.description) : '');
57
- setFields([...(initialModel.fields || []).map(m => ({...m}))]);
58
- setModelHistory(initialModel.history);
59
- setModelIcon(initialModel.icon);
60
- } else {
61
- // Mode création : on réinitialise tout pour une nouvelle génération
62
- setModelName('');
63
- setModelDescription('');
64
- setFields([]);
65
- setUseAI(true); // On active l'IA par défaut
66
- setModelVisible(false);
67
- setModelHistory(false);
68
- setModelIcon(null);
69
- }
70
- }, [initialModel]);
71
-
72
- useEffect(() => {
73
- if (generatedModels.length > 0 && selectedGeneratedModelIndex >= 0) {
74
- const selectedModel = generatedModels[selectedGeneratedModelIndex];
75
- if (selectedModel) {
76
- setModelName(selectedModel.name || '');
77
- setModelDescription(selectedModel.description || '');
78
- setFields(selectedModel.fields || []);
79
- setModelMaxRequestData(selectedModel.maxRequestData || defaultMaxRequestData);
80
- setModelIcon(selectedModel.icon || null);
81
- }
82
- }
83
- }, [generatedModels, selectedGeneratedModelIndex]);
84
-
85
- useEffect(() => {
86
- if( !changed)
87
- setModelDescription(initialModel?.name ? t('model_description_'+initialModel?.name, initialModel?.description) : '');
88
- }, [lang]);
89
-
90
- const mutationRename = useMutation(
91
- ({oldName, newName}) => {
92
- const url = `/api/model/${initialModel._id}/renameField?_user=${encodeURIComponent(getUserId(me))}`;
93
- return fetch(url, {
94
- method: 'PATCH',
95
- headers: { 'Content-Type': 'application/json' },
96
- body: JSON.stringify({ oldFieldName: oldName, newFieldName: newName }),
97
- }).then((res) => res.json());
98
- },
99
- {
100
- onSuccess: (data) => {
101
- setDatasToLoad(datas=>[...datas, modelName]);
102
- queryClient.invalidateQueries('api/models');
103
- queryClient.invalidateQueries(['api/data', modelName, 'page', page, elementsPerPage, pagedFilters[modelName], pagedSort[modelName]]);
104
- },
105
- onError: (error) => {
106
- console.error('Erreur:', error);
107
- },
108
- }
109
- );
110
-
111
- const { me } = useAuthContext()
112
-
113
- const {addNotification} = useNotificationContext()
114
-
115
- const mutation = useMutation(
116
- (modelData) => {
117
- const url = initialModel?._id ? `/api/model/${initialModel._id}` : '/api/model'; // Utilisation de initialModel pour l'URL
118
- const method = initialModel?._id ? 'PUT' : 'POST';
119
- return fetch(url+'?_user='+me.username, {
120
- method: method,
121
- headers: { 'Content-Type': 'application/json' },
122
- body: JSON.stringify(modelData),
123
- }).then((res) => res.json());
124
- },
125
- {
126
- onSuccess: (data) => {
127
-
128
- const notificationData = {
129
- title: data.success ? t('modelcreator.success', 'Modèle sauvegardé') : t(data.error, data.error),
130
- icon: data.success ? <FaInfo /> : undefined,
131
- status: data.success ? 'completed': 'error'
132
- };
133
- addNotification(notificationData);
134
-
135
- if (!initialModel) {
136
- setModelName('');
137
- setModelDescription('');
138
- setModelMaxRequestData(defaultMaxRequestData);
139
- setFields([{ name: '', type: 'string' }]);
140
- setModelHistory(undefined);
141
- setModelIcon(null);
142
- }
143
- setDatasToLoad(datas=>[...datas, modelName]);
144
-
145
- queryClient.invalidateQueries(['api/data', modelName, 'page', page, elementsPerPage, pagedFilters[modelName], pagedSort[modelName]]);
146
- queryClient.invalidateQueries('api/models');
147
-
148
-
149
- },
150
- onError: (error) => {
151
- const notificationData = {
152
- title: t(error, error),
153
- status: 'error'
154
- };
155
- addNotification(notificationData);
156
- console.error('Erreur:', error);
157
- },
158
- }
159
- );
160
-
161
- const handleAddField = () => {
162
- setFields([...fields, { name: '', type: 'string', _isNewField: true }]);
163
- };
164
-
165
- const handleRemoveField = (index) => {
166
- const newFields = [...fields];
167
- newFields.splice(index, 1);
168
- setFields(newFields);
169
- };
170
- const deleteMutation = useMutation(
171
- (modelName) =>
172
- fetch(`/api/model?name=${modelName}&_user=${encodeURIComponent(getUserId(me))}`, {
173
- method: 'DELETE',
174
- }).then((res) => res.json()),
175
- {
176
- onSuccess: () => {
177
- queryClient.invalidateQueries('api/models');
178
- if (onModelSaved) {
179
- onModelSaved(); // Vous pouvez passer un argument ici si nécessaire
180
- }
181
- // Réinitialiser le formulaire ou rediriger, selon vos besoins
182
- setModelName('');
183
- setModelDescription('');
184
- setSelectedModel(null)
185
- setModelHistory(undefined)
186
- setModelIcon(null);
187
- setFields([{ name: '', type: 'string', _isNewField: true }]);
188
- setDatasToLoad(datas => datas.filter(f => f !== modelName));
189
- setGeneratedModels(mods => {
190
- return mods.filter(m => m.name !== initialModel.name);
191
- });
192
- const notificationData = {
193
- title: t('modelcreator.deleteSuccess', 'Modèle supprimé'),
194
- icon: <FaInfo />,
195
- status: 'completed'
196
- };
197
- addNotification(notificationData);
198
- },
199
- onError: (error) => {
200
- console.error('Erreur lors de la suppression:', error);
201
-
202
- const notificationData = {
203
- title: t(error, error),
204
- status: 'error'
205
- };
206
- addNotification(notificationData);
207
- },
208
- }
209
- );
210
-
211
- const handleDelete = () => {
212
- if (initialModel && window.confirm("Êtes-vous sûr de vouloir supprimer ce modèle ?")) {
213
- deleteMutation.mutate(initialModel.name);
214
- }
215
- };
216
- const handleSubmit = (e) => {
217
- e.preventDefault();
218
- const newModel = {
219
- name: modelName,
220
- description: modelDescription,
221
- maxRequestData: modelMaxRequestData,
222
- history: modelHistory,
223
- icon: modelIcon,
224
- fields: (fields || []).map((field) => {
225
- delete field['_isNewField'];
226
- let otherFields = [];
227
- // Check for specific field types
228
- switch ((field.type)) {
229
- case 'relation':
230
- otherFields = ['relation', 'multiple', 'relationFilter'];
231
- break;
232
- case 'enum':
233
- {
234
- otherFields = ['items']
235
- break;
236
- }
237
- case 'number':
238
- otherFields = ['min', 'max', 'step', 'unit','delay', 'gauge', 'percent'];
239
- break;
240
- case 'string':
241
- case 'string_t':
242
- case 'richtext':
243
- case 'url':
244
- case 'email':
245
- case 'phone':
246
- case 'password':
247
- case 'code':
248
- const t = (field.itemsType || field.type);
249
- if (t === 'code')
250
- otherFields = ['maxlength', 'language', 'conditionBuilder'];
251
- else if( ['string_t', 'string'].includes(t))
252
- otherFields = ['maxlength', 'multiline', 'mask', 'replacement'];
253
- else
254
- otherFields = ['maxlength'];
255
- break;
256
- case 'date':
257
- case 'datetime':
258
- {
259
- otherFields = ['min','max'];
260
- break;
261
- }
262
- case 'image':
263
- case 'file':
264
- otherFields = ['mimeTypes', 'maxSize'];
265
- break;
266
- case 'calculated':
267
- otherFields = ['calculation'];
268
- break;
269
- case 'array':
270
- otherFields = ['itemsType'];
271
- break;
272
- default:
273
- }
274
- Object.keys(field).forEach(f => {
275
- if( ![...allowedFields, ...otherFields].includes(f))
276
- delete field[f];
277
- });
278
- return field;
279
- }),
280
- };
281
- setGeneratedModels(gen => gen.filter(m => m.name !== newModel.name));
282
- mutation.mutateAsync(newModel).then(data => {
283
- queryClient.invalidateQueries('api/models');
284
- newModel.fields.forEach(field => {
285
- if( field.type === "relation") {
286
- queryClient.invalidateQueries(['api/data', field.relation, relationIds[field.relation]]);
287
- }
288
- })
289
- if (onModelSaved) {
290
- onModelSaved(data.data);
291
- }
292
- });
293
-
294
- };
295
- const swapElements = (array, index1, index2) => {
296
- if( index1 < 0 || index2 < 0 || index1 >= array.length || index2 >= array.length)
297
- return;
298
- let temp = array[index1];
299
- array[index1] = array[index2];
300
- array[index2] = temp;
301
- };
302
-
303
- const handleUp = (index) => {
304
- const nf = [...fields];
305
- swapElements(nf, index, index -1);
306
- setFields(nf);
307
- };
308
- const handleDown = (index) => {
309
- const nf = [...fields];
310
- swapElements(nf, index, index + 1);
311
- setFields(nf);
312
- };
313
-
314
- const handleAddValue = (fi) => {
315
- const newFields = [...fields];
316
- if( !newFields[fi].items )
317
- newFields[fi].items = [];
318
- newFields[fi].items.push("");
319
- setFields(newFields)
320
- }
321
- const handleRemoveValue = (fi, index) => {
322
- const newFields = [...fields];
323
- const field = newFields[fi];
324
- field.items = field.items.filter((f,i) => i !== index);
325
- setFields(newFields)
326
- }
327
-
328
- const handleRenameField = (fi, oldVal) => {
329
- const prompted = window.prompt(t('core.renameFields', 'Renommer le champ'), oldVal);
330
- if (prompted && prompted !== oldVal) {
331
- mutationRename.mutateAsync({oldName: oldVal, newName: prompted}).then(e =>{
332
- const newFields = [...fields];
333
- const field = newFields[fi];
334
- field.name = prompted;
335
- setFields(newFields)
336
- });
337
- }
338
- }
339
-
340
-
341
- const modelLocked = !!initialModel && (!initialModel?._user ? true : isLocalUser(me) && initialModel?.locked);
342
-
343
- const hint = (description) => t(description, '') && <div className="hint-icon"><FaCircleInfo data-tooltip-id={`tooltipHint`} data-tooltip-content={description} /></div>
344
-
345
- const handleMoveDown = (field, index, i) => {
346
- const newFields = [...fields];
347
- if( i + 1 >= newFields[index].items.length )
348
- return;
349
- const r = newFields[index].items[i + 1];
350
- newFields[index].items[i + 1] = newFields[index].items[i];
351
- newFields[index].items[i] = r;
352
- setFields(newFields);
353
- }
354
-
355
- const handleMoveUp = (field, index, i) => {
356
- const newFields = [...fields];
357
- if( i < 1 )
358
- return;
359
- const r = newFields[index].items[i - 1];
360
- newFields[index].items[i - 1] = newFields[index].items[i];
361
- newFields[index].items[i] = r;
362
- setFields(newFields);
363
- }
364
-
365
- const [useAI, setUseAI] = useState(true);
366
- const [showModel, setModelVisible] = useState(true);
367
- const [prompt, setPrompt] = useState('');
368
-
369
- const [homePrompt, setHomePrompt] = useLocalStorage("ai_model_prompt", null);
370
- const [promptResult, setPromptResult] = useLocalStorage("ai_model_prompt_result", null);
371
-
372
- // NOUVEAU : Créer une référence pour suivre le déclenchement
373
- const hasTriggeredAutoGenerate = useRef(false);
374
-
375
- const[generationIsLoading, setGenerationIsLoading] =useState(false);
376
-
377
- const generateModelMutation = useMutation(
378
- async ({userPrompt, modelToEdit}) => {
379
- const bodyPayload = { prompt: userPrompt };
380
- // Si on est en mode édition, on ajoute le modèle au corps de la requête
381
- if (modelToEdit) {
382
- bodyPayload.existingModel = modelToEdit;
383
- }
384
-
385
- const response = await fetch(`/api/model/generate?lang=${lang}`, {
386
- method: 'POST',
387
- headers: { 'Content-Type': 'application/json' },
388
- body: JSON.stringify(bodyPayload),
389
- });
390
-
391
- if (!response.ok) {
392
- const err = await response.json();
393
- throw new Error(err.error || 'Failed to generate model');
394
- }
395
-
396
- return response.json();
397
- },
398
- {
399
- onSuccess: (data) => {
400
- const modelsList = data?.models || data || [];
401
-
402
- if (modelsList && Array.isArray(modelsList) && modelsList.length > 0) {
403
- const mds = modelsList.map(m => ({ ...m, _user: getUserId(me) }));
404
- setGeneratedModels(g=>[...g, ...mds]);
405
- onModelGenerated?.(mds);
406
- modelsList.forEach(d => gtag('event', 'model generation "' + d.name + '"'));
407
-
408
- setSelectedGeneratedModelIndex(0); // Sélectionne le premier par défaut
409
- setModelVisible(true); // Affiche la section avec la liste et le formulaire
410
- setHomePrompt(null);
411
- addNotification({ title: t('modelcreator.generate.success'), status: 'completed' });
412
- } else {
413
- // GA ne retourne aucun modèle
414
- setModelVisible(false);
415
- setHomePrompt(null);
416
- addNotification({
417
- title: t('modelcreator.generate.error'),
418
- description: t('modelcreator.generate.no_models', "Aucun modèle n'a pu être généré."),
419
- status: 'error'
420
- });
421
- }
422
- setGenerationIsLoading(false);
423
- },
424
- onError: (error) => {
425
- setPromptResult(false);
426
- addNotification({
427
- title: t('modelcreator.generate.error'),
428
- description: error.message,
429
- status: 'error'
430
- });
431
- },
432
- }
433
- );
434
-
435
- const handleGenerate = () => {
436
- if( prompt?.trim() ){
437
- gtag("event", "generate model by AI");
438
- setPromptResult(null);
439
- setGenerationIsLoading(true);
440
- generateModelMutation.mutate({
441
- userPrompt: prompt,
442
- modelToEdit: initialModel
443
- });
444
- }
445
- };
446
-
447
- useEffect(() => {
448
- // on ne veut la génération du prompt que si il y a un prompt en attente
449
- if(homePrompt && !hasTriggeredAutoGenerate.current){
450
- setPrompt(homePrompt);
451
- hasTriggeredAutoGenerate.current = true;
452
- setUseAI(true);
453
- setGenerationIsLoading(true);
454
- generateModelMutation.mutate({
455
- userPrompt: homePrompt,
456
- modelToEdit: initialModel
457
- });
458
- }
459
-
460
- }, [initialModel]); // Le tableau vide est toujours correct
461
- return (
462
- <div className="model-creator" ref={ref}>
463
-
464
-
465
- <Tooltip id={"tooltipHint"}
466
- place={"top-end"}
467
- globalCloseEvents={{ clickOutsideAnchor: true }}
468
- render={({ content, activeAnchor }) => {
469
- gtag('render hint ' + content);
470
- const c = t(content, content);
471
- return c && (
472
- <><p className="ws-pre-line">{c}</p></>)
473
- }} />
474
- <h2>
475
- {!initialModel && <Trans i18nKey={"btns.createModel"}>Créer un modèle</Trans>}
476
- {!!initialModel && <><Trans i18nKey={"btns.editModel"}>Editer le modèle</Trans> &#34;{t(`model_${modelName}`, modelName)}&#34;</>}
477
- </h2>
478
- <form onSubmit={handleSubmit}>
479
-
480
- {/* Section pour choisir le mode de création (IA ou Manuel) */}
481
- {!initialModel && (
482
- <CheckboxField
483
- label={<Trans i18nKey="modelcreator.useAI" />}
484
- checked={useAI}
485
- onChange={() => setUseAI(!useAI)}
486
- />
487
- )}
488
-
489
- {/* Section pour le prompt de l'IA (uniquement si création par IA et avant génération) */}
490
- {useAI && !selectedModel && !showModel && (
491
- <>
492
- <div className="ai-prompt-container">
493
- <>
494
- <TextField
495
- value={prompt}
496
- onChange={(e) => setPrompt(e.target.value)}
497
- multiline
498
- help={t("modelcreator.generate.help")}
499
- required
500
- disabled={generationIsLoading}
501
- />
502
- <div className="examples" dangerouslySetInnerHTML={{ __html: t('modelcreator.generate.examples') }} />
503
- <Button
504
- role={"button"}
505
- onClick={handleGenerate}
506
- disabled={generationIsLoading}
507
- >
508
- {generationIsLoading
509
- ? <Trans i18nKey="modelcreator.generating" />
510
- : <Trans i18nKey="modelcreator.generate" />
511
- }
512
- </Button>
513
- </>
514
- </div>
515
- {generationIsLoading && <p><Trans i18nKey="modelcreator.generating" /></p>}
516
- </>
517
- )}
518
-
519
- {/* Layout principal pour afficher la liste et le formulaire côte à côte APRES génération */}
520
- {showModel && useAI && (
521
- <div className="flex model-generation-layout">
522
- {/* Colonne de gauche: Liste des modèles générés */}
523
- {generatedModels.some(g => models.find(f => f.name === g.name && f._user === g._user)) && (
524
- <>
525
- <div className="generated-models-list">
526
- <h4>{t('modelcreator.generate.results_title', 'Suggestions de l\'IA')}</h4>
527
- <ul>
528
- {generatedModels.map((model, index) => (
529
- <li
530
- key={index}
531
- className={index === selectedGeneratedModelIndex ? 'active' : ''}
532
- onClick={() => setSelectedGeneratedModelIndex(index)}
533
- >
534
- {model.name}
535
- </li>
536
- ))}
537
- </ul>
538
- </div>
539
-
540
- {/* Colonne de droite: Formulaire du modèle sélectionné */}
541
- <div className="model-form-container">
542
- {/* Le formulaire existant est placé ici */}
543
- <div className="field">
544
- <div className="flex field-bg">
545
- <label htmlFor="modelName"><Trans i18nKey={"modelcreator.name"}>Nom:</Trans></label>
546
- </div>
547
- <TextField
548
- type="text"
549
- id="modelName"
550
- disabled={modelLocked}
551
- value={modelName}
552
- help={t('modelcreator.field.name.hint')}
553
- onChange={(e) => setModelName(e.target.value)}
554
- required
555
- />
556
- </div>
557
-
558
- <div className="field">
559
- <div className="checkbox-label flex flex-1">
560
- <CheckboxField
561
- label={<Trans i18nKey={"history.title"}>Historique</Trans>}
562
- disabled={modelLocked || (isLocalUser(me) && field.locked)}
563
- checked={field.required}
564
- onChange={(e) => {
565
- const newFields = [...fields];
566
- newFields[index].history = e ? { enabled: true } : undefined;
567
- setFields(newFields);
568
- }}
569
- help={field.required && t('modelcreator.history.hint')}
570
- />
571
- </div>
572
- </div>
573
-
574
-
575
- <h3><Trans i18nKey={"modelcreator.fields"}>Champs du modèle :</Trans></h3>
576
- {fields.map((field, index) => <ModelCreatorField
577
- key={initialModel?.name + '_field_' + index}
578
- handleRemoveValue={handleRemoveValue}
579
- handleAddValue={handleAddValue}
580
- handleRenameField={handleRenameField}
581
- handleUp={handleUp}
582
- handleDown={handleDown}
583
- handleRemoveField={handleRemoveField}
584
- index={index} fields={fields} field={field} model={initialModel} setFields={setFields} />)}
585
- </div></>)}
586
- </div>
587
- )}
588
-
589
- {/* Affichage du formulaire en mode manuel ou édition */}
590
- {(!useAI || initialModel) && (
591
- <div className="model-form-container">
592
- <div className="flex field-bg">
593
- <label htmlFor="modelName"><Trans i18nKey={"modelcreator.name"}>Nom:</Trans></label>
594
- </div>
595
- <TextField
596
- type="text"
597
- id="modelName"
598
- disabled={modelLocked}
599
- value={modelName}
600
- help={t('modelcreator.field.name.hint')}
601
- onChange={(e) => setModelName(e.target.value)}
602
- required
603
- />
604
-
605
- <div className="flex field-bg">
606
- <label htmlFor="modelDescription"><Trans i18nKey={"modelcreator.description"}>Description:</Trans></label>
607
- </div>
608
- <TextField
609
- multiline
610
- help={t('modelcreator.field.description')}
611
- id="modelDescription"
612
- disabled={modelLocked}
613
- value={modelDescription}
614
- onChange={(e) => {
615
- setModelDescription(e.target.value);
616
- setChanged(true)
617
- }}
618
- />
619
-
620
- <div className="flex field-bg">
621
- <label htmlFor="modelIcon"><Trans i18nKey={"modelcreator.icon"}>Icône:</Trans></label>
622
- </div>
623
- <IconField
624
- help={t('modelcreator.field.icon')}
625
- id="modelIcon"
626
- disabled={modelLocked}
627
- value={modelIcon}
628
- onChange={(e) => {
629
- setModelIcon(e);
630
- setChanged(true)
631
- }}
632
- />
633
-
634
- <div className="flex flex-no-wrap">
635
- <div className="checkbox-label flex flex-1">
636
- <CheckboxField
637
- label={<Trans i18nKey={"history"}>Historique</Trans>}
638
- help={t('modelcreator.field.history', '')}
639
- disabled={modelLocked}
640
- checked={!!modelHistory}
641
- onChange={(e) => {
642
- setModelHistory(e? { enabled: true }: false);
643
- }}
644
- />
645
- </div>
646
- </div>
647
-
648
- <h3><Trans i18nKey={"modelcreator.fields"}>Champs du modèle :</Trans></h3>
649
- {fields.map((field, index) => <ModelCreatorField
650
- key={initialModel?.name + '_field_' + index}
651
- handleRemoveValue={handleRemoveValue}
652
- handleAddValue={handleAddValue}
653
- handleRenameField={handleRenameField}
654
- handleUp={handleUp}
655
- handleDown={handleDown}
656
- handleRemoveField={handleRemoveField}
657
- index={index} fields={fields} field={field} model={initialModel} setFields={setFields} />)}
658
- </div>
659
- )}
660
-
661
- {/* Boutons d'action, visibles si un modèle est affiché ou en mode manuel */}
662
- {(showModel || !useAI || initialModel) && (
663
- <div className="actions flex">
664
-
665
- <Button type="button" onClick={handleAddField}>
666
- <FaPlus /> <Trans i18nKey={"btns.addField"}>Ajouter un champ</Trans>
667
- </Button>
668
-
669
- <Button type="submit">
670
- <FaSave />
671
- <Trans i18nKey={"btns.saveModel"}>Enregistrer le modèle</Trans>
672
- </Button>
673
- {initialModel && (
674
- <Button type="button" onClick={handleDelete}>
675
- <FaTrash /><Trans i18nKey={"btns.del"}>Supprimer le modèle</Trans>
676
- </Button>
677
- )}
678
- </div>
679
- )}
680
-
681
- </form>
682
- </div>
683
- );
684
- });
685
- ModelCreator.displayName = "ModelCreator";
686
-
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, IconField, 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 "../../src/constants.js";
18
+ import {useAuthContext} from "./contexts/AuthContext.jsx";
19
+ import {getUserHash, getUserId, isLocalUser} from "../../src/data.js";
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 "../../src/i18n.js";
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 [modelHistory, setModelHistory] = useState(!!initialModel?.history || undefined);
45
+ const [modelIcon, setModelIcon] = useState(!!initialModel?.icon || undefined);
46
+ const [fields, setFields] = useState([]);
47
+ const [changed, setChanged] = useState(false);
48
+
49
+ const [selectedGeneratedModelIndex, setSelectedGeneratedModelIndex] = useState(0); // Pour l'index du modèle choisi
50
+
51
+ useEffect(() => {
52
+ if (initialModel) {
53
+ // Mode édition : on charge les données du modèle existant
54
+ setModelName(initialModel.name || '');
55
+ setModelMaxRequestData(initialModel.maxRequestData || defaultMaxRequestData);
56
+ setModelDescription(initialModel.name ? t(`model_description_${initialModel.name}`, initialModel.description) : '');
57
+ setFields([...(initialModel.fields || []).map(m => ({...m}))]);
58
+ setModelHistory(initialModel.history);
59
+ setModelIcon(initialModel.icon);
60
+ } else {
61
+ // Mode création : on réinitialise tout pour une nouvelle génération
62
+ setModelName('');
63
+ setModelDescription('');
64
+ setFields([]);
65
+ setUseAI(true); // On active l'IA par défaut
66
+ setModelHistory(false);
67
+ setModelIcon(null);
68
+ }
69
+ }, [initialModel]);
70
+
71
+ useEffect(() => {
72
+ if (generatedModels.length > 0 && selectedGeneratedModelIndex >= 0) {
73
+ const selectedModel = generatedModels[selectedGeneratedModelIndex];
74
+ if (selectedModel) {
75
+ setModelName(selectedModel.name || '');
76
+ setModelDescription(selectedModel.description || '');
77
+ setFields(selectedModel.fields || []);
78
+ setModelMaxRequestData(selectedModel.maxRequestData || defaultMaxRequestData);
79
+ setModelIcon(selectedModel.icon || null);
80
+ }
81
+ }
82
+ }, [generatedModels, selectedGeneratedModelIndex]);
83
+
84
+ useEffect(() => {
85
+ if( !changed)
86
+ setModelDescription(initialModel?.name ? t('model_description_'+initialModel?.name, initialModel?.description) : '');
87
+ }, [lang]);
88
+
89
+ const mutationRename = useMutation(
90
+ ({oldName, newName}) => {
91
+ const url = `/api/model/${initialModel._id}/renameField?_user=${encodeURIComponent(getUserId(me))}`;
92
+ return fetch(url, {
93
+ method: 'PATCH',
94
+ headers: { 'Content-Type': 'application/json' },
95
+ body: JSON.stringify({ oldFieldName: oldName, newFieldName: newName }),
96
+ }).then((res) => res.json());
97
+ },
98
+ {
99
+ onSuccess: (data) => {
100
+ setDatasToLoad(datas=>[...datas, modelName]);
101
+ queryClient.invalidateQueries('api/models');
102
+ queryClient.invalidateQueries(['api/data', modelName, 'page', page, elementsPerPage, pagedFilters[modelName], pagedSort[modelName]]);
103
+ },
104
+ onError: (error) => {
105
+ console.error('Erreur:', error);
106
+ },
107
+ }
108
+ );
109
+
110
+ const { me } = useAuthContext()
111
+
112
+ const {addNotification} = useNotificationContext()
113
+
114
+ const mutation = useMutation(
115
+ (modelData) => {
116
+ const url = initialModel?._id ? `/api/model/${initialModel._id}` : '/api/model'; // Utilisation de initialModel pour l'URL
117
+ const method = initialModel?._id ? 'PUT' : 'POST';
118
+ return fetch(url+'?_user='+me.username, {
119
+ method: method,
120
+ headers: { 'Content-Type': 'application/json' },
121
+ body: JSON.stringify(modelData),
122
+ }).then((res) => res.json());
123
+ },
124
+ {
125
+ onSuccess: (data) => {
126
+
127
+ const notificationData = {
128
+ title: data.success ? t('modelcreator.success', 'Modèle sauvegardé') : t(data.error, data.error),
129
+ icon: data.success ? <FaInfo /> : undefined,
130
+ status: data.success ? 'completed': 'error'
131
+ };
132
+ addNotification(notificationData);
133
+
134
+ if (!initialModel) {
135
+ setModelName('');
136
+ setModelDescription('');
137
+ setModelMaxRequestData(defaultMaxRequestData);
138
+ setFields([{ name: '', type: 'string' }]);
139
+ setModelHistory(undefined);
140
+ setModelIcon(null);
141
+ }
142
+ setDatasToLoad(datas=>[...datas, modelName]);
143
+
144
+ queryClient.invalidateQueries(['api/data', modelName, 'page', page, elementsPerPage, pagedFilters[modelName], pagedSort[modelName]]);
145
+ queryClient.invalidateQueries('api/models');
146
+
147
+
148
+ },
149
+ onError: (error) => {
150
+ const notificationData = {
151
+ title: t(error, error),
152
+ status: 'error'
153
+ };
154
+ addNotification(notificationData);
155
+ console.error('Erreur:', error);
156
+ },
157
+ }
158
+ );
159
+
160
+ const handleAddField = () => {
161
+ setFields([...fields, { name: '', type: 'string', _isNewField: true }]);
162
+ };
163
+
164
+ const handleRemoveField = (index) => {
165
+ const newFields = [...fields];
166
+ newFields.splice(index, 1);
167
+ setFields(newFields);
168
+ };
169
+ const deleteMutation = useMutation(
170
+ (modelName) =>
171
+ fetch(`/api/model?name=${modelName}&_user=${encodeURIComponent(getUserId(me))}`, {
172
+ method: 'DELETE',
173
+ }).then((res) => res.json()),
174
+ {
175
+ onSuccess: () => {
176
+ queryClient.invalidateQueries('api/models');
177
+ if (onModelSaved) {
178
+ onModelSaved(); // Vous pouvez passer un argument ici si nécessaire
179
+ }
180
+ // Réinitialiser le formulaire ou rediriger, selon vos besoins
181
+ setModelName('');
182
+ setModelDescription('');
183
+ setSelectedModel(null)
184
+ setModelHistory(undefined)
185
+ setModelIcon(null);
186
+ setFields([{ name: '', type: 'string', _isNewField: true }]);
187
+ setDatasToLoad(datas => datas.filter(f => f !== modelName));
188
+ setGeneratedModels(mods => {
189
+ return mods.filter(m => m.name !== initialModel.name);
190
+ });
191
+ const notificationData = {
192
+ title: t('modelcreator.deleteSuccess', 'Modèle supprimé'),
193
+ icon: <FaInfo />,
194
+ status: 'completed'
195
+ };
196
+ addNotification(notificationData);
197
+ },
198
+ onError: (error) => {
199
+ console.error('Erreur lors de la suppression:', error);
200
+
201
+ const notificationData = {
202
+ title: t(error, error),
203
+ status: 'error'
204
+ };
205
+ addNotification(notificationData);
206
+ },
207
+ }
208
+ );
209
+
210
+ const handleDelete = () => {
211
+ if (initialModel && window.confirm("Êtes-vous sûr de vouloir supprimer ce modèle ?")) {
212
+ deleteMutation.mutate(initialModel.name);
213
+ }
214
+ };
215
+ const handleSubmit = (e) => {
216
+ e.preventDefault();
217
+ const newModel = {
218
+ name: modelName,
219
+ description: modelDescription,
220
+ maxRequestData: modelMaxRequestData,
221
+ history: modelHistory,
222
+ icon: modelIcon,
223
+ fields: (fields || []).map((field) => {
224
+ delete field['_isNewField'];
225
+ let otherFields = [];
226
+ // Check for specific field types
227
+ switch ((field.type)) {
228
+ case 'relation':
229
+ otherFields = ['relation', 'multiple', 'relationFilter'];
230
+ break;
231
+ case 'enum':
232
+ {
233
+ otherFields = ['items']
234
+ break;
235
+ }
236
+ case 'number':
237
+ otherFields = ['min', 'max', 'step', 'unit','delay', 'gauge', 'percent'];
238
+ break;
239
+ case 'string':
240
+ case 'string_t':
241
+ case 'richtext':
242
+ case 'url':
243
+ case 'email':
244
+ case 'phone':
245
+ case 'password':
246
+ case 'code':
247
+ const t = (field.itemsType || field.type);
248
+ if (t === 'code')
249
+ otherFields = ['maxlength', 'language', 'conditionBuilder'];
250
+ else if( ['string_t', 'string'].includes(t))
251
+ otherFields = ['maxlength', 'multiline', 'mask', 'replacement'];
252
+ else
253
+ otherFields = ['maxlength'];
254
+ break;
255
+ case 'date':
256
+ case 'datetime':
257
+ {
258
+ otherFields = ['min','max'];
259
+ break;
260
+ }
261
+ case 'image':
262
+ case 'file':
263
+ otherFields = ['mimeTypes', 'maxSize'];
264
+ break;
265
+ case 'calculated':
266
+ otherFields = ['calculation'];
267
+ break;
268
+ case 'array':
269
+ otherFields = ['itemsType'];
270
+ break;
271
+ default:
272
+ }
273
+ Object.keys(field).forEach(f => {
274
+ if( ![...allowedFields, ...otherFields].includes(f))
275
+ delete field[f];
276
+ });
277
+ return field;
278
+ }),
279
+ };
280
+ setGeneratedModels(gen => gen.filter(m => m.name !== newModel.name));
281
+ mutation.mutateAsync(newModel).then(data => {
282
+ queryClient.invalidateQueries('api/models');
283
+ newModel.fields.forEach(field => {
284
+ if( field.type === "relation") {
285
+ queryClient.invalidateQueries(['api/data', field.relation, relationIds[field.relation]]);
286
+ }
287
+ })
288
+ if (onModelSaved) {
289
+ onModelSaved(data.data);
290
+ }
291
+ });
292
+
293
+ };
294
+ const swapElements = (array, index1, index2) => {
295
+ if( index1 < 0 || index2 < 0 || index1 >= array.length || index2 >= array.length)
296
+ return;
297
+ let temp = array[index1];
298
+ array[index1] = array[index2];
299
+ array[index2] = temp;
300
+ };
301
+
302
+ const handleUp = (index) => {
303
+ const nf = [...fields];
304
+ swapElements(nf, index, index -1);
305
+ setFields(nf);
306
+ };
307
+ const handleDown = (index) => {
308
+ const nf = [...fields];
309
+ swapElements(nf, index, index + 1);
310
+ setFields(nf);
311
+ };
312
+
313
+ const handleAddValue = (fi) => {
314
+ const newFields = [...fields];
315
+ if( !newFields[fi].items )
316
+ newFields[fi].items = [];
317
+ newFields[fi].items.push("");
318
+ setFields(newFields)
319
+ }
320
+ const handleRemoveValue = (fi, index) => {
321
+ const newFields = [...fields];
322
+ const field = newFields[fi];
323
+ field.items = field.items.filter((f,i) => i !== index);
324
+ setFields(newFields)
325
+ }
326
+
327
+ const handleRenameField = (fi, oldVal) => {
328
+ const prompted = window.prompt(t('core.renameFields', 'Renommer le champ'), oldVal);
329
+ if (prompted && prompted !== oldVal) {
330
+ mutationRename.mutateAsync({oldName: oldVal, newName: prompted}).then(e =>{
331
+ const newFields = [...fields];
332
+ const field = newFields[fi];
333
+ field.name = prompted;
334
+ setFields(newFields)
335
+ });
336
+ }
337
+ }
338
+
339
+
340
+ const modelLocked = !!initialModel && (!initialModel?._user ? true : isLocalUser(me) && initialModel?.locked);
341
+
342
+ const hint = (description) => t(description, '') && <div className="hint-icon"><FaCircleInfo data-tooltip-id={`tooltipHint`} data-tooltip-content={description} /></div>
343
+
344
+ const handleMoveDown = (field, index, i) => {
345
+ const newFields = [...fields];
346
+ if( i + 1 >= newFields[index].items.length )
347
+ return;
348
+ const r = newFields[index].items[i + 1];
349
+ newFields[index].items[i + 1] = newFields[index].items[i];
350
+ newFields[index].items[i] = r;
351
+ setFields(newFields);
352
+ }
353
+
354
+ const handleMoveUp = (field, index, i) => {
355
+ const newFields = [...fields];
356
+ if( i < 1 )
357
+ return;
358
+ const r = newFields[index].items[i - 1];
359
+ newFields[index].items[i - 1] = newFields[index].items[i];
360
+ newFields[index].items[i] = r;
361
+ setFields(newFields);
362
+ }
363
+
364
+ const [useAI, setUseAI] = useState(true);
365
+ const [showModel, setModelVisible] = useState(false);
366
+ const [prompt, setPrompt] = useState('');
367
+
368
+ const [homePrompt, setHomePrompt] = useLocalStorage("ai_model_prompt", null);
369
+ const [promptResult, setPromptResult] = useLocalStorage("ai_model_prompt_result", null);
370
+
371
+ // NOUVEAU : Créer une référence pour suivre le déclenchement
372
+ const hasTriggeredAutoGenerate = useRef(false);
373
+
374
+ const[generationIsLoading, setGenerationIsLoading] =useState(false);
375
+
376
+ const generateModelMutation = useMutation(
377
+ async ({userPrompt, modelToEdit}) => {
378
+ const bodyPayload = { prompt: userPrompt };
379
+ // Si on est en mode édition, on ajoute le modèle au corps de la requête
380
+ if (modelToEdit) {
381
+ bodyPayload.existingModel = modelToEdit;
382
+ }
383
+
384
+ const response = await fetch(`/api/model/generate?lang=${lang}`, {
385
+ method: 'POST',
386
+ headers: { 'Content-Type': 'application/json' },
387
+ body: JSON.stringify(bodyPayload),
388
+ });
389
+
390
+ if (!response.ok) {
391
+ const err = await response.json();
392
+ throw new Error(err.error || 'Failed to generate model');
393
+ }
394
+
395
+ return response.json();
396
+ },
397
+ {
398
+ onSuccess: (data) => {
399
+ const modelsList = data?.models || data || [];
400
+
401
+ if (modelsList && Array.isArray(modelsList) && modelsList.length > 0) {
402
+ const mds = modelsList.map(m => ({ ...m, _user: getUserId(me) }));
403
+ setGeneratedModels(g=>[...g, ...mds]);
404
+ onModelGenerated?.(mds);
405
+ modelsList.forEach(d => gtag('event', 'model generation "' + d.name + '"'));
406
+
407
+ setSelectedGeneratedModelIndex(0); // Sélectionne le premier par défaut
408
+ setModelVisible(true); // Affiche la section avec la liste et le formulaire
409
+ setHomePrompt(null);
410
+ addNotification({ title: t('modelcreator.generate.success'), status: 'completed' });
411
+ } else {
412
+ // GA ne retourne aucun modèle
413
+ setModelVisible(false);
414
+ setHomePrompt(null);
415
+ addNotification({
416
+ title: t('modelcreator.generate.error'),
417
+ description: t('modelcreator.generate.no_models', "Aucun modèle n'a pu être généré."),
418
+ status: 'error'
419
+ });
420
+ }
421
+ setGenerationIsLoading(false);
422
+ },
423
+ onError: (error) => {
424
+ setPromptResult(false);
425
+ addNotification({
426
+ title: t('modelcreator.generate.error'),
427
+ description: error.message,
428
+ status: 'error'
429
+ });
430
+ },
431
+ }
432
+ );
433
+
434
+ const handleGenerate = () => {
435
+ if( prompt?.trim() ){
436
+ gtag("event", "generate model by AI");
437
+ setPromptResult(null);
438
+ setGenerationIsLoading(true);
439
+ generateModelMutation.mutate({
440
+ userPrompt: prompt,
441
+ modelToEdit: initialModel
442
+ });
443
+ }
444
+ };
445
+
446
+ useEffect(() => {
447
+ // on ne veut la génération du prompt que si il y a un prompt en attente
448
+ if(homePrompt && !hasTriggeredAutoGenerate.current){
449
+ setPrompt(homePrompt);
450
+ hasTriggeredAutoGenerate.current = true;
451
+ setUseAI(true);
452
+ setGenerationIsLoading(true);
453
+ generateModelMutation.mutate({
454
+ userPrompt: homePrompt,
455
+ modelToEdit: initialModel
456
+ });
457
+ }
458
+
459
+ }, [initialModel]); // Le tableau vide est toujours correct
460
+ return (
461
+ <div className="model-creator" ref={ref}>
462
+
463
+
464
+ <Tooltip id={"tooltipHint"}
465
+ place={"top-end"}
466
+ globalCloseEvents={{ clickOutsideAnchor: true }}
467
+ render={({ content, activeAnchor }) => {
468
+ gtag('render hint ' + content);
469
+ const c = t(content, content);
470
+ return c && (
471
+ <><p className="ws-pre-line">{c}</p></>)
472
+ }} />
473
+ <h2>
474
+ {!initialModel && <Trans i18nKey={"btns.createModel"}>Créer un modèle</Trans>}
475
+ {!!initialModel && <><Trans i18nKey={"btns.editModel"}>Editer le modèle</Trans> &#34;{t(`model_${modelName}`, modelName)}&#34;</>}
476
+ </h2>
477
+ <form onSubmit={handleSubmit}>
478
+
479
+ {/* Section pour choisir le mode de création (IA ou Manuel) */}
480
+ {!initialModel && (
481
+ <CheckboxField
482
+ label={<Trans i18nKey="modelcreator.useAI" />}
483
+ checked={useAI}
484
+ onChange={() => setUseAI(!useAI)}
485
+ />
486
+ )}
487
+
488
+ {/* Section pour le prompt de l'IA (uniquement si création par IA et avant génération) */}
489
+ {useAI && !selectedModel && !showModel && (
490
+ <>
491
+ <div className="ai-prompt-container">
492
+ <>
493
+ <TextField
494
+ value={prompt}
495
+ onChange={(e) => setPrompt(e.target.value)}
496
+ multiline
497
+ help={t("modelcreator.generate.help")}
498
+ required
499
+ disabled={generationIsLoading}
500
+ />
501
+ <div className="examples" dangerouslySetInnerHTML={{ __html: t('modelcreator.generate.examples') }} />
502
+ <Button
503
+ role={"button"}
504
+ onClick={handleGenerate}
505
+ disabled={generationIsLoading}
506
+ >
507
+ {generationIsLoading
508
+ ? <Trans i18nKey="modelcreator.generating" />
509
+ : <Trans i18nKey="modelcreator.generate" />
510
+ }
511
+ </Button>
512
+ </>
513
+ </div>
514
+ {generationIsLoading && <p><Trans i18nKey="modelcreator.generating" /></p>}
515
+ </>
516
+ )}
517
+
518
+ {/* Layout principal pour afficher la liste et le formulaire côte à côte APRES génération */}
519
+ {useAI && showModel && !initialModel && (
520
+ <div className="flex model-generation-layout">
521
+ {/* Colonne de gauche: Liste des modèles générés */}
522
+ {generatedModels.some(g => models.find(f => f.name === g.name && f._user === g._user)) && (
523
+ <>
524
+ <div className="generated-models-list">
525
+ <h4>{t('modelcreator.generate.results_title', 'Suggestions de l\'IA')}</h4>
526
+ <ul>
527
+ {generatedModels.map((model, index) => (
528
+ <li
529
+ key={index}
530
+ className={index === selectedGeneratedModelIndex ? 'active' : ''}
531
+ onClick={() => setSelectedGeneratedModelIndex(index)}
532
+ >
533
+ {model.name}
534
+ </li>
535
+ ))}
536
+ </ul>
537
+ </div>
538
+
539
+ {/* Colonne de droite: Formulaire du modèle sélectionné */}
540
+ <div className="model-form-container">
541
+ {/* Le formulaire existant est placé ici */}
542
+ <div className="field">
543
+ <div className="flex field-bg">
544
+ <label htmlFor="modelName"><Trans i18nKey={"modelcreator.name"}>Nom:</Trans></label>
545
+ </div>
546
+ <TextField
547
+ type="text"
548
+ id="modelName"
549
+ disabled={modelLocked}
550
+ value={modelName}
551
+ help={t('modelcreator.field.name.hint')}
552
+ onChange={(e) => setModelName(e.target.value)}
553
+ required
554
+ />
555
+ </div>
556
+
557
+ <div className="field">
558
+ <div className="checkbox-label flex flex-1">
559
+ <CheckboxField
560
+ label={<Trans i18nKey={"history"}>Historique</Trans>}
561
+ help={t('modelcreator.field.history', '')}
562
+ disabled={modelLocked}
563
+ checked={!!modelHistory}
564
+ onChange={(e) => {
565
+ setModelHistory(e? { enabled: true }: false);
566
+ }}
567
+ />
568
+ </div>
569
+ </div>
570
+
571
+
572
+ <h3><Trans i18nKey={"modelcreator.fields"}>Champs du modèle :</Trans></h3>
573
+ {fields.map((field, index) => <ModelCreatorField
574
+ key={initialModel?.name + '_field_' + index}
575
+ handleRemoveValue={handleRemoveValue}
576
+ handleAddValue={handleAddValue}
577
+ handleRenameField={handleRenameField}
578
+ handleUp={handleUp}
579
+ handleDown={handleDown}
580
+ handleRemoveField={handleRemoveField}
581
+ index={index} fields={fields} field={field} model={initialModel} setFields={setFields} />)}
582
+ </div></>)}
583
+ </div>
584
+ )}
585
+
586
+ {/* Affichage du formulaire en mode manuel ou édition */}
587
+ {(!useAI || !!initialModel) && (
588
+ <div className="model-form-container">
589
+ <div className="flex field-bg">
590
+ <label htmlFor="modelName"><Trans i18nKey={"modelcreator.name"}>Nom:</Trans></label>
591
+ </div>
592
+ <TextField
593
+ type="text"
594
+ id="modelName"
595
+ disabled={modelLocked}
596
+ value={modelName}
597
+ help={t('modelcreator.field.name.hint')}
598
+ onChange={(e) => setModelName(e.target.value)}
599
+ required
600
+ />
601
+
602
+ <div className="flex field-bg">
603
+ <label htmlFor="modelDescription"><Trans i18nKey={"modelcreator.description"}>Description:</Trans></label>
604
+ </div>
605
+ <TextField
606
+ multiline
607
+ help={t('modelcreator.field.description')}
608
+ id="modelDescription"
609
+ disabled={modelLocked}
610
+ value={modelDescription}
611
+ onChange={(e) => {
612
+ setModelDescription(e.target.value);
613
+ setChanged(true)
614
+ }}
615
+ />
616
+
617
+ <div className="flex field-bg">
618
+ <label htmlFor="modelIcon"><Trans i18nKey={"modelcreator.icon"}>Icône:</Trans></label>
619
+ </div>
620
+ <IconField
621
+ help={t('modelcreator.field.icon')}
622
+ id="modelIcon"
623
+ disabled={modelLocked}
624
+ value={modelIcon}
625
+ onChange={(e) => {
626
+ setModelIcon(e);
627
+ setChanged(true)
628
+ }}
629
+ />
630
+
631
+ <div className="flex flex-no-wrap">
632
+ <div className="checkbox-label flex flex-1">
633
+ <CheckboxField
634
+ label={<Trans i18nKey={"history"}>Historique</Trans>}
635
+ help={t('modelcreator.field.history', '')}
636
+ disabled={modelLocked}
637
+ checked={!!modelHistory}
638
+ onChange={(e) => {
639
+ setModelHistory(e? { enabled: true }: false);
640
+ }}
641
+ />
642
+ </div>
643
+ </div>
644
+
645
+ <h3><Trans i18nKey={"modelcreator.fields"}>Champs du modèle :</Trans></h3>
646
+ {fields.map((field, index) => <ModelCreatorField
647
+ key={initialModel?.name + '_field_' + index}
648
+ handleRemoveValue={handleRemoveValue}
649
+ handleAddValue={handleAddValue}
650
+ handleRenameField={handleRenameField}
651
+ handleUp={handleUp}
652
+ handleDown={handleDown}
653
+ handleRemoveField={handleRemoveField}
654
+ index={index} fields={fields} field={field} model={initialModel} setFields={setFields} />)}
655
+ </div>
656
+ )}
657
+
658
+ {/* Boutons d'action, visibles si un modèle est affiché ou en mode manuel */}
659
+ {(showModel || !useAI || initialModel) && (
660
+ <div className="actions flex">
661
+
662
+ <Button type="button" onClick={handleAddField}>
663
+ <FaPlus /> <Trans i18nKey={"btns.addField"}>Ajouter un champ</Trans>
664
+ </Button>
665
+
666
+ <Button type="submit">
667
+ <FaSave />
668
+ <Trans i18nKey={"btns.saveModel"}>Enregistrer le modèle</Trans>
669
+ </Button>
670
+ {initialModel && (
671
+ <Button type="button" onClick={handleDelete}>
672
+ <FaTrash /><Trans i18nKey={"btns.del"}>Supprimer le modèle</Trans>
673
+ </Button>
674
+ )}
675
+ </div>
676
+ )}
677
+
678
+ </form>
679
+ </div>
680
+ );
681
+ });
682
+ ModelCreator.displayName = "ModelCreator";
683
+
687
684
  export default ModelCreator;