data-primals-engine 1.2.4 → 1.2.5

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.
@@ -58,9 +58,7 @@ import {HiddenableCell} from "./HiddenableCell.jsx";
58
58
  import ConditionBuilder from "./ConditionBuilder.jsx";
59
59
  import {pagedFilterToMongoConds} from "./filter.js";
60
60
  import {isConditionMet} from "../../src/filter";
61
-
62
- // Ajoutez cette constante pour la clé de sessionStorage
63
- const SESSION_STORAGE_IMPORT_JOBS_KEY = 'activeImportJobs';
61
+ import {DataImporter} from "./DataImporter.jsx";
64
62
 
65
63
  const Header = ({
66
64
  reversed = false,
@@ -699,365 +697,3 @@ export function DataTable({
699
697
  );
700
698
  }
701
699
 
702
-
703
- function DataImporter({onClose}) {
704
-
705
- const [file, setFile] = useState(null);
706
- const {selectedModel, page} = useModelContext();
707
-
708
- const {me} = useAuthContext();
709
- const {t, i18n} = useTranslation();
710
-
711
- const queryClient = useQueryClient();
712
- const {addNotification} = useNotificationContext();
713
-
714
- const [hasHeaders, setHasHeaders] = useState(true);
715
- const [csvHeaders, setCSVHeaders] = useState(selectedModel.fields.map(field => field.name));
716
-
717
- // --- MODIFIÉ : État pour gérer plusieurs tâches d'importation ---
718
- // Cet objet stockera les données de progression de chaque tâche, indexées par leur jobId
719
- const [importJobs, setImportJobs] = useState({});
720
- // Cette liste stockera les IDs des tâches qui sont actuellement suivies via SSE
721
- const [activeJobIds, setActiveJobIds] = useState([]);
722
-
723
- // Référence pour stocker les instances EventSource, indexées par jobId
724
- const eventSourceRefs = useRef({});
725
-
726
- const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
727
-
728
- const [storedJobIds, setStoredJobIds] = useLocalStorage(SESSION_STORAGE_IMPORT_JOBS_KEY, []);
729
- // --- NOUVEAU : Charger/Sauvegarder les IDs des tâches actives depuis sessionStorage ---
730
- useEffect(() => {
731
- // Charger les IDs des tâches actives depuis sessionStorage au montage
732
- setActiveJobIds(storedJobIds);
733
-
734
- // Pour chaque jobId stockpour obtenir le dernier statut
735
- storedJobIds.forEach(jobId => {
736
- startProgressTracking(jobId);
737
- });
738
-
739
- // Fonction de nettoyage : fermer toutes les connexions EventSource lors du démontage du composant
740
- return () => {
741
- Object.values(eventSourceRefs.current).forEach(es => es.close());
742
- eventSourceRefs.current = {}; // Effacer les références
743
- };
744
- }, []); // S'exécute une seule fois au montage
745
-
746
- // --- NOUVEAU : Effet pour mettre à jour sessionStorage lorsque activeJobIds change ---
747
- useEffect(() => {
748
- setStoredJobIds(activeJobIds);
749
- }, [activeJobIds]);
750
-
751
-
752
- // Mutation pour initier l'importation (envoi du fichier au serveur)
753
- const {isLoading, mutate: importMutation} = useMutation(async () => {
754
- console.log('Initiating data import...');
755
- const params = new FormData();
756
- params.append('model', selectedModel?.name);
757
- params.append("_user", getUserId(me));
758
- params.append("hasHeaders", !!hasHeaders);
759
- params.append("csvHeaders", csvHeaders.join(','));
760
- if (file) {
761
- params.append("file", file);
762
- } else {
763
- addNotification({ title: t('dataimporter.noFileSelected', 'Veuillez sélectionner un fichier à importer.'), status: 'warning' });
764
- return Promise.reject(new Error("No file selected"));
765
- }
766
-
767
- try {
768
- const response = await fetch(`/api/data/import?lang=${lang}`, {
769
- method: 'POST',
770
- body: params
771
- });
772
-
773
- if (response.status === 202) {
774
- const { job } = await response.json();
775
- const { jobId} = job;
776
-
777
- // --- MODIFIÉ : Ajouter le nouvel jobId à activeJobIds et à l'état importJobs ---
778
- setActiveJobIds(prevIds => [...prevIds, jobId]);
779
- setImportJobs(prevJobs => ({
780
- ...prevJobs,
781
- [jobId]: {
782
- jobId,
783
- status: 'pending',
784
- totalRecords: 0,
785
- processedRecords: 0,
786
- errors: [],
787
- // Ajoutez d'autres champs initiaux que vous souhaitez afficher immédiatement
788
- }
789
- }));
790
- startProgressTracking(jobId); // Commencer le suivi de cette nouvelle tâche
791
- addNotification({
792
- title: t('dataimporter.initiated', 'Importation initiée. Suivi de la progression...'),
793
- icon: <FaInfo/>,
794
- status: 'info'
795
- });
796
- } else {
797
- const errorData = await response.json();
798
- addNotification({
799
- title: errorData.error || t('dataimporter.error', 'Erreur lors de l\'importation.'),
800
- status: 'error'
801
- });
802
- }
803
- } catch (e) {
804
- addNotification({
805
- title: e.message || t('dataimporter.networkError', 'Erreur réseau lors de l\'importation.'),
806
- status: 'error'
807
- });
808
- }
809
- });
810
-
811
- // Fonction pour démarrer le suivi de la progression via Server-Sent Events (SSE) pour un jobId spécifique
812
-
813
- // Fonction pour démarrer le suivi de la progression via Server-Sent Events (SSE) pour un jobId spécifique
814
- const startProgressTracking = (jobId) => {
815
- // Fermer toute connexion EventSource existante pour ce jobId pour éviter les doublons
816
- if (eventSourceRefs.current[jobId]) {
817
- eventSourceRefs.current[jobId].close();
818
- }
819
-
820
- const eventSource = new EventSource(`/api/import/progress/${jobId}`);
821
- eventSourceRefs.current[jobId] = eventSource; // Stocker l'instance pour le nettoyage
822
-
823
- eventSource.onmessage = (event) => {
824
- const data = JSON.parse(event.data);
825
- // --- MODIFIÉ : Mettre à jour la progression de la tâche spécifique ---
826
- setImportJobs(prevJobs => ({
827
- ...prevJobs,
828
- [jobId]: data
829
- }));
830
-
831
- // Si la tâche est terminée (succès ou échec), fermer la connexion SSE.
832
- // NE PAS la retirer de activeJobIds ici, pour qu'elle persiste au rafraîchissement.
833
- // Elle sera retirar le bouton "Effacer".
834
- if (data.status === 'completed' || data.status === 'failed' || data.status === 'not_found') {
835
- eventSource.close();
836
- delete eventSourceRefs.current[jobId]; // Supprimer la référence de l'EventSource
837
-
838
- // --- LIGNE MODIFIÉE/SUPPRIMÉE ---
839
- // Supprimez ou commentez la ligne suivante :
840
- // setActiveJobIds(prevIds => prevIds.filter(id => id !== jobId));
841
-
842
- queryClient.invalidateQueries(['api/data', selectedModel.name, 'page', page]); // Rafraîchir les données du tableau
843
-
844
- if (data.status === 'completed') {
845
- addNotification({
846
- title: t('dataimporter.success', 'Importation des données réussie.'),
847
- icon: <FaInfo/>,
848
- status: 'completed'
849
- });
850
- } else if (data.status === 'failed') {
851
- addNotification({
852
- title: t('dataimporter.failed', 'Importation échouée. Voir les détails pour les erreurs.'),
853
- status: 'error'
854
- });
855
- } else if (data.status === 'not_found') {
856
- addNotification({
857
- title: t('dataimporter.jobNotFound', 'Tâche d\'importation non trouvée ou déjà terminée.'),
858
- status: 'warning'
859
- });
860
- }
861
- }
862
- };
863
-
864
- eventSource.onerror = (error) => {
865
- console.error(`EventSource error for job ${jobId}:`, error);
866
- eventSource.close();
867
- delete eventSourceRefs.current[jobId];
868
- setStoredJobIds(prevIds => prevIds.filter(id => id !== jobId));
869
- };
870
- };
871
-
872
- const handleImportClick = () => {
873
- importMutation();
874
- };
875
-
876
- const handleCloseModal = () => {
877
- // Fermer toutes les connexions EventSource avant de fermer la modale
878
- Object.values(eventSourceRefs.current).forEach(es => es.close());
879
- eventSourceRefs.current = {}; // Effacer les références
880
- onClose();
881
- };
882
-
883
- // Déterminer si une importation est actuellement en cours (pour désactiver les boutons)
884
- const isAnyImportInProgress = Object.values(importJobs).some(job => job.status === 'pending' || job.status === 'processing');
885
-
886
- // Filtrer et trier les tâches à afficher (par exemple, les tâches en cours en premier)
887
- const jobsToDisplay = Object.values(importJobs).sort((a, b) => {
888
- // Trier par statut (en attente/en cours en premier, puis échoué, puis terminé)
889
- const statusOrder = { 'pending': 1, 'processing': 2, 'failed': 3, 'completed': 4, 'not_found': 5 };
890
- return statusOrder[a.status] - statusOrder[b.status];
891
- });
892
-
893
- return (
894
- <Dialog isClosable={true} isModal={true} onClose={handleCloseModal}>
895
- <>
896
- <h2>
897
- <Trans i18nKey="dataimporter.title" values={{model: t('model_' + selectedModel?.name, selectedModel?.name)}}>
898
- Importer des données dans {t('model_' + selectedModel?.name, selectedModel?.name)}
899
- </Trans>
900
- </h2>
901
- <p>
902
- <Trans i18nKey="dataimporter.info" values={{constante: (maxBytesPerSecondThrottleData / kilobytes) + 'ko/s'}}></Trans>
903
- </p>
904
-
905
- {/* Toujours afficher le formulaire de sélection de fichier et le bouton d'importation */}
906
- <FileField
907
- name="file"
908
- maxSize={maxFileSize}
909
- mimeTypes={['application/json', 'text/csv']}
910
- type="file"
911
- multiple={false}
912
- onChange={(files) => {
913
- setFile(files && files.length > 0 ? files[files.length - 1].file : null);
914
- }}
915
- />
916
- {file && (file.name.endsWith('.csv') || file.type === 'text/csv') && (
917
- <div className="checkbox-label flex flex-row">
918
- <label htmlFor="hasHeadersCheckbox">
919
- <input
920
- type="checkbox"
921
- id="hasHeadersCheckbox"
922
- checked={hasHeaders}
923
- onChange={(e) => setHasHeaders(e.target.checked)}
924
- />
925
- <Trans i18nKey="dataimporter.hasCsvHeaders"></Trans>
926
- </label>
927
- {!hasHeaders && (
928
- <table>
929
- <thead>
930
- <tr>
931
- <th><Trans i18nKey={"dataimporter.csvColumn"}>Num de colonne du CSV</Trans></th>
932
- <th><Trans i18nKey="dataimporter.field">Champ du modèle</Trans></th>
933
- </tr>
934
- </thead>
935
- <tbody>
936
- {csvHeaders.map((mappedFieldName, index) => {
937
- let fieldObjectForModelField = null;
938
- let currentFieldValueForModelField = null;
939
-
940
- if (mappedFieldName && mappedFieldName.trim() !== '') {
941
- fieldObjectForModelField = selectedModel?.fields.find(f_model => f_model.name === mappedFieldName);
942
- if (fieldObjectForModelField) {
943
- currentFieldValueForModelField = fieldObjectForModelField.name;
944
- }
945
- }
946
-
947
- return (
948
- <tr key={`${selectedModel.name}-csvmap-${index}`}>
949
- <td><Trans i18nKey="dataimporter.column" values={[index + 1]}>colonne {index + 1}</Trans></td>
950
- <td>
951
- <div className="flex">
952
- <ModelField
953
- disableable={true}
954
- showModel={false}
955
- value={selectedModel.name}
956
- fieldValue={currentFieldValueForModelField}
957
- onChange={({name: propName, value: selectedValue}) => {
958
- const newCsvHeaders = [...csvHeaders];
959
- newCsvHeaders[index] = selectedValue?.field ?? '';
960
- setCSVHeaders(newCsvHeaders);
961
- }}
962
- fields={true}
963
- model={selectedModel}
964
- field={fieldObjectForModelField}
965
- />
966
- <Button className="flex" onClick={() => {
967
- const csvH = [...csvHeaders];
968
- setCSVHeaders(csvH.filter((_, i) => i !== index));
969
- }}><FaTrash/></Button>
970
- </div>
971
- </td>
972
- </tr>
973
- );
974
- })}
975
- <tr>
976
- <td colSpan={2}>
977
- <Button onClick={() => {
978
- const csvH = [...csvHeaders];
979
- csvH.push('');
980
- setCSVHeaders(csvH)
981
- }}><Trans i18nKey="dataimporter.addColumn">Ajouter une colonne</Trans></Button>
982
- </td>
983
- </tr>
984
- </tbody>
985
- </table>
986
- )}
987
- </div>
988
- )}
989
- <div>
990
- <Button onClick={handleImportClick} disabled={isLoading || !file}>
991
- <Trans i18nKey="btns.import">Importer</Trans>
992
- </Button>
993
- </div>
994
-
995
- {/* Afficher la progression pour toutes les tâches d'importation actives/suivies */}
996
- {jobsToDisplay.length > 0 && (
997
- <div className="import-jobs-list">
998
- <h3><Trans i18nKey="dataimporter.activeImports">Importations en cours / terminées</Trans></h3>
999
- {jobsToDisplay.map(job => {
1000
- const progressPercentage = job.totalRecords > 0
1001
- ? (job.processedRecords / job.totalRecords) * 100
1002
- : 0;
1003
- const isJobFinished = job.status === 'completed' || job.status === 'failed' || job.status === 'not_found';
1004
-
1005
- return (
1006
- <div key={job.jobId} className="import-progress-container">
1007
- <h4><Trans i18nKey="dataimporter.jobId">Tâche ID:</Trans> {job.jobId?.substring(0, 8)}...</h4>
1008
- <p>
1009
- <Trans i18nKey="dataimporter.status">Statut:</Trans>{' '}
1010
- <strong>{t(`dataimporter.status.${job.status}`, job.status)}</strong>
1011
- </p>
1012
- {job.totalRecords > 0 && (
1013
- <p>
1014
- <Trans i18nKey="dataimporter.recordsProcessed">Enregistrements traités:</Trans>{' '}
1015
- {job.processedRecords} / {job.totalRecords}
1016
- </p>
1017
- )}
1018
- <div className="progress-bar-wrapper">
1019
- <div
1020
- className="progress-bar"
1021
- style={{ width: `${progressPercentage}%` }}
1022
- >
1023
- {progressPercentage.toFixed(0)}%
1024
- </div>
1025
- </div>
1026
-
1027
- {job.errors && job.errors.length > 0 && (
1028
- <div className="import-errors">
1029
- <h4><Trans i18nKey="dataimporter.errors">Erreurs:</Trans></h4>
1030
- <ul>
1031
- {job.errors.map((error, index) => (
1032
- <li key={index}>{error}</li>
1033
- ))}
1034
- </ul>
1035
- </div>
1036
- )}
1037
- {isJobFinished && (
1038
- <div className="flex justify-end mt-2">
1039
- <Button onClick={() => {
1040
- setImportJobs(prevJobs => {
1041
- const newJobs = { ...prevJobs };
1042
- delete newJobs[job.jobId];
1043
- return newJobs;
1044
- });
1045
- setActiveJobIds(prevIds => prevIds.filter(id => id !== job.jobId));
1046
- }}><Trans i18nKey="btns.clear">Effacer</Trans></Button>
1047
- </div>
1048
- )}
1049
- </div>
1050
- );
1051
- })}
1052
- </div>
1053
- )}
1054
-
1055
- <div className="flex justify-end mt-4">
1056
- <Button onClick={handleCloseModal} disabled={isAnyImportInProgress}>
1057
- <Trans i18nKey="btns.close">Fermer</Trans>
1058
- </Button>
1059
- </div>
1060
- </>
1061
- </Dialog>
1062
- );
1063
- }
@@ -70,4 +70,21 @@
70
70
  .flex.justify-end.mt-4 button:disabled {
71
71
  background-color: #cccccc;
72
72
  cursor: not-allowed;
73
- }
73
+ }
74
+ .preview-table {
75
+ width: 100%;
76
+ border-collapse: collapse;
77
+ font-size: 0.9em;
78
+ }
79
+
80
+ .preview-table td {
81
+ border: 1px solid #ddd;
82
+ padding: 4px;
83
+ }
84
+
85
+ .preview-table-container {
86
+ max-height: 300px;
87
+ overflow: auto;
88
+ border: 1px solid #eee;
89
+ margin-bottom: 10px;
90
+ }
@@ -1411,56 +1411,98 @@ export const ModelField = ({field, disableable=false, showModel=true, value, fie
1411
1411
  const {models} = useModelContext();
1412
1412
  const {me} = useAuthContext();
1413
1413
  const {t} = useTranslation()
1414
- const [modelValue, setModelValue] = useState(value);
1415
- const [modelFieldValue, setModelFieldValue] = useState(null);
1416
1414
  const [checked, setChecked] = useState(true);
1417
1415
 
1418
- const itemsFields = [...models.find(f=>f.name === modelValue && me?.username === f._user)?.fields.map(m => ({label: m.name, value: m.name})) || []];
1416
+ // Trouver le modèle correspondant à la valeur
1417
+ const selectedModel = models.find(m => m.name === value && m._user === me?.username);
1419
1418
 
1420
- useEffect(() => {
1421
- setModelValue(value)
1422
- }, [value]);
1419
+ // Préparer les options pour les champs du modèle
1420
+ const fieldOptions = selectedModel?.fields.map(f => ({
1421
+ label: t(`field_${f.name}`, f.name),
1422
+ value: f.name
1423
+ })) || [];
1423
1424
 
1424
- useEffect(() => {
1425
- onChange({name: field.name, value: modelValue});
1426
- }, [modelValue]);
1425
+ // Gestion du changement de modèle
1426
+ const handleModelChange = (e) => {
1427
+ const newModel = e.value;
1428
+ const firstField = fieldOptions[0]?.value || null;
1427
1429
 
1428
- useEffect(() => {
1429
- if( !fieldValue){
1430
- setModelFieldValue(itemsFields.length > 0 ? itemsFields[0].value : null)
1431
- }else
1432
- setModelFieldValue(fieldValue)
1433
- }, [fieldValue]);
1434
-
1435
- const dis = disableable ? <><CheckboxField checked={checked} onChange={e => {
1436
- setChecked(e.target.checked)
1437
- if (!e.target.checked) {
1438
- setModelValue(null);
1430
+ if (fields) {
1431
+ onChange({name: field?.name, value: { model: newModel, field: firstField }});
1432
+ } else {
1433
+ onChange({name: field?.name, value: newModel});
1439
1434
  }
1440
- }} /></> : null;
1441
-
1442
- if( !fields )
1443
- return <div className={"flex flex-1"}>
1444
- {dis}{checked && (<SelectField className="flex-1" value={modelValue} onChange={(e) => {
1445
- setModelValue(e.value)
1446
- onChange({name: field.name, value: e.value});
1447
- }} items={[...models.filter(f=>f._user === me?.username).map(m => ({label: t(`model_${m.name}`, m.name), value: m.name}))]}/>
1448
- )}
1449
- </div>;
1450
- return <div className={"flex flex-1"}>{dis}{checked && (<div className="flex flex-stretch" key={field?.name??'def'}>
1451
- {showModel && (<SelectField disabled={!!checked} className="flex-1" value={modelValue} onChange={(e) => {
1452
- setModelValue(e.value)
1453
- onChange({name: field.name, value: { model: e.value, field: itemsFields[0] }});
1454
- }} items={[...models.filter(f=>f._user === me?.username).map(m => ({label: t(`model_${m.name}`, m.name), value: m.name}))]}/>
1435
+ };
1436
+
1437
+ // Gestion du changement de champ
1438
+ const handleFieldChange = (e) => {
1439
+ onChange({name: field?.name, value: { model: value, field: e.value }});
1440
+ };
1441
+
1442
+ const dis = disableable ? (
1443
+ <CheckboxField
1444
+ checked={checked}
1445
+ onChange={e => {
1446
+ setChecked(e.target.checked);
1447
+ if (!e.target.checked) {
1448
+ onChange({name: field?.name, value: null});
1449
+ }
1450
+ }}
1451
+ />
1452
+ ) : null;
1453
+
1454
+ if (!fields) {
1455
+ return (
1456
+ <div className="flex flex-1">
1457
+ {dis}
1458
+ {checked && (
1459
+ <SelectField
1460
+ className="flex-1"
1461
+ value={value}
1462
+ onChange={handleModelChange}
1463
+ items={models
1464
+ .filter(m => m._user === me?.username)
1465
+ .map(m => ({
1466
+ label: t(`model_${m.name}`, m.name),
1467
+ value: m.name
1468
+ }))
1469
+ }
1470
+ />
1471
+ )}
1472
+ </div>
1473
+ );
1474
+ }
1475
+
1476
+ return (
1477
+ <div className="flex flex-1">
1478
+ {dis}
1479
+ {checked && (
1480
+ <div className="flex flex-stretch" key={field?.name ?? 'def'}>
1481
+ {showModel && (
1482
+ <SelectField
1483
+ className="flex-1"
1484
+ value={value}
1485
+ onChange={handleModelChange}
1486
+ items={models
1487
+ .filter(m => m._user === me?.username)
1488
+ .map(m => ({
1489
+ label: t(`model_${m.name}`, m.name),
1490
+ value: m.name
1491
+ }))
1492
+ }
1493
+ />
1494
+ )}
1495
+ <SelectField
1496
+ className="flex-1"
1497
+ value={fieldValue || (fieldOptions[0]?.value || null)}
1498
+ onChange={handleFieldChange}
1499
+ items={fieldOptions}
1500
+ />
1501
+ </div>
1455
1502
  )}
1456
- <SelectField className="flex-1" value={modelFieldValue} onChange={(e) =>{
1457
- setModelFieldValue(e.value)
1458
- onChange({name: field.name, value: { model: modelValue, field:e.value}});
1459
- }} items={itemsFields}/>
1460
- </div>)}
1461
- </div>
1462
- }
1463
- export const ColorField = ({name, label, value, disabled, onChange, className, ...rest}) => {
1503
+ </div>
1504
+ );
1505
+ };export const ColorField = ({name, label, value, disabled, onChange, className, ...rest}) => {
1464
1506
  // 1. État interne pour une réactivité immédiate de l'interface.
1465
1507
  const [internalValue, setInternalValue] = useState(value);
1466
1508
 
@@ -526,33 +526,37 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
526
526
  {/* Colonne de droite: Formulaire du modèle sélectionné */}
527
527
  <div className="model-form-container">
528
528
  {/* Le formulaire existant est placé ici */}
529
- <div className="flex field-bg">
530
- <label htmlFor="modelName"><Trans i18nKey={"modelcreator.name"}>Nom:</Trans></label>
529
+ <div className="field">
530
+ <div className="flex field-bg">
531
+ <label htmlFor="modelName"><Trans i18nKey={"modelcreator.name"}>Nom:</Trans></label>
532
+ </div>
533
+ <TextField
534
+ type="text"
535
+ id="modelName"
536
+ disabled={modelLocked}
537
+ value={modelName}
538
+ help={t('modelcreator.field.name.hint')}
539
+ onChange={(e) => setModelName(e.target.value)}
540
+ required
541
+ />
531
542
  </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 field-bg">
543
- <label htmlFor="modelDescription"><Trans i18nKey={"modelcreator.description"}>Description:</Trans></label>
543
+
544
+ <div className="field">
545
+ <div className="flex field-bg">
546
+ <label htmlFor="modelDescription"><Trans i18nKey={"modelcreator.description"}>Description:</Trans></label>
547
+ </div>
548
+ <TextField
549
+ multiline
550
+ help={t('modelcreator.field.description')}
551
+ id="modelDescription"
552
+ disabled={modelLocked}
553
+ value={modelDescription}
554
+ onChange={(e) => {
555
+ setModelDescription(e.target.value);
556
+ setChanged(true)
557
+ }}
558
+ />
544
559
  </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
560
  <h3><Trans i18nKey={"modelcreator.fields"}>Champs du modèle :</Trans></h3>
557
561
  {fields.map((field, index) => <ModelCreatorField
558
562
  key={initialModel?.name + '_field_' + index}
@@ -1,5 +1,6 @@
1
1
  .model-creator {
2
2
  flex: 1;
3
+ width: 100%;
3
4
  max-width: 720px;
4
5
  form {
5
6
  padding: 0;
@@ -31,6 +32,9 @@
31
32
  border-radius: 8px;
32
33
  padding: 16px 8px;
33
34
  @media only screen and (min-width: 768px) {
35
+ padding: 1rem; // Plus d'espace intérieur
36
+ }
37
+ @media only screen and (min-width: 1024px) {
34
38
  padding: 1.5rem; // Plus d'espace intérieur
35
39
  }
36
40
  margin-bottom: 1.5rem; // Plus d'espace entre les champs
@@ -129,8 +133,17 @@
129
133
 
130
134
  .model-form-container {
131
135
  flex: 1; // Le formulaire prend le reste de la place
136
+ > .field {
137
+ box-sizing: border-box;
138
+ margin-bottom: 28px;
139
+ }
132
140
  }
133
141
 
142
+ .actions.right {
143
+ margin-right: 0;
144
+ margin-left: auto;
145
+ width: fit-content;
146
+ }
134
147
  // Styles pour la zone de prompt de l'IA
135
148
  .ai-prompt-container {
136
149
  background-color: #f8f9fa;