data-primals-engine 1.4.3 → 1.5.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.
Files changed (77) hide show
  1. package/README.md +913 -867
  2. package/client/package-lock.json +49 -0
  3. package/client/package.json +1 -0
  4. package/client/src/AddWidgetTypeModal.jsx +47 -43
  5. package/client/src/App.jsx +3 -7
  6. package/client/src/App.scss +25 -3
  7. package/client/src/AssistantChat.jsx +363 -323
  8. package/client/src/AssistantChat.scss +30 -12
  9. package/client/src/Dashboard.jsx +480 -396
  10. package/client/src/Dashboard.scss +1 -1
  11. package/client/src/DashboardHtmlViewItem.jsx +147 -0
  12. package/client/src/DashboardView.jsx +104 -19
  13. package/client/src/DataEditor.jsx +12 -5
  14. package/client/src/DataLayout.jsx +805 -762
  15. package/client/src/DataLayout.scss +14 -0
  16. package/client/src/DataTable.jsx +63 -77
  17. package/client/src/Dialog.scss +1 -1
  18. package/client/src/Field.jsx +591 -322
  19. package/client/src/FlexDataRenderer.jsx +2 -0
  20. package/client/src/FlexTreeUtils.js +1 -1
  21. package/client/src/FlexViewCard.jsx +44 -0
  22. package/client/src/HistoryDialog.jsx +47 -14
  23. package/client/src/HtmlViewBuilderModal.jsx +91 -0
  24. package/client/src/HtmlViewBuilderModal.scss +18 -0
  25. package/client/src/HtmlViewCard.jsx +44 -0
  26. package/client/src/HtmlViewCard.scss +35 -0
  27. package/client/src/KPIDialog.jsx +11 -1
  28. package/client/src/KanbanCard.jsx +1 -2
  29. package/client/src/ModelCreator.jsx +6 -6
  30. package/client/src/ModelCreatorField.jsx +74 -31
  31. package/client/src/ModelList.jsx +93 -54
  32. package/client/src/Notification.jsx +136 -136
  33. package/client/src/Notification.scss +0 -18
  34. package/client/src/Pagination.jsx +5 -3
  35. package/client/src/RelationField.jsx +354 -258
  36. package/client/src/RelationSelectorWidget.jsx +173 -0
  37. package/client/src/constants.js +1 -1
  38. package/client/src/contexts/ModelContext.jsx +10 -1
  39. package/client/src/contexts/UIContext.jsx +72 -63
  40. package/client/src/filter.js +262 -212
  41. package/client/src/hooks/useTutorials.jsx +62 -65
  42. package/client/src/hooks/useValidation.js +75 -0
  43. package/client/src/translations.js +26 -24
  44. package/package.json +3 -1
  45. package/perf/README.md +147 -0
  46. package/perf/artillery-hooks.js +37 -0
  47. package/perf/perf-shot-hardwork.yml +84 -0
  48. package/perf/perf-shot-search.yml +45 -0
  49. package/perf/setup.yml +26 -0
  50. package/server.js +1 -1
  51. package/src/constants.js +3 -28
  52. package/src/core.js +15 -1
  53. package/src/data.js +1 -1
  54. package/src/defaultModels.js +63 -7
  55. package/src/email.js +5 -2
  56. package/src/engine.js +5 -3
  57. package/src/filter.js +5 -3
  58. package/src/i18n.js +710 -10
  59. package/src/modules/assistant/assistant.js +151 -19
  60. package/src/modules/bucket.js +14 -16
  61. package/src/modules/data/data.backup.js +11 -8
  62. package/src/modules/data/data.core.js +118 -92
  63. package/src/modules/data/data.history.js +531 -492
  64. package/src/modules/data/data.js +9 -56
  65. package/src/modules/data/data.operations.js +3282 -2999
  66. package/src/modules/data/data.relations.js +686 -686
  67. package/src/modules/data/data.routes.js +118 -24
  68. package/src/modules/data/data.scheduling.js +2 -1
  69. package/src/modules/data/data.validation.js +85 -3
  70. package/src/modules/file.js +247 -236
  71. package/src/modules/user.js +4 -1
  72. package/src/modules/workflow.js +9 -10
  73. package/src/openai.jobs.js +2 -0
  74. package/src/packs.js +5482 -5461
  75. package/src/providers.js +22 -7
  76. package/test/data.integration.test.js +136 -2
  77. package/test/import_export.integration.test.js +1 -1
@@ -12,6 +12,7 @@ import {maxAIReflectiveSteps} from "../../constants.js";
12
12
  import {providers} from "./constants.js";
13
13
  import {ChatDeepSeek} from "@langchain/deepseek";
14
14
  import {ChatAnthropic} from "@langchain/anthropic";
15
+ import {Config} from "../../config.js";
15
16
 
16
17
  let logger = null;
17
18
 
@@ -52,14 +53,40 @@ export const assistantGlobalLimiter = rateLimit({
52
53
  });
53
54
 
