data-primals-engine 1.5.0 → 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.
- package/README.md +35 -0
- package/client/src/AddWidgetTypeModal.jsx +47 -43
- package/client/src/App.jsx +2 -6
- package/client/src/App.scss +12 -0
- package/client/src/AssistantChat.jsx +363 -323
- package/client/src/AssistantChat.scss +27 -10
- package/client/src/Dashboard.jsx +480 -396
- package/client/src/Dashboard.scss +1 -1
- package/client/src/DashboardHtmlViewItem.jsx +147 -0
- package/client/src/DashboardView.jsx +654 -569
- package/client/src/DataEditor.jsx +10 -3
- package/client/src/DataLayout.jsx +805 -755
- package/client/src/DataLayout.scss +14 -0
- package/client/src/DataTable.jsx +39 -75
- package/client/src/Dialog.scss +1 -1
- package/client/src/Field.jsx +2057 -1825
- package/client/src/FlexViewCard.jsx +44 -0
- package/client/src/HistoryDialog.jsx +47 -14
- package/client/src/HtmlViewBuilderModal.jsx +91 -0
- package/client/src/HtmlViewBuilderModal.scss +18 -0
- package/client/src/HtmlViewCard.jsx +44 -0
- package/client/src/HtmlViewCard.scss +35 -0
- package/client/src/KanbanCard.jsx +1 -2
- package/client/src/ModelCreator.jsx +5 -4
- package/client/src/ModelCreatorField.jsx +51 -4
- package/client/src/ModelList.jsx +92 -53
- package/client/src/Notification.jsx +136 -136
- package/client/src/Notification.scss +0 -18
- package/client/src/Pagination.jsx +5 -3
- package/client/src/RelationField.jsx +354 -258
- package/client/src/RelationSelectorWidget.jsx +173 -0
- package/client/src/contexts/ModelContext.jsx +10 -1
- package/client/src/contexts/UIContext.jsx +72 -63
- package/client/src/filter.js +262 -212
- package/client/src/hooks/useValidation.js +75 -0
- package/client/src/translations.js +24 -24
- package/package.json +2 -1
- package/src/constants.js +1 -1
- package/src/defaultModels.js +1596 -1544
- package/src/i18n.js +710 -10
- package/src/modules/assistant/assistant.js +148 -18
- package/src/modules/bucket.js +2 -1
- package/src/modules/data/data.core.js +118 -92
- package/src/modules/data/data.history.js +531 -492
- package/src/modules/data/data.js +3 -53
- package/src/modules/data/data.operations.js +77 -26
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +1879 -1821
- package/src/modules/data/data.validation.js +81 -2
- package/src/modules/file.js +247 -238
- package/src/packs.js +5482 -5478
- package/test/data.integration.test.js +1115 -1060
|
@@ -53,14 +53,40 @@ export const assistantGlobalLimiter = rateLimit({
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
async function searchModels(query, user) {
|
|
56
|
-
if (!query) return [];
|
|
56
|
+
if (!query) return { main: [], related: [] };
|
|
57
57
|
const searchRegex = new RegExp(query, 'i');
|
|
58
|
-
|
|
58
|
+
const mainModels = await modelsCollection.find({
|
|
59
59
|
$or: [{ _user: user.username }, { _user: { $exists: false } }],
|
|
60
60
|
$and: [{ $or: [{ name: { $regex: searchRegex } }, { description: { $regex: searchRegex } }] }]
|
|
61
61
|
}, {
|
|
62
62
|
projection: { name: 1, description: 1, fields: 1, _id: 0 }
|
|
63
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 };
|
|
64
90
|
}
|
|
65
91
|
|
|
66
92
|
const createSystemPrompt = (modelDefs, lang) => {
|
|
@@ -87,7 +113,7 @@ Tu as accès aux outils et actions suivants.
|
|
|
87
113
|
|
|
88
114
|
OUTILS DE RAISONNEMENT INTERNE: (Utilisés pour collecter de l'information avant de décider de l'action finale)
|
|
89
115
|
1. **search_models**: Pour rechercher les modèles de données disponibles.
|
|
90
|
-
- 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.
|
|
91
117
|
====
|
|
92
118
|
|
|
93
119
|
ACTIONS FINALES: (Actions qui terminent ta réflexion et renvoient un résultat à l'utilisateur)
|
|
@@ -119,9 +145,24 @@ ACTIONS FINALES: (Actions qui terminent ta réflexion et renvoient un résultat
|
|
|
119
145
|
- \`yAxis\` (string): (Optionnel, sauf pour sum/avg/min/max) Le champ numérique à agréger du modèle.
|
|
120
146
|
- \`filter\` (object): (Optionnel, filtre de la recherche, même écriture stricte que pour les filtres de recherche (voir plus bas pour les exemples)
|
|
121
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
|
+
|
|
122
162
|
Voici le mémo pour assigner des valeurs aux champs des modèles,avec ces types de données :
|
|
123
163
|
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
|
|
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
|
|
125
166
|
utilise un filtre en retour si le type de champ est code et language='json' et conditionBuilder=true
|
|
126
167
|
utilise une chaine si c'est un type de champ code par défaut.
|
|
127
168
|
utilise une structure { "iso2langcode":"content..." } pour le champ multi-traductions richtext_t
|
|
@@ -135,13 +176,11 @@ utilise un tableau d'_ids pour remplir les champs relation multiple=true
|
|
|
135
176
|
utilise la valeur en héxadecimal, ex: '#FF0000' pour les champs de type : color
|
|
136
177
|
utilise les valeurs de cron standard '* * * * * *' pour : cronSchedule
|
|
137
178
|
|
|
138
|
-
PROCESSUS DE RAISONNEMENT:
|
|
139
|
-
a- L'utilisateur pose une question
|
|
140
|
-
b-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Si tu n'as aucune commande à exécuter directement, réponds simplement à l'utilisateur avec "displayMessage".
|
|
144
|
-
|
|
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.
|
|
145
184
|
CONTEXTE ACTUEL:
|
|
146
185
|
- Date du jour de la conversation : ${dt}
|
|
147
186
|
- La langue ISO à utiliser dans la conversation : ${lang}
|
|
@@ -183,6 +222,67 @@ COMMANDE FINALE :
|
|
|
183
222
|
}
|
|
184
223
|
}
|
|
185
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
|
+
|
|
186
286
|
Absolute rules:
|
|
187
287
|
====
|
|
188
288
|
NEVER use the non-aggregated syntax of the MongoDB operators :
|
|
@@ -275,11 +375,17 @@ async function executeTool(action, params, user, allModels) {
|
|
|
275
375
|
return resultString;
|
|
276
376
|
}
|
|
277
377
|
case 'search_models': {
|
|
278
|
-
const foundModels = await searchModels(params.query, user);
|
|
378
|
+
const { main: foundModels, related: relatedModels } = await searchModels(params.query, user);
|
|
279
379
|
|
|
280
380
|
if (foundModels.length > 0) {
|
|
281
|
-
|
|
381
|
+
let responseText = "J'ai trouvé les modèles suivants qui pourraient correspondre : " +
|
|
282
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;
|
|
283
389
|
} else {
|
|
284
390
|
return "Je n'ai trouvé aucun modèle correspondant à votre recherche.";
|
|
285
391
|
}
|
|
@@ -367,18 +473,25 @@ async function handleChatRequest(message, history, provider, context, user, conf
|
|
|
367
473
|
// Parsing JSON robuste
|
|
368
474
|
let parsedResponse;
|
|
369
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);
|
|
370
479
|
|
|
371
|
-
|
|
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
|
+
}
|
|
372
487
|
|
|
373
488
|
if (!parsedResponse.action || !parsedResponse.params) {
|
|
374
489
|
throw new Error("Réponse JSON invalide: 'action' ou 'params' manquant.");
|
|
375
490
|
}
|
|
376
491
|
} catch (parseError) {
|
|
377
492
|
logger.error(`[Assistant] Erreur de parsing de la réponse de l'IA: ${parseError.message}. Réponse brute: "${llmOutput}"`);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
displayMessage: llmOutput || i18n.t('assistant.invalidResponse', "Désolé, je n'ai pas pu formuler une réponse correcte. Veuillez réessayer.")
|
|
381
|
-
};
|
|
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 };
|
|
382
495
|
}
|
|
383
496
|
|
|
384
497
|
logger.debug(`[Assistant] Action décidée par l'IA: ${parsedResponse.action}`, parsedResponse);
|
|
@@ -398,6 +511,23 @@ async function handleChatRequest(message, history, provider, context, user, conf
|
|
|
398
511
|
return { success: true, chartConfig: params };
|
|
399
512
|
}
|
|
400
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
|
+
|
|
401
531
|
// NOUVEAU: Action de recherche à afficher, gérée par le front-end
|
|
402
532
|
if (action === 'search') {
|
|
403
533
|
const searchResult = await searchData({
|
package/src/modules/bucket.js
CHANGED
|
@@ -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
|
});
|
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
}
|