Lucas ARRIESSE commited on
Commit
3075a31
·
1 Parent(s): 1cca255

Fix merge conflict

Browse files
Files changed (3) hide show
  1. index.html +12 -6
  2. static/script.js +14 -284
  3. static/ui-utils.js +270 -0
index.html CHANGED
@@ -13,10 +13,6 @@
13
  <!-- Loading Overlay -->
14
  <div id="loading-overlay" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden">
15
  <div class="bg-white p-6 rounded-lg shadow-lg text-center">
16
- <!-- <div id="loading-bar" class="w-64 h-4 bg-gray-200 rounded-full mb-4">
17
- <div class="h-full bg-blue-500 rounded-full animate-pulse"></div>
18
- </div> -->
19
-
20
  <span class="loading loading-spinner loading-xl"></span>
21
  <p id="progress-text" class="text-gray-700">Chargement en cours...</p>
22
  </div>
@@ -247,8 +243,8 @@
247
  <div id="solutions-tab-contents" class="hidden pt-10">
248
  <!--Header de catégorisation des requirements-->
249
  <div class="w-full mx-auto mt-4 p-4 bg-base-100 rounded-box shadow-lg border border-base-200">
 
250
  <div class="flex flex-wrap justify-begin items-center gap-6">
251
-
252
  <!-- Number input -->
253
  <div class="form-control flex flex-col items-center justify-center">
254
  <label class="label" for="category-count">
@@ -262,7 +258,7 @@
262
  <label class="label">
263
  <span class="label-text text-base-content">Number of categories</span>
264
  </label>
265
- <input type="checkbox" class="toggle toggle-primary" id="auto-detect-toggle" />
266
  <div class="text-xs mt-1 text-base-content">Manual | Auto detect</div>
267
  </div>
268
 
@@ -271,6 +267,15 @@
271
  Categorize
272
  </button>
273
  </div>
 
 
 
 
 
 
 
 
 
274
  </div>
275
  <div id="categorized-requirements-container pt-10" class="mb-6">
276
  <div class="flex mb-4 pt-10">
@@ -326,6 +331,7 @@
326
  </div>
327
 
328
  <script src="/static/sse.js"></script>
 
329
  <script src="/static/script.js"></script>
330
  </body>
331
 
 
13
  <!-- Loading Overlay -->
14
  <div id="loading-overlay" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden">
15
  <div class="bg-white p-6 rounded-lg shadow-lg text-center">
 
 
 
 
16
  <span class="loading loading-spinner loading-xl"></span>
17
  <p id="progress-text" class="text-gray-700">Chargement en cours...</p>
18
  </div>
 
243
  <div id="solutions-tab-contents" class="hidden pt-10">
244
  <!--Header de catégorisation des requirements-->
245
  <div class="w-full mx-auto mt-4 p-4 bg-base-100 rounded-box shadow-lg border border-base-200">
246
+ <!-- Séléction du nb de catégories / mode auto-->
247
  <div class="flex flex-wrap justify-begin items-center gap-6">
 
248
  <!-- Number input -->
249
  <div class="form-control flex flex-col items-center justify-center">
250
  <label class="label" for="category-count">
 
258
  <label class="label">
259
  <span class="label-text text-base-content">Number of categories</span>
260
  </label>
261
+ <input type="checkbox" class="toggle toggle-primary" id="auto-detect-toggle" checked="true" />
262
  <div class="text-xs mt-1 text-base-content">Manual | Auto detect</div>
263
  </div>
264
 
 
267
  Categorize
268
  </button>
269
  </div>
270
+ <div class="flex flex-wrap justify-end items-center gap-6">
271
+ <div class="tooltip" data-tip="Use Insight Finder's API to generate solutions">
272
+ <label class="label">
273
+ <span class="label-text text-base-content">Use insight finder solver</span>
274
+ </label>
275
+ <input type="checkbox" class="toggle toggle-primary" id="use-insight-finder-solver"
276
+ checked="false" />
277
+ </div>
278
+ </div>
279
  </div>
