data-primals-engine 1.5.0 → 1.5.2

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 (63) hide show
  1. package/README.md +37 -0
  2. package/client/src/AddWidgetTypeModal.jsx +47 -43
  3. package/client/src/App.jsx +2 -6
  4. package/client/src/App.scss +13 -1
  5. package/client/src/AssistantChat.jsx +363 -323
  6. package/client/src/AssistantChat.scss +27 -10
  7. package/client/src/Dashboard.jsx +480 -396
  8. package/client/src/Dashboard.scss +1 -1
  9. package/client/src/DashboardHtmlViewItem.jsx +147 -0
  10. package/client/src/DashboardView.jsx +654 -569
  11. package/client/src/DataEditor.jsx +10 -3
  12. package/client/src/DataLayout.jsx +807 -755
  13. package/client/src/DataLayout.scss +14 -0
  14. package/client/src/DataTable.jsx +39 -75
  15. package/client/src/Dialog.scss +1 -1
  16. package/client/src/Field.jsx +2057 -1825
  17. package/client/src/FlexViewCard.jsx +44 -0
  18. package/client/src/HistoryDialog.jsx +69 -14
  19. package/client/src/HtmlViewBuilderModal.jsx +91 -0
  20. package/client/src/HtmlViewBuilderModal.scss +18 -0
  21. package/client/src/HtmlViewCard.jsx +44 -0
  22. package/client/src/HtmlViewCard.scss +35 -0
  23. package/client/src/KanbanCard.jsx +1 -2
  24. package/client/src/ModelCreator.jsx +5 -4
  25. package/client/src/ModelCreatorField.jsx +51 -4
  26. package/client/src/ModelList.jsx +280 -236
  27. package/client/src/Notification.jsx +136 -136
  28. package/client/src/Notification.scss +0 -18
  29. package/client/src/Pagination.jsx +5 -3
  30. package/client/src/RelationField.jsx +354 -258
  31. package/client/src/RelationSelectorWidget.jsx +173 -0
  32. package/client/src/contexts/ModelContext.jsx +10 -1
  33. package/client/src/contexts/UIContext.jsx +72 -63
  34. package/client/src/filter.js +263 -212
  35. package/client/src/hooks/useValidation.js +75 -0
  36. package/client/src/translations.js +24 -24
  37. package/package.json +7 -6
  38. package/src/constants.js +1 -1
  39. package/src/core.js +8 -1
  40. package/src/defaultModels.js +1596 -1544
  41. package/src/engine.js +85 -43
  42. package/src/events.js +137 -113
  43. package/src/i18n.js +710 -10
  44. package/src/index.js +3 -0
  45. package/src/modules/assistant/assistant.js +253 -134
  46. package/src/modules/assistant/constants.js +2 -1
  47. package/src/modules/bucket.js +2 -1
  48. package/src/modules/data/data.core.js +118 -92
  49. package/src/modules/data/data.history.js +555 -492
  50. package/src/modules/data/data.js +3 -53
  51. package/src/modules/data/data.operations.js +3381 -3231
  52. package/src/modules/data/data.relations.js +686 -686
  53. package/src/modules/data/data.routes.js +1879 -1821
  54. package/src/modules/data/data.validation.js +81 -2
  55. package/src/modules/file.js +247 -238
  56. package/src/modules/user.js +1 -0
  57. package/src/modules/workflow.js +2 -2
  58. package/src/openai.jobs.js +3 -2
  59. package/src/packs.js +5482 -5478
  60. package/src/sso.js +2 -2
  61. package/src/workers/import-export-worker.js +1 -1
  62. package/test/data.history.integration.test.js +264 -192
  63. package/test/data.integration.test.js +149 -3
package/src/index.js CHANGED
@@ -5,6 +5,9 @@ export { Engine } from './engine.js';
5
5
  export { GameObject, Logger, BenchmarkTool } from './gameObject.js';
6
6
  export { Config } from './config.js';
7
7
  export { event_on, event_trigger, event_off } from './core.js';
8
+ export { Event } from './events.js';
9
+
10
+ export { sendSseToUser } from './modules/data/data.routes.js';
8
11
 
9
12
  export { UserProvider } from './providers.js';
10
13
 
@@ -1,44 +1,47 @@
1
1
  import { getCollectionForUser, modelsCollection } from "../mongodb.js";