54
55
  async function searchModels(query, user) {
55
- if (!query) return [];
56
+ if (!query) return { main: [], related: [] };
56
57
  const searchRegex = new RegExp(query, 'i');
57
- return await modelsCollection.find({
58
+ const mainModels = await modelsCollection.find({
58
59
  $or: [{ _user: user.username }, { _user: { $exists: false } }],
59
60
  $and: [{ $or: [{ name: { $regex: searchRegex } }, { description: { $regex: searchRegex } }] }]
60
61
  }, {
61
62
  projection: { name: 1, description: 1, fields: 1, _id: 0 }
62
63
  }).limit(10).toArray();
64
+
65
+ if (mainModels.length === 0) {
66
+ return { main: [], related: [] };
67
+ }
68
+
69
+ const relatedModelNames = new Set();
70
+ mainModels.forEach(model => {
71
+ model.fields.forEach(field => {
72
+ if (field.type === 'relation' && field.relation) {
73
+ relatedModelNames.add(field.relation);
74
+ }
75
+ });
76
+ });
77
+
78
+ const mainModelNames = new Set(mainModels.map(m => m.name));
79
+ const finalRelatedModelNames = [...relatedModelNames].filter(name => !mainModelNames.has(name));
80
+
81
+ let relatedModels = [];
82
+ if (finalRelatedModelNames.length > 0) {
83
+ relatedModels = await modelsCollection.find({
84
+ name: { $in: finalRelatedModelNames },
85
+ $or: [{ _user: user.username }, { _user: { $exists: false } }]
86
+ }, { projection: { name: 1, description: 1, fields: 1, _id: 0 } }).toArray();
87
+ }
88
+
89
+ return { main: mainModels, related: relatedModels };
63
90
  }
64
91
 
65
92
  const createSystemPrompt = (modelDefs, lang) => {
@@ -86,7 +113,7 @@ Tu as accès aux outils et actions suivants.
86
113
 
87
114
  OUTILS DE RAISONNEMENT INTERNE: (Utilisés pour collecter de l'information avant de décider de l'action finale)
88
115
  1. **search_models**: Pour rechercher les modèles de données disponibles.
89
- - Utilisation: { "action": "search_models", "params": { "query": "^regexToSearchFor$" } }
116
+ - Utilisation: { "action": "search_models", "params": { "query": "^regexToSearchFor$" } }. Il retourne aussi la structure des modèles directement liés.
90
117
  ====
91
118
 
92
119
  ACTIONS FINALES: (Actions qui terminent ta réflexion et renvoient un résultat à l'utilisateur)
@@ -118,9 +145,24 @@ ACTIONS FINALES: (Actions qui terminent ta réflexion et renvoient un résultat
118
145
  - \`yAxis\` (string): (Optionnel, sauf pour sum/avg/min/max) Le champ numérique à agréger du modèle.
119
146
  - \`filter\` (object): (Optionnel, filtre de la recherche, même écriture stricte que pour les filtres de recherche (voir plus bas pour les exemples)
120
147
 
148
+ 7. **generateHtmlView**: Pour créer une vue personnalisée en utilisant un template HTML.
149
+ - Utilisation: { "action": "generateHtmlView", "params": { ...config } }
150
+ - Le paramètre \`config\` doit contenir :
151
+ - \`title\` (string): Un titre pour la vue.
152
+ - \`model\` (string): Le nom du modèle de données à utiliser.
153
+ - \`template\` (string): Un template au format Handlebars.js. **RÈGLE CRITIQUE : N'utilise QUE les noms de champs (\`fieldName\`) exacts fournis par l'outil \`search_models\` pour le modèle principal ET pour ses modèles liés. N'invente JAMAIS de champs.**
154
+ Pour une liste, tu DOIS utiliser une boucle \`{{#each data}}...{{/each}}\`.
155
+ À l'intérieur d'une boucle, accède aux champs avec \`{{this.fieldName}}\`.
156
+ Pour les champs de type \`string_t\` ou \`richtext_t\`, accède à la traduction avec \`{{this.fieldName.value}}\`.
157
+ Les champs de type 'relation' sont automatiquement peuplés (hydratés), tu peux donc accéder à leurs propriétés directement (ex: \`{{this.relationField.name}}\` ou \`{{this.relationField.name.value}}\` si le champ 'name' de la relation est un 'string_t').
158
+ - \`css\` (string): (Optionnel) Du CSS riche et créatif pour styliser le template. N'hésite pas à utiliser des dégradés, des ombres, des animations et des polices de caractères pour un rendu professionnel et attrayant. **Règle absolue : tu dois préfixer TOUS tes sélecteurs avec \`#{{containerId}}\` pour isoler les styles.**
159
+ - \`filter\` (object): (Optionnel) Un filtre pour sélectionner les documents à afficher.
160
+ - \`limit\` (number): (Optionnel, défaut 10) Le nombre maximum de documents à récupérer.
161
+
121
162
  Voici le mémo pour assigner des valeurs aux champs des modèles,avec ces types de données :
122
163
  utilise une chaine de caractère convertible en ObjectId (mongodb) lorsque le nom du champ est _id
123
- utilise une chaine de caracteres lorsque le type de champ est : string, string_t , password, url, phone, email, richtext
164
+ utilise une chaine de caracteres lorsque le type de champ est : string , password, url, phone, email, richtext
165
+ utilise un objet { key: "trKey", value: "Translation"} lorsque le champ est string_t
124
166
  utilise un filtre en retour si le type de champ est code et language='json' et conditionBuilder=true
125
167
  utilise une chaine si c'est un type de champ code par défaut.
126
168
  utilise une structure { "iso2langcode":"content..." } pour le champ multi-traductions richtext_t
@@ -134,13 +176,11 @@ utilise un tableau d'_ids pour remplir les champs relation multiple=true
134
176
  utilise la valeur en héxadecimal, ex: '#FF0000' pour les champs de type : color
135
177
  utilise les valeurs de cron standard '* * * * * *' pour : cronSchedule
136
178
 
137
- PROCESSUS DE RAISONNEMENT:
138
- a- L'utilisateur pose une question, ou demande une action de ta part.
139
- b- Utilise l'outil **search_models** pour trouver le(s) modèle(s) qui correspondent à la question.
140
- Si tu as déjà fait la recherche dans la conversation, garde la définition initiale et n'effectue pas de recherche, va directement à l'étape c
141
- c- Une fois la réponse retournée et intégrée, tu devras utiliser les autres outils (search, post, etc.) dans la conversation pour satisfaire la question initiale, en utilisant les informations des modèles précédents (COMMANDE FINALE)
142
- Si tu n'as aucune commande à exécuter directement, réponds simplement à l'utilisateur avec "displayMessage".
143
-
179
+ PROCESSUS DE RAISONNEMENT STRICT:
180
+ a- L'utilisateur pose une question.
181
+ b- **Étape 1 (Obligatoire):** Appelle l'outil \`search_models\` pour obtenir la structure EXACTE du modèle de données. C'est ta seule source de vérité pour les noms de champs.
182
+ c- **Étape 2 (Obligatoire):** Analyse la réponse de \`search_models\` que le système t'a fournie.
183
+ d- **Étape 3 (Commande Finale):** Construis ta commande finale (\`search\`, \`generateHtmlView\`, etc.). **Règle absolue :** Pour les filtres et les templates, tu ne dois utiliser QUE les noms de champs (\`name\`) et les types (\`type\`) que tu as lus dans la réponse de \`search_models\` à l'étape c. N'invente RIEN.
144
184
  CONTEXTE ACTUEL:
145
185
  - Date du jour de la conversation : ${dt}
146
186
  - La langue ISO à utiliser dans la conversation : ${lang}
@@ -182,6 +222,67 @@ COMMANDE FINALE :
182
222
  }
183
223
  }
184
224
 
225
+ Question: Crée un tableau de bord des rôles et permissions avec un design sophistiqué.
226
+ Ta réponse: { "action" : "search_models", "params": { "query": "role" } }
227
+ COMMANDE FINALE :
228
+ {
229
+ "action": "generateHtmlView",
230
+ "params": {
231
+ "title": "Matrice des Rôles et Permissions",
232
+ "model": "role",
233
+ "template": "<div class='roles-container'>{{#each data}}<div class='role-card'><div class='role-header'><span class='role-icon'>🛡️</span><h3>{{this.name.value}}</h3></div><ul class='permissions-list'>{{#each this.permissions}}<li data-tooltip-html='{{this.description}}'><span class='permission-name'>{{this.name.value}}</span></li>{{/each}}</ul></div>{{/each}}</div>",
234
+ "css": "#{{containerId}} .roles-container{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:25px;padding:25px;font-family:'Poppins',sans-serif;background:#111827}#{{containerId}} .role-card{background:rgba(31,41,55,.5);border-radius:16px;padding:20px;position:relative;overflow:hidden;border:1px solid transparent;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);transition:transform .3s ease,box-shadow .3s ease}#{{containerId}} .role-card::before{content:'';position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1;margin:-1px;border-radius:inherit;background:conic-gradient(from 180deg at 50% 50%,#2a8af6 0deg,#a855f7 180deg,#f59e0b 360deg);animation:rotate-gradient 5s linear infinite}@keyframes rotate-gradient{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}#{{containerId}} .role-card:hover{transform:translateY(-8px);box-shadow:0 20px 30px rgba(0,0,0,.2)}#{{containerId}} .role-header{display:flex;align-items:center;gap:12px;margin-bottom:15px;padding-bottom:10px;border-bottom:1px solid rgba(255,255,255,.1)}#{{containerId}} .role-header .role-icon{font-size:1.8em}#{{containerId}} .role-header h3{margin:0;font-size:1.4em;font-weight:600;color:#f9fafb}#{{containerId}} .permissions-list{list-style:none;padding:0;margin:0;max-height:200px;overflow-y:auto}#{{containerId}} .permissions-list::-webkit-scrollbar{width:6px}#{{containerId}} .permissions-list::-webkit-scrollbar-track{background:rgba(255,255,255,.05);border-radius:3px}#{{containerId}} .permissions-list::-webkit-scrollbar-thumb{background:#4f46e5;border-radius:3px}#{{containerId}} .permissions-list::-webkit-scrollbar-thumb:hover{background:#6366f1}#{{containerId}} .permissions-list li{padding:8px 12px;margin-bottom:6px;background:rgba(255,255,255,.05);border-radius:8px;color:#d1d5db;cursor:help;transition:background-color .2s ease;font-size:.95em}#{{containerId}} .permissions-list li:hover{background:rgba(79,70,229,.5);color:#fff}",
235
+ "filter": {},
236
+ "limit": 10
237
+ }
238
+ }
239
+
240
+ Question: Affiche une carte de visite stylisée pour le premier contact.
241
+ Ta réponse: { "action" : "search_models", "params": { "query": "contact" } }
242
+ COMMANDE FINALE :
243
+ {
244
+ "action": "generateHtmlView",
245
+ "params": {
246
+ "title": "Carte de Visite",
247
+ "model": "contact",
248
+ "template": "<div class='card-container'><h3>{{this.firstName}} {{this.lastName}}</h3><p>{{this.email}}</p></div>",
249
+ "css": "#{{containerId}} .card-container { border: 1px solid #ccc; border-radius: 8px; padding: 16px; background-color: #f9f9f9; } #{{containerId}} h3 { margin-top: 0; color: #333; }",
250
+ "filter": {},
251
+ "limit": 1
252
+ }
253
+ }
254
+
255
+ Question: Montre-moi un catalogue des produits.
256
+ Ta réponse: { "action" : "search_models", "params": { "query": "product" } }
257
+ COMMANDE FINALE :
258
+ {
259
+ "action": "generateHtmlView",
260
+ "params": {
261
+ "title": "Catalogue Produits",
262
+ "model": "product",
263
+ "template": "<div class='product-grid'>{{#each data}}<div class='product-card'><div class='product-image-container'><img src='{{this.image.0.url}}' alt='{{this.name.value}}' class='product-image'><span class='product-brand'>{{this.brand.name}}</span></div><div class='product-info'><h3 class='product-name'>{{this.name.value}}</h3><p class='product-category'>{{this.category.name.value}}</p><div class='product-footer'><span class='product-price'>{{this.price}} {{this.currency.symbol}}</span><button class='add-to-cart-btn'>Ajouter au panier</button></div></div></div>{{/each}}</div>",
264
+ "css": "#{{containerId}} .product-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:2rem;padding:2rem;font-family:'Lato',sans-serif;background-color:#f8f9fa}#{{containerId}} .product-card{background-color:#fff;border-radius:12px;box-shadow:0 4px 6px rgba(0,0,0,.05),0 1px 3px rgba(0,0,0,.05);overflow:hidden;transition:transform .3s ease,box-shadow .3s ease;display:flex;flex-direction:column}#{{containerId}} .product-card:hover{transform:translateY(-5px);box-shadow:0 12px 20px rgba(0,0,0,.08),0 3px 8px rgba(0,0,0,.06)}#{{containerId}} .product-image-container{position:relative;width:100%;padding-top:100%}#{{containerId}} .product-image{position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover}#{{containerId}} .product-brand{position:absolute;top:10px;right:10px;background-color:rgba(0,0,0,.6);color:#fff;padding:4px 8px;border-radius:6px;font-size:.8em;font-weight:700}#{{containerId}} .product-info{padding:15px;display:flex;flex-direction:column;flex-grow:1}#{{containerId}} .product-name{font-size:1.1em;font-weight:700;color:#343a40;margin:0 0 5px 0;line-height:1.3}#{{containerId}} .product-category{font-size:.85em;color:#6c757d;margin:0 0 15px 0;flex-grow:1}#{{containerId}} .product-footer{display:flex;justify-content:space-between;align-items:center;border-top:1px solid #e9ecef;padding-top:10px;margin-top:auto}#{{containerId}} .product-price{font-size:1.2em;font-weight:700;color:#007bff}#{{containerId}} .add-to-cart-btn{background-color:#007bff;color:#fff;border:none;padding:8px 12px;border-radius:8px;cursor:pointer;font-weight:600;transition:background-color .2s ease}#{{containerId}} .add-to-cart-btn:hover{background-color:#0056b3}",
265
+ "filter": {},
266
+ "limit": 12
267
+ }
268
+ }
269
+
270
+ Question: Affiche-moi les dernières requêtes API avec un design futuriste.
271
+ Ta réponse: { "action" : "search_models", "params": { "query": "request" } }
272
+ COMMANDE FINALE :
273
+ {
274
+ "action": "generateHtmlView",
275
+ "params": {
276
+ "title": "Journal des Requêtes API",
277
+ "model": "request",
278
+ "template": "<div class=\"requests-grid\">{{#each data}}<div class=\"request-card status-{{this.status}}\"><div class=\"card-header\"><span class=\"method-badge method-{{this.method}}\">{{this.method}}</span><span class=\"status-code\" data-tooltip-html=\"Code de statut HTTP\">{{this.status}}</span></div><div class=\"card-body\"><p class=\"url\" data-tooltip-html=\"URL de la requête\">{{this.url}}</p></div><div class=\"card-footer\"><span class=\"latency\" data-tooltip-html=\"Latence de la réponse\">⏱️ {{this.latencyMs}} ms</span><span class=\"timestamp\" data-tooltip-html=\"Date et heure\">{{this.timestamp}}</span></div><div class=\"glow-effect\"></div></div>{{/each}}</div>",
279
+ "css": "#{{containerId}} .requests-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:20px;font-family:'Orbitron',sans-serif;padding:10px}#{{containerId}} .request-card{background:rgba(10,25,47,.8);border:1px solid #00aaff;border-radius:12px;padding:15px;position:relative;overflow:hidden;transition:transform .3s ease,box-shadow .3s ease;backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px)}#{{containerId}} .request-card:hover{transform:translateY(-5px);box-shadow:0 10px 20px rgba(0,170,255,.3)}#{{containerId}} .glow-effect{position:absolute;top:0;left:0;width:100%;height:100%;background:radial-gradient(circle at 50% 0,rgba(0,170,255,.2),transparent 70%);animation:pulse 4s infinite ease-in-out;pointer-events:none}@keyframes pulse{0%,100%{opacity:.5}50%{opacity:1}}#{{containerId}} .card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;border-bottom:1px solid rgba(0,170,255,.2);padding-bottom:8px}#{{containerId}} .method-badge{padding:4px 8px;border-radius:5px;font-weight:700;font-size:.9em;color:#fff;text-shadow:0 0 5px currentColor}#{{containerId}} .method-GET{background-color:#00aaff}#{{containerId}} .method-POST{background-color:#4caf50}#{{containerId}} .method-PUT{background-color:#ff9800}#{{containerId}} .method-DELETE{background-color:#f44336}#{{containerId}} .method-PATCH{background-color:#9c27b0}#{{containerId}} .status-code{font-size:1.2em;font-weight:700;color:#fff}#{{containerId}} .status-200 .status-code{color:#4caf50;text-shadow:0 0 8px #4caf50}#{{containerId}} .status-404 .status-code{color:#f44336;text-shadow:0 0 8px #f44336}#{{containerId}} .status-500 .status-code{color:#ff9800;text-shadow:0 0 8px #ff9800}#{{containerId}} .card-body .url{color:#e0e0e0;font-size:.95em;word-break:break-all;margin:0}#{{containerId}} .card-footer{display:flex;justify-content:space-between;align-items:center;margin-top:15px;font-size:.8em;color:#88a1b9}#{{containerId}} .latency,#{{containerId}} .timestamp{display:flex;align-items:center;gap:5px}",
280
+ "filter": {},
281
+ "sort": { "_id": -1 },
282
+ "limit": 12
283
+ }
284
+ }
285
+
185
286
  Absolute rules:
186
287
  ====
187
288
  NEVER use the non-aggregated syntax of the MongoDB operators :
@@ -274,11 +375,17 @@ async function executeTool(action, params, user, allModels) {
274
375
  return resultString;
275
376
  }
276
377
  case 'search_models': {
277
- const foundModels = await searchModels(params.query, user);
378
+ const { main: foundModels, related: relatedModels } = await searchModels(params.query, user);
278
379
 
279
380
  if (foundModels.length > 0) {
280
- return "J'ai trouvé les modèles suivants qui pourraient correspondre : " +
381
+ let responseText = "J'ai trouvé les modèles suivants qui pourraient correspondre : " +
281
382
  foundModels.map(m => `\n- Modèle "${m.name}": ${m.description || 'Pas de description.'}\n- Champs: ${m.fields.map(f => JSON.stringify(f, null, 2))}`).join('');
383
+
384
+ if (relatedModels.length > 0) {
385
+ responseText += "\n\nPour votre information, voici la structure des modèles liés que vous pouvez utiliser dans les templates :";
386
+ responseText += relatedModels.map(m => `\n- Modèle lié "${m.name}":\n- Champs: ${m.fields.map(f => JSON.stringify(f, null, 2))}`).join('');
387
+ }
388
+ return responseText;
282
389
  } else {
283
390
  return "Je n'ai trouvé aucun modèle correspondant à votre recherche.";
284
391
  }
@@ -356,7 +463,8 @@ async function handleChatRequest(message, history, provider, context, user, conf
356
463
 
357
464
 
358
465
  // --- BOUCLE DE RAISONNEMENT ET D'EXÉCUTION D'OUTILS ---
359
- for (let i = 0; i < maxAIReflectiveSteps; i++) {
466
+ const m = Config.Get('maxAIReflectiveSteps', maxAIReflectiveSteps);
467
+ for (let i = 0; i < m; i++) {
360
468
  logger.debug(`[Assistant] Tour de boucle ${i + 1}. Invocation de l'IA...`);
361
469
 
362
470
  const response = await llm.invoke(conversationHistory);
@@ -365,18 +473,25 @@ async function handleChatRequest(message, history, provider, context, user, conf
365
473
  // Parsing JSON robuste
366
474
  let parsedResponse;
367
475
  try {
476
+ // Tente d'extraire le JSON de la réponse, même s'il est entouré de texte.
477
+ const jsonRegex = /\{[\s\S]*\}/s; // 's' flag pour que '.' matche les nouvelles lignes
478
+ const match = llmOutput.match(jsonRegex);
368
479
 
369
- parsedResponse = JSON.parse(llmOutput);
480
+ if (match && match[0]) {
481
+ // Si un JSON est trouvé, on tente de le parser
482
+ parsedResponse = JSON.parse(match[0]);
483
+ } else {
484
+ // Aucun JSON trouvé, c'est probablement une réponse textuelle simple.
485
+ return { success: true, displayMessage: llmOutput };
486
+ }
370
487
 
371
488
  if (!parsedResponse.action || !parsedResponse.params) {
372
489
  throw new Error("Réponse JSON invalide: 'action' ou 'params' manquant.");
373
490
  }
374
491
  } catch (parseError) {
375
492
  logger.error(`[Assistant] Erreur de parsing de la réponse de l'IA: ${parseError.message}. Réponse brute: "${llmOutput}"`);
376
- return {
377
- success: true,
378
- displayMessage: llmOutput || i18n.t('assistant.invalidResponse', "Désolé, je n'ai pas pu formuler une réponse correcte. Veuillez réessayer.")
379
- };
493
+ // Si le parsing échoue, on renvoie le message brut de l'IA, qui est peut-être une réponse textuelle valide.
494
+ return { success: true, displayMessage: llmOutput };
380
495
  }
381
496
 
382
497
  logger.debug(`[Assistant] Action décidée par l'IA: ${parsedResponse.action}`, parsedResponse);
@@ -396,6 +511,23 @@ async function handleChatRequest(message, history, provider, context, user, conf
396
511
  return { success: true, chartConfig: params };
397
512
  }
398
513
 
514
+ // Action de génération de vue HTML
515
+ if (action === 'generateHtmlView') {
516
+ const viewData = await searchData({
517
+ model: params.model,
518
+ filter: params.filter,
519
+ limit: params.limit || 10,
520
+ depth: 2 // Pour avoir accès aux relations de premier niveau dans les templates
521
+ }, user);
522
+
523
+ if (viewData.data.length === 0) {
524
+ return { success: true, displayMessage: i18n.t('assistant.htmlView.noResult', "Je n'ai trouvé aucune donnée correspondante pour cette vue.") };
525
+ }
526
+
527
+ // On retourne la configuration de la vue ET les données au client.
528
+ return { success: true, htmlViewConfig: { ...params, data: viewData.data } };
529
+ }
530
+
399
531
  // NOUVEAU: Action de recherche à afficher, gérée par le front-end
400
532
  if (action === 'search') {
401
533
  const searchResult = await searchData({
@@ -6,13 +6,14 @@ import {decryptValue, encryptValue} from "../data.js";
6
6
  import {loadFromDump, validateRestoreRequest} from "./data/index.js";
7
7
  import {Logger} from "../gameObject.js";
8
8
  import {middlewareAuthenticator, userInitiator} from "./user.js";
9
- import {awsDefaultConfig, maxBytesPerSecondThrottleData} from "../constants.js";
9
+ import {awsDefaultConfig, getHost, maxBytesPerSecondThrottleData} from "../constants.js";
10
10
  import crypto from "node:crypto";
11
11
  import i18n from "../../src/i18n.js";
12
12
  import {sendEmail} from "../email.js";
13
13
  import {throttleMiddleware} from "../middlewares/throttle.js";
14
14
  import {getCollectionForUser} from "./mongodb.js";
15
15
  import {safeAssignObject} from "../core.js";
16
+ import {Config} from "../config.js";
16
17
 
17
18
  const restoreRequests = {};
18
19
 
@@ -37,6 +38,7 @@ export const requestRestore = async (user, lang) => {
37
38
  content: i18n.t('email.backup.restoreRequest.content', {
38
39
  user: user?.username,
39
40
  fullToken: fullRestoreToken,
41
+ host: getHost(),
40
42
  modelsToken: modelsRestoreToken
41
43
  })
42
44
  });
@@ -47,15 +49,6 @@ export const requestRestore = async (user, lang) => {
47
49
  }
48
50
  };
49
51
 
50
- const getDefaultS3Config = () => {
51
- return {
52
- accessKeyId: process.env.AWS_ACCESS_KEY_ID,
53
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, // Utiliser la clé déchiffrée
54
- region: process.env.AWS_REGION || awsDefaultConfig.region,
55
- bucketName: process.env.AWS_BUCKET || awsDefaultConfig.bucketName
56
- };
57
- }
58
-
59
52
  /**
60
53
  * Récupère la configuration S3 depuis les variables d'environnement de l'utilisateur en base de données.
61
54
  * @param {object} user - L'objet utilisateur.
@@ -98,12 +91,16 @@ async function _getUserS3ConfigFromDb(user) {
98
91
  * @returns {Promise<object>} - L'objet de configuration S3 final.
99
92
  */
100
93
  export async function getUserS3Config(user) {
94
+
95
+ const adc = awsDefaultConfig;
96
+ const dc = Config.Get('awsDefaultConfig', {});
97
+
101
98
  // 1. Récupérer la configuration globale par défaut
102
99
  const defaultConfig = {
103
- accessKeyId: process.env.AWS_ACCESS_KEY_ID,
104
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
105
- region: process.env.AWS_REGION || awsDefaultConfig.region,
106
- bucketName: process.env.AWS_BUCKET_NAME || awsDefaultConfig.bucketName
100
+ accessKeyId: dc.accessKeyId || process.env.AWS_ACCESS_KEY_ID,
101
+ secretAccessKey: dc.secretAccessKey || process.env.AWS_SECRET_ACCESS_KEY,
102
+ region: dc.region || process.env.AWS_REGION || adc.region,
103
+ bucketName: dc.bucketName || process.env.AWS_BUCKET_NAME || adc.bucketName
107
104
  };
108
105
 
109
106
  // 2. Récupérer la configuration spécifique de l'utilisateur
@@ -243,13 +240,14 @@ export const getS3Stream = (s3Config, s3Key) => {
243
240
  return s3.getObject(params).createReadStream();
244
241
  };
245
242
 
246
- const throttle = throttleMiddleware(maxBytesPerSecondThrottleData);
247
-
248
243
  let engine, logger;
249
244
  export async function onInit(defaultEngine) {
250
245
  engine = defaultEngine;
251
246
  logger = engine.getComponent(Logger);
252
247
 
248
+ const m = Config.Get('maxBytesPerSecondThrottleData', maxBytesPerSecondThrottleData);
249
+ const throttle = throttleMiddleware(m);
250
+
253
251
  const userMiddlewares = await engine.userProvider.getMiddlewares();
254
252
  engine.post('/api/backup/request-restore', [throttle, middlewareAuthenticator, userInitiator, ...userMiddlewares], async (req, res) => {
255
253
  const user = req.me; // Assuming you have user authentication middleware
@@ -153,7 +153,8 @@ export const loadFromDump = async (user, options = {}) => {
153
153
  }
154
154
 
155
155
  // --- EXÉCUTION DE MONGORESTORE ---
156
- const restoreSourceDir = path.join(tmpRestoreDir, dbName);
156
+ const d = Config.Get('dbName', dbName);
157
+ const restoreSourceDir = path.join(tmpRestoreDir, d);
157
158
  if (!fs.existsSync(restoreSourceDir)) {
158
159
  throw new Error(`Restore source directory (${restoreSourceDir}) not found.`);
159
160
  }
@@ -161,15 +162,15 @@ export const loadFromDump = async (user, options = {}) => {
161
162
  let command;
162
163
  const args = [
163
164
  '--uri', dbUrl,
164
- '--db', dbName
165
+ '--db', d
165
166
  ];
166
167
 
167
168
  if (modelsOnly) {
168
- args.push('--nsInclude', `${dbName}.models`);
169
+ args.push('--nsInclude', `${d}.models`);
169
170
  } else {
170
171
  // mongorestore accepte plusieurs fois l'option --nsInclude
171
- args.push('--nsInclude', `${dbName}.datas`);
172
- args.push('--nsInclude', `${dbName}.models`);
172
+ args.push('--nsInclude', `${d}.datas`);
173
+ args.push('--nsInclude', `${d}.models`);
173
174
  }
174
175
  // Le répertoire source est le dernier argument
175
176
  args.push(restoreSourceDir);
@@ -242,6 +243,8 @@ export const dumpUserData = async (user) => {
242
243
  const backupFrequency = await engine.userProvider.getBackupFrequency(user);
243
244
  logger.info(`Fréquence de sauvegarde : ${backupFrequency}.`);
244
245
 
246
+
247
+ const d = Config.Get('dbName', dbName);
245
248
  const collections = await MongoDatabase.listCollections().toArray();
246
249
  for (const collection of collections) {
247
250
  const collsToBackup = [await getUserCollectionName(user), 'models'];
@@ -249,7 +252,7 @@ export const dumpUserData = async (user) => {
249
252
  const query = {_user: user.username};
250
253
  const args = [
251
254
  '--uri', dbUrl,
252
- '--db', dbName,
255
+ '--db', d,
253
256
  '--out', localTempDumpDir,
254
257
  '--collection', collection.name,
255
258
  '--query', JSON.stringify(query)
@@ -258,9 +261,9 @@ export const dumpUserData = async (user) => {
258
261
  await execFileAsync('mongodump', args);
259
262
  }
260
263
  }
261
- const dumpSourceDir = path.join(localTempDumpDir, dbName);
264
+ const dumpSourceDir = path.join(localTempDumpDir, d);
262
265
  if (fs.existsSync(dumpSourceDir)) {
263
- await tar.create({gzip: true, file: localArchivePath, C: localTempDumpDir}, [dbName]);
266
+ await tar.create({gzip: true, file: localArchivePath, C: localTempDumpDir}, [d]);
264
267
  logger.info(`Archive de sauvegarde locale créée : ${localArchivePath}`);
265
268
  } else {
266
269
  logger.warn(`Le répertoire de dump ${dumpSourceDir} était vide. Aucune archive n'a créée.`);
@@ -1,93 +1,119 @@
1
- import NodeCache from "node-cache";
2
- import path from "node:path";
3
- import {Worker} from 'worker_threads';
4
- import {fileURLToPath} from "node:url";
5
- export const modelsCache = new NodeCache( { stdTTL: 100, checkperiod: 120 } );
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
-
10
- export const mongoDBWhitelist = [
11
- "$$NOW", "$in", "$eq", "$gt", "$gte", "$in", "$lt", "$lte", "$ne", "$nin", "$type", "$size",
12
- "$and", "$not", "$nor", "$or", "$regexMatch", "$find", "$elemMatch", "$filter", "$toString", "$toObjectId",
13
- "$concat",
14
- '$add', '$subtract', '$multiply', '$divide', '$mod', '$pow', "$sqrt",
15
- "$rand",
16
- "$abs", '$sin', '$cos', '$tan', '$asin', '$acos', '$atan',
17
- "$toDate", "$toBool", "$toString", "$toInt", "$toDouble",
18
- "$dateDiff", "$dateSubtract", "$dateAdd", "$dateToString",
19
- '$year', '$month', '$week', '$dayOfMonth', '$dayOfWeek', '$dayOfYear', '$hour', '$minute', '$second', '$millisecond',
20
- '$geoNear', '$regex', '$text', '$nearSphere','$geoWithin','$geoIntersects'
21
- ];
22
- export let importJobs = {};
23
-
24
-
25
- /**
26
- * Exécute une tâche d'import/export (parsing, stringify) dans un worker thread.
27
- * @param {('parse-json'|'parse-csv'|'stringify-json')} action - L'action à effectuer.
28
- * @param {object} payload - Les données nécessaires pour l'action.
29
- * @returns {Promise<any>} - Une promesse qui se résout avec les données traitées.
30
- */
31
- export function runImportExportWorker(action, payload) {
32
- return new Promise((resolve, reject) => {
33
- const workerPath = path.resolve(process.cwd(), './src/workers/import-export-worker.js');
34
- const worker = new Worker(workerPath);
35
-
36
- worker.postMessage({ action, payload });
37
-
38
- worker.on('message', (result) => {
39
- if (result.success) {
40
- resolve(result.data);
41
- } else {
42
- // Correction : On s'assure de toujours passer une chaîne de caractères à new Error()
43
- const errorMessage = result.error || `Import/Export Worker failed with an unknown error. Action: ${action}.`;
44
- reject(new Error(errorMessage));
45
- }
46
- worker.terminate();
47
- });
48
-
49
- worker.on('error', (err) => {
50
- reject(err);
51
- worker.terminate();
52
- });
53
-
54
- worker.on('exit', (code) => {
55
- if (code !== 0) {
56
- reject(new Error(`Import/Export Worker stopped with exit code ${code}`));
57
- }
58
- });
59
- });
60
- }
61
- /** Exécute une tâche de cryptographie dans un worker thread.
62
- * @param {('encrypt'|'decrypt'|'hash')} action - L'action à effectuer.
63
- * @param {object} payload - Les données nécessaires pour l'action.
64
- * @returns {Promise<any>} - Une promesse qui se résout avec le résultat (si pertinent).
65
- */
66
- export function runCryptoWorkerTask(action, payload) {
67
- return new Promise((resolve, reject) => {
68
- const workerPath = path.resolve(__dirname, '../../workers/crypto-worker.js');
69
- const worker = new Worker(workerPath);
70
-
71
- worker.postMessage({ action, payload });
72
-
73
- worker.on('message', (result) => {
74
- if (result.success) {
75
- resolve(result.data); // Résout avec les données (ex: le hash) ou undefined si pas de retour
76
- } else {
77
- reject(new Error(result.error));
78
- }
79
- worker.terminate();
80
- });
81
-
82
- worker.on('error', (err) => {
83
- reject(err);
84
- worker.terminate();
85
- });
86
-
87
- worker.on('exit', (code) => {
88
- if (code !== 0) {
89
- reject(new Error(`Crypto Worker stopped with exit code ${code}`));
90
- }
91
- });
92
- });
1
+ import NodeCache from "node-cache";
2
+ import path from "node:path";
3
+ import {Worker} from 'worker_threads';
4
+ import {fileURLToPath} from "node:url";
5
+ export const modelsCache = new NodeCache( { stdTTL: 100, checkperiod: 120 } );
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ /**
11
+ * Helper to escape characters for regex.
12
+ * @param {string} str The string to escape.
13
+ * @returns {string} The escaped string.
14
+ */
15
+ function escapeRegex(str) {
16
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
17
+ }
18
+
19
+ /**
20
+ * Generates a regular expression from a mask pattern and replacement rules.
21
+ * @param {string} mask - The mask pattern, e.g., '+0 (___) ___-__-__'.
22
+ * @param {object} replacement - An object mapping placeholder characters to regex patterns, e.g., { "_": "\\d" }.
23
+ * @returns {string|null} - A regular expression string or null if inputs are invalid.
24
+ */
25
+ export function generateRegexFromMask(mask, replacement) {
26
+ if (!mask || !replacement || typeof replacement !== 'object') return null;
27
+
28
+ let regex = '^';
29
+ for (const char of mask) {
30
+ regex += replacement[char] ? `(${replacement[char]})` : escapeRegex(char);
31
+ }
32
+ regex += '$';
33
+ return regex;
34
+ }
35
+
36
+ export const mongoDBWhitelist = [
37
+ "$$NOW", "$in", "$eq", "$gt", "$gte", "$in", "$lt", "$lte", "$ne", "$nin", "$type", "$size",
38
+ "$and", "$not", "$nor", "$or", "$regexMatch", "$find", "$elemMatch", "$filter", "$toString", "$toObjectId",
39
+ "$concat",
40
+ '$add', '$subtract', '$multiply', '$divide', '$mod', '$pow', "$sqrt",
41
+ "$rand",
42
+ "$abs", '$sin', '$cos', '$tan', '$asin', '$acos', '$atan',
43
+ "$toDate", "$toBool", "$toString", "$toInt", "$toDouble",
44
+ "$dateDiff", "$dateSubtract", "$dateAdd", "$dateToString",
45
+ '$year', '$month', '$week', '$dayOfMonth', '$dayOfWeek', '$dayOfYear', '$hour', '$minute', '$second', '$millisecond',
46
+ '$geoNear', '$regex', '$text', '$nearSphere','$geoWithin','$geoIntersects'
47
+ ];
48
+ export let importJobs = {};
49
+
50
+
51
+ /**
52
+ * Exécute une tâche d'import/export (parsing, stringify) dans un worker thread.
53
+ * @param {('parse-json'|'parse-csv'|'stringify-json')} action - L'action à effectuer.
54
+ * @param {object} payload - Les données nécessaires pour l'action.
55
+ * @returns {Promise<any>} - Une promesse qui se résout avec les données traitées.
56
+ */
57
+ export function runImportExportWorker(action, payload) {
58
+ return new Promise((resolve, reject) => {
59
+ const workerPath = path.resolve(process.cwd(), './src/workers/import-export-worker.js');
60
+ const worker = new Worker(workerPath);
61
+
62
+ worker.postMessage({ action, payload });
63
+
64
+ worker.on('message', (result) => {
65
+ if (result.success) {
66
+ resolve(result.data);
67
+ } else {
68
+ // Correction : On s'assure de toujours passer une chaîne de caractères à new Error()
69
+ const errorMessage = result.error || `Import/Export Worker failed with an unknown error. Action: ${action}.`;
70
+ reject(new Error(errorMessage));
71
+ }
72
+ worker.terminate();
73
+ });
74
+
75
+ worker.on('error', (err) => {
76
+ reject(err);
77
+ worker.terminate();
78
+ });
79
+
80
+ worker.on('exit', (code) => {
81
+ if (code !== 0) {
82
+ reject(new Error(`Import/Export Worker stopped with exit code ${code}`));
83
+ }
84
+ });
85
+ });
86
+ }
87
+ /** Exécute une tâche de cryptographie dans un worker thread.
88
+ * @param {('encrypt'|'decrypt'|'hash')} action - L'action à effectuer.
89
+ * @param {object} payload - Les données nécessaires pour l'action.
90
+ * @returns {Promise<any>} - Une promesse qui se résout avec le résultat (si pertinent).
91
+ */
92
+ export function runCryptoWorkerTask(action, payload) {
93
+ return new Promise((resolve, reject) => {
94
+ const workerPath = path.resolve(__dirname, '../../workers/crypto-worker.js');
95
+ const worker = new Worker(workerPath);
96
+
97
+ worker.postMessage({ action, payload });
98
+
99
+ worker.on('message', (result) => {
100
+ if (result.success) {
101
+ resolve(result.data); // Résout avec les données (ex: le hash) ou undefined si pas de retour
102
+ } else {
103
+ reject(new Error(result.error));
104
+ }
105
+ worker.terminate();
106
+ });
107
+
108
+ worker.on('error', (err) => {
109
+ reject(err);
110
+ worker.terminate();
111
+ });
112
+
113
+ worker.on('exit', (code) => {
114
+ if (code !== 0) {
115
+ reject(new Error(`Crypto Worker stopped with exit code ${code}`));
116
+ }
117
+ });
118
+ });
93
119
  }