280
  <div id="categorized-requirements-container pt-10" class="mb-6">
281
  <div class="flex mb-4 pt-10">
 
331
  </div>
332
 
333
  <script src="/static/sse.js"></script>
334
+ <script src="/static/ui-utils.js"></script>
335
  <script src="/static/script.js"></script>
336
  </body>
337
 
static/script.js CHANGED
@@ -17,277 +17,6 @@ let lastSelectedRequirementsChecksum = null;
17
  // les requirements ont ils été extraits au moins une fois ?
18
  let hasRequirementsExtracted = false;
19
 
20
- // =============================================================================
21
- // FONCTIONS UTILITAIRES POUR LA GESTION DES ÉLÉMENTS
22
- // =============================================================================
23
-
24
- /**
25
- * Active/désactive des éléments par leurs IDs
26
- * @param {string[]} elementIds - Liste des IDs des éléments à activer
27
- * @param {boolean} enabled - true pour activer, false pour désactiver
28
- */
29
- function toggleElementsEnabled(elementIds, enabled = true) {
30
- elementIds.forEach(id => {
31
- const element = document.getElementById(id);
32
- if (element) {
33
- if (enabled) {
34
- element.removeAttribute('disabled');
35
- } else {
36
- element.setAttribute('disabled', 'true');
37
- }
38
- }
39
- });
40
- }
41
-
42
- /**
43
- * Affiche/masque des conteneurs par leurs IDs
44
- * @param {string[]} containerIds - Liste des IDs des conteneurs à afficher
45
- * @param {boolean} visible - true pour afficher, false pour masquer
46
- */
47
- function toggleContainersVisibility(containerIds, visible = true) {
48
- containerIds.forEach(id => {
49
- const container = document.getElementById(id);
50
- if (container) {
51
- if (visible) {
52
- container.classList.remove('hidden');
53
- } else {
54
- container.classList.add('hidden');
55
- }
56
- }
57
- });
58
- }
59
-
60
- /**
61
- * Affiche le loading overlay avec un message personnalisé
62
- * @param {string} message - Message à afficher
63
- */
64
- function showLoadingOverlay(message = 'Loading...') {
65
- document.getElementById('progress-text').textContent = message;
66
- toggleContainersVisibility(['loading-overlay'], true);
67
- }
68
-
69
- /**
70
- * Masque le loading overlay
71
- */
72
- function hideLoadingOverlay() {
73
- toggleContainersVisibility(['loading-overlay'], false);
74
- }
75
-
76
- /**
77
- * Réinitialise un select et ajoute des options
78
- * @param {string} selectId - ID du select
79
- * @param {Object} options - Objet avec les options {value: text}
80
- * @param {string} defaultText - Texte par défaut
81
- */
82
- function populateSelect(selectId, options, defaultText = 'Select...') {
83
- const select = document.getElementById(selectId);
84
- if (select) {
85
- select.innerHTML = `<option value="">${defaultText}</option>`;
86
- Object.entries(options).forEach(([text, value]) => {
87
- const option = document.createElement('option');
88
- option.value = value;
89
- option.textContent = text;
90
- select.appendChild(option);
91
- });
92
- }
93
- }
94
-
95
- function populateCheckboxDropdown(optionsContainerId, options, filterType, labelId, selectionSet) {
96
- const container = document.getElementById(optionsContainerId);
97
- container.innerHTML = '';
98
- selectionSet.clear(); // reset all
99
-
100
- // Ajoute chaque option
101
- options.forEach(option => {
102
- const safeId = `${filterType}-${encodeURIComponent(option).replace(/[%\s]/g, '_')}`;
103
- const label = document.createElement('label');
104
- label.className = "flex items-center gap-2 cursor-pointer py-1";
105
- label.innerHTML = `
106
- <input type="checkbox" class="${filterType}-checkbox option-checkbox" id="${safeId}" value="${option}">
107
- <span>${option}</span>
108
- `;
109
- label.querySelector('input').addEventListener('change', function () {
110
- if (this.checked) {
111
- selectionSet.add(this.value);
112
- } else {
113
- selectionSet.delete(this.value);
114
- }
115
- // Gestion du label "Tous"
116
- updateCheckboxDropdownLabel(filterType, labelId, selectionSet, options.length);
117
- // Gestion du "Tous" global
118
- const allBox = document.querySelector(`.${filterType}-checkbox[value="all"]`);
119
- if (allBox && allBox.checked) allBox.checked = false;
120
- // Si plus rien n'est coché, recoche "Tous"
121
- if (selectionSet.size === 0 && allBox) allBox.checked = true;
122
- applyFilters();
123
- });
124
- container.appendChild(label);
125
- });
126
-
127
- // Réinitialise le label
128
- updateCheckboxDropdownLabel(filterType, labelId, selectionSet, options.length);
129
-
130
- // Gestion de "Tous"
131
- const allBox = document.querySelector(`.${filterType}-checkbox[value="all"]`);
132
- if (allBox) {
133
- allBox.addEventListener('change', function () {
134
- if (this.checked) {
135
- // Décoche tout le reste
136
- selectionSet.clear();
137
- container.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
138
- this.checked = true; // reste coché
139
- updateCheckboxDropdownLabel(filterType, labelId, selectionSet, options.length);
140
- applyFilters();
141
- }
142
- });
143
- }
144
- }
145
-
146
- function updateCheckboxDropdownLabel(type, labelId, set, totalCount) {
147
- const label = document.getElementById(labelId);
148
- if (!set.size) {
149
- label.textContent = type.charAt(0).toUpperCase() + type.slice(1) + " (All)";
150
- } else if (set.size === 1) {
151
- label.textContent = [...set][0];
152
- } else {
153
- label.textContent = `${type.charAt(0).toUpperCase() + type.slice(1)} (${set.size}/${totalCount})`;
154
- }
155
- }
156
-
157
- function updateSelectedFilters(filterType, value, isChecked) {
158
- if (isChecked) {
159
- selectedFilters[filterType].add(value);
160
- } else {
161
- selectedFilters[filterType].delete(value);
162
- }
163
- }
164
-
165
- function populateDaisyDropdown(menuId, options, labelId, onSelect) {
166
- const menu = document.getElementById(menuId);
167
- menu.innerHTML = '';
168
- // Option "Tous"
169
- const liAll = document.createElement('li');
170
- liAll.innerHTML = `<a data-value="">All</a>`;
171
- liAll.querySelector('a').onclick = e => {
172
- e.preventDefault();
173
- document.getElementById(labelId).textContent = "Type";
174
- onSelect("");
175
- };
176
- menu.appendChild(liAll);
177
-
178
- // Ajoute chaque option
179
- options.forEach(opt => {
180
- const li = document.createElement('li');
181
- li.innerHTML = `<a data-value="${opt}">${opt}</a>`;
182
- li.querySelector('a').onclick = e => {
183
- e.preventDefault();
184
- document.getElementById(labelId).textContent = opt;
185
- onSelect(opt);
186
- };
187
- menu.appendChild(li);
188
- });
189
- }
190
-
191
- function updateFilterLabel(filterType) {
192
- const selectedCount = selectedFilters[filterType].size;
193
- const labelElement = document.getElementById(`${filterType}-filter-label`);
194
-
195
- if (selectedCount === 0) {
196
- labelElement.textContent = `${filterType} (All)`;
197
- } else {
198
- labelElement.textContent = `${filterType} (${selectedCount} selected)`;
199
- }
200
- }
201
-
202
- /**
203
- * Extrait les données du tableau selon un mapping
204
- * @param {Object} mapping - Mapping des colonnes {columnName: propertyName}
205
- * @returns {Array} Données extraites
206
- */
207
- function extractTableData(mapping) {
208
- const tbody = document.querySelector('#data-table tbody');
209
- const rows = tbody.querySelectorAll('tr');
210
- const data = [];
211
-
212
- rows.forEach(row => {
213
- const checkboxes = row.querySelectorAll('input[type="checkbox"]:checked');
214
- if (checkboxes.length > 0) {
215
- const rowData = {};
216
- Object.entries(mapping).forEach(([columnName, propertyName]) => {
217
- const cell = row.querySelector(`td[data-column="${columnName}"]`);
218
- if (cell) {
219
- if (columnName == "URL") {
220
- rowData[propertyName] = cell.querySelector('a').getAttribute('href');
221
- } else {
222
- rowData[propertyName] = cell.textContent.trim();
223
- }
224
- }
225
- });
226
- data.push(rowData);
227
- }
228
- });
229
-
230
- return data;
231
- }
232
-
233
- const TABS = {
234
- 'doc-table-tab': 'doc-table-tab-contents',
235
- 'requirements-tab': 'requirements-tab-contents',
236
- 'solutions-tab': 'solutions-tab-contents',
237
- 'query-tab': 'query-tab-contents'
238
- };
239
-
240
- /**
241
- * Bascule l'affichage sur le nouveau tab
242
- * @param {*} newTab
243
- */
244
- function switchTab(newTab) {
245
- // Remove active tab style from all tabs
246
- Object.keys(TABS).forEach(tabId => {
247
- const tabElement = document.getElementById(tabId);
248
- if (tabElement) {
249
- tabElement.classList.remove("tab-active");
250
- }
251
- });
252
-
253
- // Hide all tab contents
254
- Object.values(TABS).forEach(contentId => {
255
- const contentElement = document.getElementById(contentId);
256
- if (contentElement) {
257
- contentElement.classList.add("hidden");
258
- }
259
- });
260
-
261
- // Activate the new tab if it exists in the mapping
262
- if (newTab in TABS) {
263
- const newTabElement = document.getElementById(newTab);
264
- const newContentElement = document.getElementById(TABS[newTab]);
265
-
266
- if (newTabElement) newTabElement.classList.add("tab-active");
267
- if (newContentElement) newContentElement.classList.remove("hidden");
268
- }
269
- }
270
-
271
- /**
272
- * Bascule l'affichage vers la tab uniquement si les requirements sont
273
- */
274
- function enableTabSwitching() {
275
- Object.keys(TABS).forEach(tabId => {
276
- const tab = document.getElementById(tabId);
277
- if (tab)
278
- tab.classList.remove("tab-disabled");
279
- })
280
- }
281
-
282
-
283
- /**
284
- * Change l'état d'activation du number box de choix de nb de catégories.
285
- */
286
- function debounceAutoCategoryCount(state) {
287
- document.getElementById('category-count').disabled = state;
288
- }
289
-
290
-
291
  // =============================================================================