2
2
  import { Logger } from "../../gameObject.js";
3
- import { ChatOpenAI } from "@langchain/openai";
4
- import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
5
3
  import { HumanMessage, SystemMessage } from "@langchain/core/messages";
6
4
  import {searchData, patchData, deleteData, insertData} from "../data/index.js";
7
- import { getDataAsString } from "../../data.js";
8
- import i18n from "../../i18n.js";
9
- import {generateLimiter} from "../user.js";
10
- import rateLimit from "express-rate-limit";
11
- import {maxAIReflectiveSteps} from "../../constants.js";
12
5
  import {providers} from "./constants.js";
13
- import {ChatDeepSeek} from "@langchain/deepseek";
14
- import {ChatAnthropic} from "@langchain/anthropic";
15
6
  import {Config} from "../../config.js";
7
+ import { Event } from "../../events.js";
8
+ import rateLimit from "express-rate-limit";
9
+ import {generateLimiter} from "../user.js";
10
+ import {maxAIReflectiveSteps} from "../../constants.js";
11
+ import i18n from "../../i18n.js";
12
+ import {parseSafeJSON} from "../../core.js";
16
13
 
17
14
  let logger = null;
18
15
 
19
- export const getAIProvider= (aiProvider, aiModel, apiKey)=>{
20
- let llm;
16
+ export const getAIProvider= async (aiProvider, aiModel, apiKey)=>{
21
17
  try {
22
18
  switch (aiProvider) {
23
- case 'OpenAI':
24
- llm = new ChatOpenAI({apiKey, model: aiModel, temperature: 0.7});
25
- break;
26
- case 'Google':
27
- llm = new ChatGoogleGenerativeAI({apiKey, model: aiModel, temperature: 0.7});
28
- break;
29
- case 'DeepSeek':
30
- llm = new ChatDeepSeek({apiKey, model: aiModel, temperature: 0.7});
31
- break;
32
- case 'Anthropic':
33
- llm = new ChatAnthropic({apiKey, model: aiModel, temperature: 0.7});
34
- break;
19
+ case 'OpenAI': {
20
+ const { ChatOpenAI } = await import("@langchain/openai");
21
+ return new ChatOpenAI({apiKey, model: aiModel, temperature: 0.7});
22
+ }
23
+ case 'Google': {
24
+ const { ChatGoogleGenerativeAI } = await import("@langchain/google-genai");
25
+ return new ChatGoogleGenerativeAI({apiKey, model: aiModel, temperature: 0.7});
26
+ }
27
+ case 'DeepSeek': {
28
+ const { ChatDeepSeek } = await import("@langchain/deepseek");
29
+ return new ChatDeepSeek({apiKey, model: aiModel, temperature: 0.7});
30
+ }
31
+ case 'Anthropic': {
32
+ const { ChatAnthropic } = await import("@langchain/anthropic");
33
+ return new ChatAnthropic({apiKey, model: aiModel, temperature: 0.7});
34
+ }
35
35
  default:
36
36
  throw new Error(`Unsupported AI provider: ${aiProvider}`);
37
37
  }
38
- return llm;
39
- }
40
- catch (e) {
41
- return null;
38
+ } catch (e) {
39
+ if (e.code === 'ERR_MODULE_NOT_FOUND') {
40
+ logger.error(`[Assistant] The package for the '${aiProvider}' provider is not installed. Please run 'npm install @langchain/${aiProvider.toLowerCase()}' to use this provider.`);
41
+ throw new Error(`The AI provider '${aiProvider}' is not installed. Please ask the administrator to install the corresponding package.`);
42
+ }
43
+ logger.error(`[Assistant] Error initializing AI provider '${aiProvider}': ${e.message}`);
44
+ throw e; // Re-throw other errors
42
45
  }
43
46
  }
44
47
  export const assistantGlobalLimiter = rateLimit({
@@ -53,14 +56,40 @@ export const assistantGlobalLimiter = rateLimit({
53
56
  });
54
57
 
55
58
  async function searchModels(query, user) {
56
- if (!query) return [];
59
+ if (!query) return { main: [], related: [] };
57
60
  const searchRegex = new RegExp(query, 'i');
58
- return await modelsCollection.find({
61
+ const mainModels = await modelsCollection.find({
59
62
  $or: [{ _user: user.username }, { _user: { $exists: false } }],
60
63
  $and: [{ $or: [{ name: { $regex: searchRegex } }, { description: { $regex: searchRegex } }] }]
61
64
  }, {
62
65
  projection: { name: 1, description: 1, fields: 1, _id: 0 }
63
66
  }).limit(10).toArray();
67
+
68
+ if (mainModels.length === 0) {
69
+ return { main: [], related: [] };
70
+ }
71
+
72
+ const relatedModelNames = new Set();
73
+ mainModels.forEach(model => {
74
+ model.fields.forEach(field => {
75
+ if (field.type === 'relation' && field.relation) {
76
+ relatedModelNames.add(field.relation);
77
+ }
78
+ });
79
+ });
80
+
81
+ const mainModelNames = new Set(mainModels.map(m => m.name));
82
+ const finalRelatedModelNames = [...relatedModelNames].filter(name => !mainModelNames.has(name));
83
+
84
+ let relatedModels = [];
85
+ if (finalRelatedModelNames.length > 0) {
86
+ relatedModels = await modelsCollection.find({
87
+ name: { $in: finalRelatedModelNames },
88
+ $or: [{ _user: user.username }, { _user: { $exists: false } }]
89
+ }, { projection: { name: 1, description: 1, fields: 1, _id: 0 } }).toArray();
90
+ }
91
+
92
+ return { main: mainModels, related: relatedModels };
64
93
  }
65
94
 
66
95
  const createSystemPrompt = (modelDefs, lang) => {
@@ -87,7 +116,7 @@ Tu as accès aux outils et actions suivants.
87
116
 
88
117
  OUTILS DE RAISONNEMENT INTERNE: (Utilisés pour collecter de l'information avant de décider de l'action finale)
89
118
  1. **search_models**: Pour rechercher les modèles de données disponibles.
90
- - Utilisation: { "action": "search_models", "params": { "query": "^regexToSearchFor$" } }
119
+ - Utilisation: { "action": "search_models", "params": { "query": "^regexToSearchFor$" } }. Il retourne aussi la structure des modèles directement liés.
91
120
  ====
92
121
 
93
122
  ACTIONS FINALES: (Actions qui terminent ta réflexion et renvoient un résultat à l'utilisateur)
@@ -119,9 +148,24 @@ ACTIONS FINALES: (Actions qui terminent ta réflexion et renvoient un résultat
119
148
  - \`yAxis\` (string): (Optionnel, sauf pour sum/avg/min/max) Le champ numérique à agréger du modèle.
120
149
  - \`filter\` (object): (Optionnel, filtre de la recherche, même écriture stricte que pour les filtres de recherche (voir plus bas pour les exemples)
121
150
 
151
+ 7. **generateHtmlView**: Pour créer une vue personnalisée en utilisant un template HTML.
152
+ - Utilisation: { "action": "generateHtmlView", "params": { ...config } }
153
+ - Le paramètre \`config\` doit contenir :
154
+ - \`title\` (string): Un titre pour la vue.
155
+ - \`model\` (string): Le nom du modèle de données à utiliser.
156
+ - \`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.**
157
+ Pour une liste, tu DOIS utiliser une boucle \`{{#each data}}...{{/each}}\`.
158
+ À l'intérieur d'une boucle, accède aux champs avec \`{{this.fieldName}}\`.
159
+ Pour les champs de type \`string_t\` ou \`richtext_t\`, accède à la traduction avec \`{{this.fieldName.value}}\`.
160
+ 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').
161
+ - \`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.**
162
+ - \`filter\` (object): (Optionnel) Un filtre pour sélectionner les documents à afficher.
163
+ - \`limit\` (number): (Optionnel, défaut 10) Le nombre maximum de documents à récupérer.
164
+
122
165
  Voici le mémo pour assigner des valeurs aux champs des modèles,avec ces types de données :
123
166
  utilise une chaine de caractère convertible en ObjectId (mongodb) lorsque le nom du champ est _id
124
- utilise une chaine de caracteres lorsque le type de champ est : string, string_t , password, url, phone, email, richtext
167
+ utilise une chaine de caracteres lorsque le type de champ est : string , password, url, phone, email, richtext
168
+ utilise un objet { key: "trKey", value: "Translation"} lorsque le champ est string_t
125
169
  utilise un filtre en retour si le type de champ est code et language='json' et conditionBuilder=true
126
170
  utilise une chaine si c'est un type de champ code par défaut.
127
171
  utilise une structure { "iso2langcode":"content..." } pour le champ multi-traductions richtext_t
@@ -135,13 +179,11 @@ utilise un tableau d'_ids pour remplir les champs relation multiple=true
135
179
  utilise la valeur en héxadecimal, ex: '#FF0000' pour les champs de type : color
136
180
  utilise les valeurs de cron standard '* * * * * *' pour : cronSchedule
137
181
 
138
- PROCESSUS DE RAISONNEMENT:
139
- a- L'utilisateur pose une question, ou demande une action de ta part.
140
- b- Utilise l'outil **search_models** pour trouver le(s) modèle(s) qui correspondent à la question.
141
- 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
142
- 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)
143
- Si tu n'as aucune commande à exécuter directement, réponds simplement à l'utilisateur avec "displayMessage".
144
-
182
+ PROCESSUS DE RAISONNEMENT STRICT:
183
+ a- L'utilisateur pose une question.
184
+ 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.
185
+ c- **Étape 2 (Obligatoire):** Analyse la réponse de \`search_models\` que le système t'a fournie.
186
+ 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.
145
187
  CONTEXTE ACTUEL:
146
188
  - Date du jour de la conversation : ${dt}
147
189
  - La langue ISO à utiliser dans la conversation : ${lang}
@@ -183,6 +225,67 @@ COMMANDE FINALE :
183
225
  }
184
226
  }
185
227
 
228
+ Question: Crée un tableau de bord des rôles et permissions avec un design sophistiqué.
229
+ Ta réponse: { "action" : "search_models", "params": { "query": "role" } }
230
+ COMMANDE FINALE :
231
+ {
232
+ "action": "generateHtmlView",
233
+ "params": {
234
+ "title": "Matrice des Rôles et Permissions",
235
+ "model": "role",
236
+ "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>",
237
+ "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}",
238
+ "filter": {},
239
+ "limit": 10
240
+ }
241
+ }
242
+
243
+ Question: Affiche une carte de visite stylisée pour le premier contact.
244
+ Ta réponse: { "action" : "search_models", "params": { "query": "contact" } }
245
+ COMMANDE FINALE :
246
+ {
247
+ "action": "generateHtmlView",
248
+ "params": {
249
+ "title": "Carte de Visite",
250
+ "model": "contact",
251
+ "template": "<div class='card-container'><h3>{{this.firstName}} {{this.lastName}}</h3><p>{{this.email}}</p></div>",
252
+ "css": "#{{containerId}} .card-container { border: 1px solid #ccc; border-radius: 8px; padding: 16px; background-color: #f9f9f9; } #{{containerId}} h3 { margin-top: 0; color: #333; }",
253
+ "filter": {},
254
+ "limit": 1
255
+ }
256
+ }
257
+
258
+ Question: Montre-moi un catalogue des produits.
259
+ Ta réponse: { "action" : "search_models", "params": { "query": "product" } }
260
+ COMMANDE FINALE :
261
+ {
262
+ "action": "generateHtmlView",
263
+ "params": {
264
+ "title": "Catalogue Produits",
265
+ "model": "product",
266
+ "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>",
267
+ "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}",
268
+ "filter": {},
269
+ "limit": 12
270
+ }
271
+ }
272
+
273
+ Question: Affiche-moi les dernières requêtes API avec un design futuriste.
274
+ Ta réponse: { "action" : "search_models", "params": { "query": "request" } }
275
+ COMMANDE FINALE :
276
+ {
277
+ "action": "generateHtmlView",
278
+ "params": {
279
+ "title": "Journal des Requêtes API",
280
+ "model": "request",
281
+ "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>",
282
+ "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}",
283
+ "filter": {},
284
+ "sort": { "_id": -1 },
285
+ "limit": 12
286
+ }
287
+ }
288
+
186
289
  Absolute rules:
187
290
  ====
188
291
  NEVER use the non-aggregated syntax of the MongoDB operators :
@@ -217,14 +320,10 @@ COMMANDE FINALE :
217
320
  * @returns {object} Le filtre corrigé.
218
321
  */
219
322
  function correctAIFilter(filter) {
220
- if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
221
- return filter; // Pas un objet à corriger
222
- }
323
+ if (!filter || typeof filter !== 'object' || Array.isArray(filter)) return filter;
223
324
 
224
325
  const keys = Object.keys(filter);
225
- if (keys.length <= 1) {
226
- return filter; // Rien à corriger
227
- }
326
+ if (keys.length <= 1) return filter;
228
327
 
229
328
  // Vérifie si toutes les clés sont des opérateurs (commencent par '$')
230
329
  const allKeysAreOperators = keys.every(key => key.startsWith('$'));
@@ -235,7 +334,7 @@ function correctAIFilter(filter) {
235
334
  return { '$and': andConditions };
236
335
  }
237
336
 
238
- return filter; // Aucune correction nécessaire
337
+ return filter;
239
338
  }
240
339
 
241
340
  /**
@@ -250,36 +349,18 @@ async function executeTool(action, params, user, allModels) {
250
349
  logger.debug(`[Assistant] Exécution de l'outil: ${action} avec les paramètres:`, params);
251
350
  try {
252
351
  switch (action) {
253
- case 'search': {
254
- const modelDef = allModels.find(m => m.name === params.model);
255
- if (!modelDef) {
256
- return `Erreur: Le modèle '${params.model}' n'a pas été trouvé. Impossible de lancer la recherche.`;
257
- }
258
-
259
- const searchResult = await searchData({
260
- model: params.model,
261
- filter: params.filter,
262
- limit: params.limit || 10,
263
- sort: params.sort
264
- }, user);
265
-
266
- if (searchResult.data.length === 0) {
267
- return i18n.t('assistant.noResults', "Aucun résultat trouvé.");
268
- }
269
-
270
- const resultString = i18n.t('assistant.searchResults', "Voici les résultats :") +
271
- searchResult.data.map(item =>
272
- `\n- ${getDataAsString(modelDef, item, { i18n, t: i18n.t }, allModels, true)}`
273
- ).join('');
274
-
275
- return resultString;
276
- }
277
352
  case 'search_models': {
278
- const foundModels = await searchModels(params.query, user);
353
+ const { main: foundModels, related: relatedModels } = await searchModels(params.query, user);
279
354
 
280
355
  if (foundModels.length > 0) {
281
- return "J'ai trouvé les modèles suivants qui pourraient correspondre : " +
356
+ let responseText = "J'ai trouvé les modèles suivants qui pourraient correspondre : " +
282
357
  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('');
358
+
359
+ if (relatedModels.length > 0) {
360
+ responseText += "\n\nPour votre information, voici la structure des modèles liés que vous pouvez utiliser dans les templates :";
361
+ responseText += relatedModels.map(m => `\n- Modèle lié "${m.name}":\n- Champs: ${m.fields.map(f => JSON.stringify(f, null, 2))}`).join('');
362
+ }
363
+ return responseText;
283
364
  } else {
284
365
  return "Je n'ai trouvé aucun modèle correspondant à votre recherche.";
285
366
  }
@@ -296,16 +377,13 @@ async function executeTool(action, params, user, allModels) {
296
377
  /**
297
378
  * Gère la requête de chat, soit en exécutant une action confirmée,
298
379
  * soit en lançant la boucle de raisonnement de l'IA.
299
- * @param {string} message - Le message de l'utilisateur.
300
- * @param {Array} history - L'historique de la conversation.
301
- * @param {string} provider - Le fournisseur d'IA ('OpenAI' ou 'google').
302
- * @param {object} context - Contexte additionnel.
380
+ @param {object} params - La liste des paramètres envoyés par la requete
303
381
  * @param {object} user - L'objet utilisateur.
304
- * @param {object} confirmedAction - Une action pré-approuvée par l'utilisateur.
305
382
  * @returns {Promise<object>} La réponse de l'assistant.
306
383
  */
307
- async function handleChatRequest(message, history, provider, context, user, confirmedAction) {
384
+ export async function handleChatRequest(params, user) {
308
385
 
386
+ const { message, history, provider, confirmedAction } = params;
309
387
  const allModels = await modelsCollection.find({$or: [{_user: {$exists: false}}, {_user: user.username}]}).toArray();
310
388
 
311
389
  // --- GESTION D'UNE ACTION CONFIRMÉE ---
@@ -329,8 +407,9 @@ async function handleChatRequest(message, history, provider, context, user, conf
329
407
 
330
408
  // --- INITIALISATION DE L'IA ---
331
409
  let llm;
410
+ let llmOptions = { };
332
411
  try {
333
- const p = provider || 'OpenAI';
412
+ const p = Config.Get('assistant.provider', provider ||'OpenAI');
334
413
  const envKeyName = providers[p].key;
335
414
  if (!envKeyName) return {success: false, message: `Fournisseur IA non supporté : ${p}`};
336
415
 
@@ -340,15 +419,17 @@ async function handleChatRequest(message, history, provider, context, user, conf
340
419
 
341
420
  if (!apiKey) return {success: false, message: `Clé API pour ${p} (${envKeyName}) non trouvée.`};
342
421
 
343
- llm = getAIProvider(p, providers[p]?.defaultModel, apiKey);
422
+ const model = Config.Get('assistant.model', providers[p]?.defaultModel);
423
+ llmOptions = { provider: p, model, apiKey };
424
+ llm = await getAIProvider(llmOptions.provider, llmOptions.model, llmOptions.apiKey);
344
425
  } catch (initError) {
345
426
  logger.error(`[Assistant] Erreur d'initialisation du client IA: ${initError.message}`);
346
427
  return {success: false, message: `Erreur d'initialisation du client IA: ${initError.message}`};
347
428
  }
348
429
 
349
- // --- PRÉPARATION DE L'HISTORIQUE DE CONVERSATION ---
350
- const systemPrompt = createSystemPrompt([], user.lang || 'en');
351
- const conversationHistory = history
430
+ const systemPrompt = await Event.Trigger('OnSystemPrompt', 'event', 'user', user);
431
+
432
+ const conversationHistory = (history || [])
352
433
  .filter(msg => msg.text && !(msg.from === 'bot' && msg.text.startsWith(i18n.t('assistant.welcome'))))
353
434
  .map(msg => new (msg.from === 'user' ? HumanMessage : SystemMessage)(msg.text));
354
435
 
@@ -367,80 +448,56 @@ async function handleChatRequest(message, history, provider, context, user, conf
367
448
  // Parsing JSON robuste
368
449
  let parsedResponse;
369
450
  try {
451
+ // Tente d'extraire le JSON de la réponse, même s'il est entouré de texte.
452
+ const jsonRegex = /\{[\s\S]*\}/s; // 's' flag pour que '.' matche les nouvelles lignes
453
+ const match = llmOutput.match(jsonRegex);
370
454
 
371
- parsedResponse = JSON.parse(llmOutput);
455
+ if (match && match[0]) {
456
+ // Si un JSON est trouvé, on tente de le parser
457
+ parsedResponse = parseSafeJSON(match[0]);
458
+ } else {
459
+ // Aucun JSON trouvé, c'est probablement une réponse textuelle simple.
460
+ return { success: true, displayMessage: llmOutput };
461
+ }
372
462
 
373
463
  if (!parsedResponse.action || !parsedResponse.params) {
374
464
  throw new Error("Réponse JSON invalide: 'action' ou 'params' manquant.");
375
465
  }
376
466
  } catch (parseError) {
377
467
  logger.error(`[Assistant] Erreur de parsing de la réponse de l'IA: ${parseError.message}. Réponse brute: "${llmOutput}"`);
378
- return {
379
- success: true,
380
- displayMessage: llmOutput || i18n.t('assistant.invalidResponse', "Désolé, je n'ai pas pu formuler une réponse correcte. Veuillez réessayer.")
381
- };
468
+ // Si le parsing échoue, on renvoie le message brut de l'IA, qui est peut-être une réponse textuelle valide.
469
+ return { success: true, displayMessage: llmOutput };
382
470
  }
383
471
 
384
472
  logger.debug(`[Assistant] Action décidée par l'IA: ${parsedResponse.action}`, parsedResponse);
385
473
  conversationHistory.push(new SystemMessage(JSON.stringify(parsedResponse)));
386
474
 
387
- const { action, params } = parsedResponse;
475
+ const { action, params:parsedParams } = parsedResponse;
388
476
 
389
477
  // Correction automatique du filtre généré par l'IA, qui hallucine parfois
390
- if (params && params.filter) {
391
- logger.debug(`[Assistant] Filtre original de l'IA: ${JSON.stringify(params.filter)}`);
392
- params.filter = correctAIFilter(params.filter);
478
+ if (parsedParams && parsedParams.filter) {
479
+ logger.debug(`[Assistant] Filtre original de l'IA: ${JSON.stringify(parsedParams.filter)}`);
480
+ parsedParams.filter = correctAIFilter(parsedParams.filter);
393
481
  }
394
482
 
395
- // Action de génération de graphique, gérée par le front-end
396
- if (action === 'generateChart') {
397
- // On retourne directement la configuration du graphique au client.
398
- return { success: true, chartConfig: params };
483
+ // Outils pour le raisonnement interne de l'IA
484
+ if (['search_models'].includes(action)) { // On a enlevé 'search' d'ici
485
+ const toolResult = await executeTool(action, parsedParams, user, allModels);
486
+ conversationHistory.push(new SystemMessage(`Résultat de l'outil '${action}':\n${toolResult}`));
487
+ continue; // On continue la boucle pour que l'IA puisse raisonner avec ce nouveau résultat
399
488
  }
400
489
 
401
- // NOUVEAU: Action de recherche à afficher, gérée par le front-end
402
- if (action === 'search') {
403
- const searchResult = await searchData({
404
- model: params.model,
405
- filter: params.filter,
406
- limit: params.limit || 10,
407
- sort: params.sort
408
- }, user);
409
- if (searchResult.data?.length > 0) {
410
- return { success: true, dataResult: { model: params.model, data: searchResult.data } };
411
- } else {
412
- return { success: true, displayMessage: i18n.t('assistant.search.noResults', "Je n'ai trouvé aucun résultat.") };
413
- }
414
- }
490
+ const res = await Event.Trigger('OnChatAction', 'event', 'user', action, params, parsedResponse, llmOptions, user);
415
491
 
416
- // Actions nécessitant une confirmation de l'utilisateur
417
- if (['post', 'update', 'delete'].includes(action)) {
418
- const confirmationMessage = i18n.t('assistant.confirmActionPrompt', "Veuillez confirmer l'action suivante :");
492
+ if( !res ) {
493
+ // Si l'action n'est reconnue par aucune des logiques ci-dessus
494
+ logger.warn(`[Assistant] Action non reconnue reçue de l'IA: ${action}`);
419
495
  return {
420
496
  success: true,
421
- displayMessage: confirmationMessage,
422
- confirmationRequest: parsedResponse
497
+ displayMessage: i18n.t('assistant.unknownAction', "Désolé, je ne comprends pas la commande '{{action}}'.", {action})
423
498
  };
424
499
  }
425
-
426
- // Action finale pour afficher un message
427
- if (action === 'displayMessage') {
428
- return { success: true, displayMessage: params.message };
429
- }
430
-
431
- // Outils pour le raisonnement interne de l'IA
432
- if (['search_models'].includes(action)) { // On a enlevé 'search' d'ici
433
- const toolResult = await executeTool(action, params, user, allModels);
434
- conversationHistory.push(new SystemMessage(`Résultat de l'outil '${action}':\n${toolResult}`));
435
- continue; // On continue la boucle pour que l'IA puisse raisonner avec ce nouveau résultat
436
- }
437
-
438
- // Si l'action n'est reconnue par aucune des logiques ci-dessus
439
- logger.warn(`[Assistant] Action non reconnue reçue de l'IA: ${action}`);
440
- return {
441
- success: true,
442
- displayMessage: i18n.t('assistant.unknownAction', "Désolé, je ne comprends pas la commande '{{action}}'.", { action })
443
- };
500
+ return res;
444
501
  }
445
502
 
446
503
  // Si la boucle se termine sans une action finale
@@ -473,12 +530,74 @@ async function executeConfirmedAction(action, params, user) {
473
530
  }
474
531
  }
475
532
 
533
+ /**
534
+ * Gère une action finale décidée par l'IA et retourne une réponse pour le client.
535
+ * Cette fonction est appelée via le système d'événements 'OnChatAction'.
536
+ * @param {string} action - L'action à exécuter.
537
+ * @param {object} params - Les paramètres de l'action.
538
+ * @param {object} parsedResponse - La réponse JSON originale de l'IA, pour les demandes de confirmation.
539
+ * @param {object} user - L'objet utilisateur.
540
+ * @returns {Promise<object|boolean>} - L'objet de réponse final ou false si l'action n'est pas reconnue.
541
+ */
542
+ async function handleFinalChatAction(action, params, parsedResponse, user) {
543
+ // Action de génération de graphique, gérée par le front-end
544
+ if (action === 'generateChart') {
545
+ return {success: true, chartConfig: params};
546
+ }
547
+ // Action de génération de vue HTML
548
+ if (action === 'generateHtmlView') {
549
+ const viewData = await searchData({
550
+ model: params.model,
551
+ filter: params.filter,
552
+ limit: params.limit || 10,
553
+ depth: 2 // Pour avoir accès aux relations de premier niveau dans les templates
554
+ }, user);
555
+
556
+ if (viewData.data.length === 0) {
557
+ return {
558
+ success: true,
559
+ displayMessage: i18n.t('assistant.htmlView.noResult', "Je n'ai trouvé aucune donnée correspondante pour cette vue.")
560
+ };
561
+ }
562
+ return {success: true, htmlViewConfig: {...params, data: viewData.data}};
563
+ }
564
+
565
+ // Action de recherche à afficher, gérée par le front-end
566
+ if (action === 'search') {
567
+ const searchResult = await searchData({ model: params.model, filter: params.filter, limit: params.limit || 10, sort: params.sort }, user);
568
+ if (searchResult.data?.length > 0) {
569
+ return {success: true, dataResult: {model: params.model, data: searchResult.data}};
570
+ } else {
571
+ return { success: true, displayMessage: i18n.t('assistant.search.noResults', "Je n'ai trouvé aucun résultat.") };
572
+ }
573
+ }
574
+
575
+ // Actions nécessitant une confirmation de l'utilisateur
576
+ if (['post', 'update', 'delete'].includes(action)) {
577
+ return { success: true, displayMessage: i18n.t('assistant.confirmActionPrompt', "Veuillez confirmer l'action suivante :"), confirmationRequest: parsedResponse };
578
+ }
579
+
580
+ // Action finale pour afficher un message
581
+ if (action === 'displayMessage') {
582
+ return {success: true, displayMessage: params.message};
583
+ }
584
+
585
+ return false; // Action non reconnue par cet écouteur
586
+ }
587
+
476
588
 
477
589
  export async function onInit(engine) {
478
590
  logger = engine.getComponent(Logger);
479
591
 
480
592
  const {middlewareAuthenticator, userInitiator} = await import('../user.js');
481
593
 
594
+ Event.Listen('OnSystemPrompt', (user) => {
595
+ return createSystemPrompt([], user.lang || 'en');
596
+ }, 'event', 'user');
597
+ // Enregistre le gestionnaire central pour les actions finales du chat.
598
+ // D'autres modules pourraient également écouter cet événement pour étendre les fonctionnalités.
599
+ Event.Listen('OnChatAction', handleFinalChatAction,"event", "user");
600
+
482
601
  engine.post('/api/assistant/chat', [middlewareAuthenticator, userInitiator, assistantGlobalLimiter, generateLimiter], async (req, res) => {
483
602
  // On récupère TOUTES les propriétés du body, y compris l'action confirmée
484
603
  const {message, history, provider, context, confirmedAction} = req.fields;
@@ -500,7 +619,7 @@ export async function onInit(engine) {
500
619
  }
501
620
 
502
621
  try {
503
- const result = await handleChatRequest(message, history, provider, context, req.me, confirmedAction);
622
+ const result = await handleChatRequest(req.fields, req.me);
504
623
  res.json(result);
505
624
  } catch (error) {
506
625
  logger.error(`[Endpoint /api/assistant/chat] Erreur inattendue: ${error.message}`, error.stack);
@@ -8,7 +8,8 @@ export const providers = {
8
8
  key:"GOOGLE_API_KEY"
9
9
  },
10
10
  "DeepSeek": {
11
- key: "DEEPSEEK_API_KEY"
11
+ key: "DEEPSEEK_API_KEY",
12
+ defaultModel: 'deepseek-chat'
12
13
  },
13
14
  "Anthropic": {
14
15
  key:"ANTHROPIC_API_KEY"
@@ -6,7 +6,7 @@ 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";
@@ -38,6 +38,7 @@ export const requestRestore = async (user, lang) => {
38
38
  content: i18n.t('email.backup.restoreRequest.content', {
39
39
  user: user?.username,
40
40
  fullToken: fullRestoreToken,
41
+ host: getHost(),
41
42
  modelsToken: modelsRestoreToken
42
43
  })
43
44
  });