292
  // FONCTIONS MÉTIER
293
  // =============================================================================
@@ -348,8 +77,6 @@ async function getTDocs() {
348
  'filters-container',
349
  'action-buttons-container',
350
  'doc-table-tab-contents',
351
- // 'data-table-container',
352
- // 'data-table-info-container'
353
  ], true);
354
 
355
  hasRequirementsExtracted = false;
@@ -1298,30 +1025,33 @@ function clearAllSolutions() {
1298
  document.querySelectorAll('.solution-accordion').forEach(a => a.remove());
1299
  }
1300
 
1301
- async function generateSolutions(selected_categories, user_constraints) {
1302
  console.log(selected_categories);
1303
 
1304
  let input_req = structuredClone(selected_categories);
1305
  input_req.user_constraints = user_constraints;
 
 
 
1306
 
1307
- let response = await fetch('https://organizedprogrammers-reqxtract-api.hf.space/solution/search_solutions_gemini/v2', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input_req) })
1308
  let responseObj = await response.json()
1309
  return responseObj;
1310
  }
1311
 
1312
- async function generateCriticisms(solutions) {
1313
  let response = await fetch('https://organizedprogrammers-reqxtract-api.hf.space/solution/criticize_solution', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(solutions) })
1314
  let responseObj = await response.json()
1315
  solutionsCriticizedVersions.push(responseObj)
1316
  }
1317
 
1318
- async function refineSolutions(critiques) {
1319
  let response = await fetch('https://organizedprogrammers-reqxtract-api.hf.space/solution/refine_solutions', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(critiques) })
1320
  let responseObj = await response.json()
1321
- await generateCriticisms(responseObj)
1322
  }
1323
 
1324
- async function workflow(steps = 1) {
1325
  let soluce;
1326
  showLoadingOverlay('Génération des solutions & critiques ....');
1327
 
@@ -1341,11 +1071,11 @@ async function workflow(steps = 1) {
1341
  }
1342
 
1343
  if (solutionsCriticizedVersions.length == 0) {
1344
- soluce = await generateSolutions(selected_requirements, user_constraints ? user_constraints : null);
1345
- await generateCriticisms(soluce)
1346
  } else {
1347
  let prevSoluce = solutionsCriticizedVersions[solutionsCriticizedVersions.length - 1];
1348
- await refineSolutions(prevSoluce)
1349
  }
1350
  }
1351
  hideLoadingOverlay();
@@ -1377,10 +1107,10 @@ document.addEventListener('DOMContentLoaded', function () {
1377
  // Événements pour les boutons de solutions (à implémenter plus tard)
1378
  document.getElementById('get-solutions-btn').addEventListener('click', () => {
1379
  const n_steps = document.getElementById('solution-gen-nsteps').value;
1380
- workflow(n_steps);
1381
  });
1382
  document.getElementById('get-solutions-step-btn').addEventListener('click', () => {
1383
- workflow();
1384
  });
1385
  });
1386
 
 
17
  // les requirements ont ils été extraits au moins une fois ?
18
  let hasRequirementsExtracted = false;
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  // =============================================================================
21
  // FONCTIONS MÉTIER
22
  // =============================================================================
 
77
  'filters-container',
78
  'action-buttons-container',
79
  'doc-table-tab-contents',
 
 
80
  ], true);
81
 
82
  hasRequirementsExtracted = false;
 
1025
  document.querySelectorAll('.solution-accordion').forEach(a => a.remove());
1026
  }
1027
 
1028
+ async function generateSolutionsV1(selected_categories, user_constraints = null, useIFSolver = false) {
1029
  console.log(selected_categories);
1030
 
1031
  let input_req = structuredClone(selected_categories);
1032
  input_req.user_constraints = user_constraints;
1033
+ console.log(useIFSolver);
1034
+
1035
+ const base_url = useIFSolver ? "https://organizedprogrammers-reqxtract-api.hf.space/solution/search_solutions_if" : "https://organizedprogrammers-reqxtract-api.hf.space/solution/search_solutions_gemini/v2"
1036
 
1037
+ let response = await fetch(base_url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input_req) })
1038
  let responseObj = await response.json()
1039
  return responseObj;
1040
  }
1041
 
1042
+ async function generateCriticismsV1(solutions) {
1043
  let response = await fetch('https://organizedprogrammers-reqxtract-api.hf.space/solution/criticize_solution', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(solutions) })
1044
  let responseObj = await response.json()
1045
  solutionsCriticizedVersions.push(responseObj)
1046
  }
1047
 
1048
+ async function refineSolutionsV1(critiques) {
1049
  let response = await fetch('https://organizedprogrammers-reqxtract-api.hf.space/solution/refine_solutions', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(critiques) })
1050
  let responseObj = await response.json()
1051
+ await generateCriticismsV1(responseObj)
1052
  }
1053
 
1054
+ async function workflow(steps = 1, useIFSolver = false) {
1055
  let soluce;
1056
  showLoadingOverlay('Génération des solutions & critiques ....');
1057
 
 
1071
  }
1072
 
1073
  if (solutionsCriticizedVersions.length == 0) {
1074
+ soluce = await generateSolutionsV1(selected_requirements, user_constraints ? user_constraints : null, useIFSolver);
1075
+ await generateCriticismsV1(soluce)
1076
  } else {
1077
  let prevSoluce = solutionsCriticizedVersions[solutionsCriticizedVersions.length - 1];
1078
+ await refineSolutionsV1(prevSoluce)
1079
  }
1080
  }
1081
  hideLoadingOverlay();
 
1107
  // Événements pour les boutons de solutions (à implémenter plus tard)
1108
  document.getElementById('get-solutions-btn').addEventListener('click', () => {
1109
  const n_steps = document.getElementById('solution-gen-nsteps').value;
1110
+ workflow(n_steps, document.getElementById('use-insight-finder-solver').checked);
1111
  });
1112
  document.getElementById('get-solutions-step-btn').addEventListener('click', () => {
1113
+ workflow(1, document.getElementById('use-insight-finder-solver').checked);
1114
  });
1115
  });
1116
 
static/ui-utils.js ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // =============================================================================
2
+ // FONCTIONS UTILITAIRES POUR LA GESTION DES ÉLÉMENTS
3
+ // =============================================================================
4
+
5
+ /**
6
+ * Active/désactive des éléments par leurs IDs
7
+ * @param {string[]} elementIds - Liste des IDs des éléments à activer
8
+ * @param {boolean} enabled - true pour activer, false pour désactiver
9
+ */
10
+ function toggleElementsEnabled(elementIds, enabled = true) {
11
+ elementIds.forEach(id => {
12
+ const element = document.getElementById(id);
13
+ if (element) {
14
+ if (enabled) {
15
+ element.removeAttribute('disabled');
16
+ } else {
17
+ element.setAttribute('disabled', 'true');
18
+ }
19
+ }
20
+ });
21
+ }
22
+
23
+ /**
24
+ * Affiche/masque des conteneurs par leurs IDs
25
+ * @param {string[]} containerIds - Liste des IDs des conteneurs à afficher
26
+ * @param {boolean} visible - true pour afficher, false pour masquer
27
+ */
28
+ function toggleContainersVisibility(containerIds, visible = true) {
29
+ containerIds.forEach(id => {
30
+ const container = document.getElementById(id);
31
+ if (container) {
32
+ if (visible) {
33
+ container.classList.remove('hidden');
34
+ } else {
35
+ container.classList.add('hidden');
36
+ }
37
+ }
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Affiche le loading overlay avec un message personnalisé
43
+ * @param {string} message - Message à afficher
44
+ */
45
+ function showLoadingOverlay(message = 'Chargement en cours...') {
46
+ document.getElementById('progress-text').textContent = message;
47
+ toggleContainersVisibility(['loading-overlay'], true);
48
+ }
49
+
50
+ /**
51
+ * Masque le loading overlay
52
+ */
53
+ function hideLoadingOverlay() {
54
+ toggleContainersVisibility(['loading-overlay'], false);
55
+ }
56
+
57
+ /**
58
+ * Réinitialise un select et ajoute des options
59
+ * @param {string} selectId - ID du select
60
+ * @param {Object} options - Objet avec les options {value: text}
61
+ * @param {string} defaultText - Texte par défaut
62
+ */
63
+ function populateSelect(selectId, options, defaultText = 'Sélectionner...') {
64
+ const select = document.getElementById(selectId);
65
+ if (select) {
66
+ select.innerHTML = `<option value="">${defaultText}</option>`;
67
+ Object.entries(options).forEach(([text, value]) => {
68
+ const option = document.createElement('option');
69
+ option.value = value;
70
+ option.textContent = text;
71
+ select.appendChild(option);
72
+ });
73
+ }
74
+ }
75
+
76
+ function populateCheckboxDropdown(optionsContainerId, options, filterType, labelId, selectionSet) {
77
+ const container = document.getElementById(optionsContainerId);
78
+ container.innerHTML = '';
79
+ selectionSet.clear(); // reset all
80
+
81
+ // Ajoute chaque option
82
+ options.forEach(option => {
83
+ const safeId = `${filterType}-${encodeURIComponent(option).replace(/[%\s]/g, '_')}`;
84
+ const label = document.createElement('label');
85
+ label.className = "flex items-center gap-2 cursor-pointer py-1";
86
+ label.innerHTML = `
87
+ <input type="checkbox" class="${filterType}-checkbox option-checkbox" id="${safeId}" value="${option}">
88
+ <span>${option}</span>
89
+ `;
90
+ label.querySelector('input').addEventListener('change', function () {
91
+ if (this.checked) {
92
+ selectionSet.add(this.value);
93
+ } else {
94
+ selectionSet.delete(this.value);
95
+ }
96
+ // Gestion du label "Tous"
97
+ updateCheckboxDropdownLabel(filterType, labelId, selectionSet, options.length);
98
+ // Gestion du "Tous" global
99
+ const allBox = document.querySelector(`.${filterType}-checkbox[value="all"]`);
100
+ if (allBox && allBox.checked) allBox.checked = false;
101
+ // Si plus rien n'est coché, recoche "Tous"
102
+ if (selectionSet.size === 0 && allBox) allBox.checked = true;
103
+ applyFilters();
104
+ });
105
+ container.appendChild(label);
106
+ });
107
+
108
+ // Réinitialise le label
109
+ updateCheckboxDropdownLabel(filterType, labelId, selectionSet, options.length);
110
+
111
+ // Gestion de "Tous"
112
+ const allBox = document.querySelector(`.${filterType}-checkbox[value="all"]`);
113
+ if (allBox) {
114
+ allBox.addEventListener('change', function () {
115
+ if (this.checked) {
116
+ // Décoche tout le reste
117
+ selectionSet.clear();
118
+ container.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
119
+ this.checked = true; // reste coché
120
+ updateCheckboxDropdownLabel(filterType, labelId, selectionSet, options.length);
121
+ applyFilters();
122
+ }
123
+ });
124
+ }
125
+ }
126
+
127
+ function updateCheckboxDropdownLabel(type, labelId, set, totalCount) {
128
+ const label = document.getElementById(labelId);
129
+ if (!set.size) {
130
+ label.textContent = type.charAt(0).toUpperCase() + type.slice(1) + " (Tous)";
131
+ } else if (set.size === 1) {
132
+ label.textContent = [...set][0];
133
+ } else {
134
+ label.textContent = `${type.charAt(0).toUpperCase() + type.slice(1)} (${set.size}/${totalCount})`;
135
+ }
136
+ }
137
+
138
+ function updateSelectedFilters(filterType, value, isChecked) {
139
+ if (isChecked) {
140
+ selectedFilters[filterType].add(value);
141
+ } else {
142
+ selectedFilters[filterType].delete(value);
143
+ }
144
+ }
145
+
146
+ function populateDaisyDropdown(menuId, options, labelId, onSelect) {
147
+ const menu = document.getElementById(menuId);
148
+ menu.innerHTML = '';
149
+ // Option "Tous"
150
+ const liAll = document.createElement('li');
151
+ liAll.innerHTML = `<a data-value="">Tous</a>`;
152
+ liAll.querySelector('a').onclick = e => {
153
+ e.preventDefault();
154
+ document.getElementById(labelId).textContent = "Type";
155
+ onSelect("");
156
+ };
157
+ menu.appendChild(liAll);
158
+
159
+ // Ajoute chaque option
160
+ options.forEach(opt => {
161
+ const li = document.createElement('li');
162
+ li.innerHTML = `<a data-value="${opt}">${opt}</a>`;
163
+ li.querySelector('a').onclick = e => {
164
+ e.preventDefault();
165
+ document.getElementById(labelId).textContent = opt;
166
+ onSelect(opt);
167
+ };
168
+ menu.appendChild(li);
169
+ });
170
+ }
171
+
172
+ function updateFilterLabel(filterType) {
173
+ const selectedCount = selectedFilters[filterType].size;
174
+ const labelElement = document.getElementById(`${filterType}-filter-label`);
175
+
176
+ if (selectedCount === 0) {
177
+ labelElement.textContent = `${filterType} (Tous)`;
178
+ } else {
179
+ labelElement.textContent = `${filterType} (${selectedCount} sélectionné${selectedCount > 1 ? 's' : ''})`;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Extrait les données du tableau selon un mapping
185
+ * @param {Object} mapping - Mapping des colonnes {columnName: propertyName}
186
+ * @returns {Array} Données extraites
187
+ */
188
+ function extractTableData(mapping) {
189
+ const tbody = document.querySelector('#data-table tbody');
190
+ const rows = tbody.querySelectorAll('tr');
191
+ const data = [];
192
+
193
+ rows.forEach(row => {
194
+ const checkboxes = row.querySelectorAll('input[type="checkbox"]:checked');
195
+ if (checkboxes.length > 0) {
196
+ const rowData = {};
197
+ Object.entries(mapping).forEach(([columnName, propertyName]) => {
198
+ const cell = row.querySelector(`td[data-column="${columnName}"]`);
199
+ if (cell) {
200
+ if (columnName == "URL") {
201
+ rowData[propertyName] = cell.querySelector('a').getAttribute('href');
202
+ } else {
203
+ rowData[propertyName] = cell.textContent.trim();
204
+ }
205
+ }
206
+ });
207
+ data.push(rowData);
208
+ }
209
+ });
210
+
211
+ return data;
212
+ }
213
+
214
+ const TABS = {
215
+ 'doc-table-tab': 'doc-table-tab-contents',
216
+ 'requirements-tab': 'requirements-tab-contents',
217
+ 'solutions-tab': 'solutions-tab-contents',
218
+ 'query-tab': 'query-tab-contents'
219
+ };
220
+
221
+ /**
222
+ * Bascule l'affichage sur le nouveau tab
223
+ * @param {*} newTab
224
+ */
225
+ function switchTab(newTab) {
226
+ // Remove active tab style from all tabs
227
+ Object.keys(TABS).forEach(tabId => {
228
+ const tabElement = document.getElementById(tabId);
229
+ if (tabElement) {
230
+ tabElement.classList.remove("tab-active");
231
+ }
232
+ });
233
+
234
+ // Hide all tab contents
235
+ Object.values(TABS).forEach(contentId => {
236
+ const contentElement = document.getElementById(contentId);
237
+ if (contentElement) {
238
+ contentElement.classList.add("hidden");
239
+ }
240
+ });
241
+
242
+ // Activate the new tab if it exists in the mapping
243
+ if (newTab in TABS) {
244
+ const newTabElement = document.getElementById(newTab);
245
+ const newContentElement = document.getElementById(TABS[newTab]);
246
+
247
+ if (newTabElement) newTabElement.classList.add("tab-active");
248
+ if (newContentElement) newContentElement.classList.remove("hidden");
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Bascule l'affichage vers la tab uniquement si les requirements sont
254
+ */
255
+ function enableTabSwitching() {
256
+ Object.keys(TABS).forEach(tabId => {
257
+ const tab = document.getElementById(tabId);
258
+ if (tab)
259
+ tab.classList.remove("tab-disabled");
260
+ })
261
+ }
262
+
263
+
264
+ /**
265
+ * Change l'état d'activation du number box de choix de nb de catégories.
266
+ */
267
+ function debounceAutoCategoryCount(state) {
268
+ document.getElementById('category-count').disabled = state;
269
+ }
270
+