MMOON commited on
Commit
769300f
·
verified ·
1 Parent(s): 7a08987

Create index.html

Browse files
Files changed (1) hide show
  1. index.html +2059 -0
index.html ADDED
@@ -0,0 +1,2059 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --- START OF FILE ASSITANT IFSAP SPECIFIQUE.html ---
2
+
3
+ <!DOCTYPE html>
4
+ <html lang="fr">
5
+ <head>
6
+ <meta charset="UTF-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <title>Assistant VisiPilot pour Plan d'Actions IFS</title>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
11
+ <style>
12
+ /* General Styles */
13
+ body {
14
+ font-family: 'Inter', 'Roboto', sans-serif;
15
+ margin: 0;
16
+ padding: 0;
17
+ background-color: #f8fafd;
18
+ color: #333;
19
+ line-height: 1.6;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1200px;
24
+ margin: 25px auto;
25
+ padding: 30px;
26
+ background-color: #ffffff;
27
+ border-radius: 12px;
28
+ box-shadow: 0 5px 25px rgba(0, 0, 0, 0.1);
29
+ }
30
+
31
+ /* Banner */
32
+ .banner {
33
+ background-image: url('https://raw.githubusercontent.com/M00N69/BUSCAR/main/logo%2002%20copie.jpg');
34
+ background-size: cover;
35
+ height: 120px;
36
+ background-position: center;
37
+ margin-bottom: 0;
38
+ border-radius: 10px 10px 0 0;
39
+ position: relative;
40
+ background-blend-mode: overlay;
41
+ background-color: rgba(26, 115, 232, 0.05);
42
+ }
43
+
44
+ /* Header sous la bannière */
45
+ .header-section {
46
+ background: linear-gradient(135deg, #1a73e8 0%, #004080 100%);
47
+ color: white;
48
+ text-align: center;
49
+ padding: 25px 20px;
50
+ margin-bottom: 30px;
51
+ border-radius: 0 0 10px 10px;
52
+ }
53
+
54
+ .main-header {
55
+ font-size: 32px;
56
+ font-weight: 700;
57
+ color: white;
58
+ margin-bottom: 8px;
59
+ letter-spacing: -0.5px;
60
+ }
61
+
62
+ .main-subtitle {
63
+ color: rgba(255, 255, 255, 0.9);
64
+ font-size: 18px;
65
+ margin: 0;
66
+ font-weight: 400;
67
+ }
68
+
69
+ /* Expander */
70
+ .expander-header {
71
+ background-color: #e8f0fe;
72
+ border: 1px solid #c5dafc;
73
+ border-radius: 8px;
74
+ padding: 18px 25px;
75
+ cursor: pointer;
76
+ display: flex;
77
+ justify-content: space-between;
78
+ align-items: center;
79
+ margin-bottom: 20px;
80
+ font-weight: 500;
81
+ color: #1a73e8;
82
+ transition: background-color 0.3s ease, box-shadow 0.2s ease;
83
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
84
+ }
85
+
86
+ .expander-header:hover {
87
+ background-color: #d2e3fc;
88
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
89
+ }
90
+
91
+ .expander-header::after {
92
+ content: '▼';
93
+ margin-left: 15px;
94
+ transition: transform 0.3s ease;
95
+ font-size: 0.9em;
96
+ }
97
+
98
+ .expander-header.expanded::after {
99
+ content: '▲';
100
+ transform: rotate(180deg);
101
+ }
102
+
103
+ .expander-content {
104
+ background-color: #f0f6ff;
105
+ border: 1px solid #d2e3fc;
106
+ border-top: none;
107
+ border-radius: 0 0 8px 8px;
108
+ padding: 25px;
109
+ margin-top: -15px;
110
+ display: none;
111
+ overflow: hidden;
112
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.03);
113
+ }
114
+
115
+ .expander-content ol {
116
+ padding-left: 25px;
117
+ margin-top: 15px;
118
+ }
119
+
120
+ .expander-content li {
121
+ margin-bottom: 10px;
122
+ }
123
+
124
+ .expander-content strong {
125
+ color: #004080;
126
+ }
127
+
128
+ /* Form Elements */
129
+ .form-group {
130
+ margin-bottom: 25px;
131
+ padding: 15px;
132
+ background-color: #f7f9fc;
133
+ border-radius: 8px;
134
+ border: 1px solid #e0e0e0;
135
+ }
136
+
137
+ .form-group label {
138
+ display: block;
139
+ margin-bottom: 10px;
140
+ font-weight: 500;
141
+ color: #5f6368;
142
+ font-size: 1.1em;
143
+ }
144
+
145
+ .form-input {
146
+ width: calc(100% - 24px);
147
+ padding: 12px;
148
+ border: 1px solid #dadce0;
149
+ border-radius: 8px;
150
+ font-size: 16px;
151
+ box-sizing: border-box;
152
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
153
+ }
154
+
155
+ .form-input:focus {
156
+ border-color: #1a73e8;
157
+ box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
158
+ outline: none;
159
+ }
160
+
161
+ /* File Drop Zone */
162
+ .file-drop-zone {
163
+ border: 2px dashed #dadce0;
164
+ border-radius: 12px;
165
+ padding: 32px;
166
+ text-align: center;
167
+ transition: all 0.3s ease;
168
+ cursor: pointer;
169
+ background: linear-gradient(135deg, #fafbfc 0%, #f8fafd 100%);
170
+ }
171
+
172
+ .file-drop-zone:hover {
173
+ border-color: #1a73e8;
174
+ background: linear-gradient(135deg, #e8f0fe 0%, #f0f6ff 100%);
175
+ transform: translateY(-2px);
176
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
177
+ }
178
+
179
+ .file-drop-icon {
180
+ font-size: 48px;
181
+ color: #1a73e8;
182
+ opacity: 0.7;
183
+ margin-bottom: 16px;
184
+ }
185
+
186
+ /* Buttons */
187
+ .btn {
188
+ display: inline-flex;
189
+ align-items: center;
190
+ justify-content: center;
191
+ gap: 8px;
192
+ padding: 12px 24px;
193
+ border: none;
194
+ border-radius: 8px;
195
+ font-size: 14px;
196
+ font-weight: 500;
197
+ cursor: pointer;
198
+ transition: all 0.3s ease;
199
+ text-transform: uppercase;
200
+ letter-spacing: 0.5px;
201
+ min-height: 44px;
202
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
203
+ }
204
+
205
+ .btn:hover {
206
+ transform: translateY(-2px);
207
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
208
+ }
209
+
210
+ .btn:disabled {
211
+ opacity: 0.6;
212
+ cursor: not-allowed;
213
+ transform: none !important;
214
+ }
215
+
216
+ .btn-primary {
217
+ background: linear-gradient(135deg, #004080 0%, #1a73e8 100%);
218
+ color: white;
219
+ }
220
+
221
+ .btn-success {
222
+ background: linear-gradient(135deg, #34a853 0%, #2e8b4e 100%);
223
+ color: white;
224
+ }
225
+
226
+ .btn-danger {
227
+ background: linear-gradient(135deg, #ea4335 0%, #d93025 100%);
228
+ color: white;
229
+ }
230
+
231
+ .btn-warning {
232
+ background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
233
+ color: white;
234
+ }
235
+
236
+ .btn-small {
237
+ padding: 8px 16px;
238
+ font-size: 12px;
239
+ min-height: 36px;
240
+ }
241
+
242
+ /* Messages */
243
+ .message {
244
+ padding: 16px 20px;
245
+ border-radius: 8px;
246
+ margin-bottom: 20px;
247
+ display: none;
248
+ align-items: center;
249
+ gap: 12px;
250
+ font-weight: 500;
251
+ }
252
+
253
+ .message.show {
254
+ display: flex;
255
+ }
256
+
257
+ .message-success {
258
+ background: #e6f4ea;
259
+ color: #1e8e3e;
260
+ border: 1px solid #c8e6c9;
261
+ }
262
+
263
+ .message-error {
264
+ background: #fce8e6;
265
+ color: #c5221f;
266
+ border: 1px solid #f9bdbb;
267
+ }
268
+
269
+ /* Data Table */
270
+ .data-table-container {
271
+ background: #ffffff;
272
+ border-radius: 12px;
273
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
274
+ overflow: hidden;
275
+ margin-top: 35px;
276
+ display: none;
277
+ }
278
+
279
+ .data-table-header {
280
+ background: linear-gradient(135deg, #004080 0%, #1a73e8 100%);
281
+ color: white;
282
+ padding: 20px 24px;
283
+ display: flex;
284
+ justify-content: space-between;
285
+ align-items: center;
286
+ flex-wrap: wrap;
287
+ gap: 12px;
288
+ }
289
+
290
+ .data-table-title {
291
+ font-size: 20px;
292
+ font-weight: 600;
293
+ }
294
+
295
+ .data-table {
296
+ width: 100%;
297
+ border-collapse: collapse;
298
+ }
299
+
300
+ .data-table th,
301
+ .data-table td {
302
+ border: 1px solid #e8eaed;
303
+ padding: 15px;
304
+ text-align: left;
305
+ vertical-align: top;
306
+ font-size: 15px;
307
+ }
308
+
309
+ .data-table th {
310
+ background-color: #f0f4f7;
311
+ color: #3c4043;
312
+ font-weight: 500;
313
+ }
314
+
315
+ .data-table tbody tr:nth-child(even) {
316
+ background-color: #fcfdff;
317
+ }
318
+
319
+ .data-table tbody tr:hover {
320
+ background-color: #f5f8fc;
321
+ }
322
+
323
+ .requirement-link {
324
+ color: #1a73e8;
325
+ text-decoration: none;
326
+ font-weight: 500;
327
+ transition: color 0.2s ease;
328
+ }
329
+
330
+ .requirement-link:hover {
331
+ color: #004080;
332
+ text-decoration: underline;
333
+ }
334
+
335
+ /* Bulk Generation */
336
+ .bulk-generation-container {
337
+ margin-top: 16px;
338
+ padding: 16px;
339
+ background: linear-gradient(135deg, #e8f0fe 0%, #f0f6ff 100%);
340
+ border: 1px solid #1a73e8;
341
+ border-radius: 8px;
342
+ text-align: center;
343
+ }
344
+
345
+ .generation-options {
346
+ display: flex;
347
+ gap: 12px;
348
+ align-items: center;
349
+ justify-content: center;
350
+ margin-top: 12px;
351
+ flex-wrap: wrap;
352
+ }
353
+
354
+ .option-select {
355
+ padding: 4px 8px;
356
+ border: 1px solid #dadce0;
357
+ border-radius: 4px;
358
+ font-size: 11px;
359
+ background: #ffffff;
360
+ }
361
+
362
+ .bulk-progress {
363
+ display: none;
364
+ margin-top: 16px;
365
+ padding: 16px;
366
+ background: #ffffff;
367
+ border-radius: 8px;
368
+ border: 1px solid #e8eaed;
369
+ }
370
+
371
+ .bulk-progress.show {
372
+ display: block;
373
+ }
374
+
375
+ .progress-header {
376
+ display: flex;
377
+ justify-content: space-between;
378
+ align-items: center;
379
+ margin-bottom: 12px;
380
+ }
381
+
382
+ .progress-counter {
383
+ background: #1a73e8;
384
+ color: white;
385
+ padding: 4px 12px;
386
+ border-radius: 12px;
387
+ font-size: 12px;
388
+ font-weight: 500;
389
+ }
390
+
391
+ .progress-bar-container {
392
+ background: #e0e0e0;
393
+ border-radius: 8px;
394
+ height: 8px;
395
+ overflow: hidden;
396
+ margin-bottom: 12px;
397
+ }
398
+
399
+ .progress-bar {
400
+ background: linear-gradient(90deg, #004080 0%, #1a73e8 100%);
401
+ height: 100%;
402
+ width: 0%;
403
+ transition: width 0.3s ease;
404
+ }
405
+
406
+ .current-item {
407
+ font-size: 13px;
408
+ color: #5f6368;
409
+ display: flex;
410
+ align-items: center;
411
+ gap: 8px;
412
+ }
413
+
414
+ .loading-spinner {
415
+ display: inline-block;
416
+ width: 16px;
417
+ height: 16px;
418
+ border: 2px solid rgba(26, 115, 232, 0.3);
419
+ border-radius: 50%;
420
+ border-top-color: #1a73e8;
421
+ animation: spin 1s ease-in-out infinite;
422
+ }
423
+
424
+ @keyframes spin {
425
+ to { transform: rotate(360deg); }
426
+ }
427
+
428
+ /* Modal */
429
+ .modal {
430
+ display: none;
431
+ position: fixed;
432
+ top: 0;
433
+ left: 0;
434
+ width: 100%;
435
+ height: 100%;
436
+ background: rgba(0, 0, 0, 0.6);
437
+ z-index: 1000;
438
+ justify-content: center;
439
+ align-items: center;
440
+ padding: 20px;
441
+ backdrop-filter: blur(4px);
442
+ }
443
+
444
+ .modal-content {
445
+ background: #ffffff;
446
+ border-radius: 12px;
447
+ max-width: 1000px;
448
+ width: 100%;
449
+ max-height: 100vh;
450
+ overflow: hidden;
451
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
452
+ }
453
+
454
+ .modal-header {
455
+ background: linear-gradient(135deg, #004080 0%, #1a73e8 100%);
456
+ color: white;
457
+ padding: 24px;
458
+ display: flex;
459
+ justify-content: space-between;
460
+ align-items: center;
461
+ }
462
+
463
+ .modal-close {
464
+ background: none;
465
+ border: none;
466
+ color: white;
467
+ font-size: 24px;
468
+ cursor: pointer;
469
+ padding: 4px;
470
+ border-radius: 50%;
471
+ transition: background-color 0.2s ease;
472
+ }
473
+
474
+ .modal-close:hover {
475
+ background: rgba(255, 255, 255, 0.1);
476
+ }
477
+
478
+ .modal-body {
479
+ padding: 24px;
480
+ max-height: 60vh;
481
+ overflow-y: auto;
482
+ }
483
+
484
+ .modal-field {
485
+ margin-bottom: 20px;
486
+ }
487
+
488
+ .modal-field-label {
489
+ font-weight: 600;
490
+ color: #3c4043;
491
+ margin-bottom: 8px;
492
+ display: block;
493
+ }
494
+
495
+ .modal-textarea {
496
+ width: 100%;
497
+ min-height: 120px;
498
+ padding: 16px;
499
+ border: 2px solid #dadce0;
500
+ border-radius: 8px;
501
+ font-family: inherit;
502
+ font-size: 14px;
503
+ resize: vertical;
504
+ display: none;
505
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
506
+ }
507
+
508
+ .modal-textarea:focus {
509
+ border-color: #1a73e8;
510
+ box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1);
511
+ outline: none;
512
+ }
513
+
514
+ .textarea-toggle {
515
+ background: #ffffff;
516
+ border: 1px solid #dadce0;
517
+ border-radius: 6px;
518
+ padding: 8px 12px;
519
+ cursor: pointer;
520
+ color: #1a73e8;
521
+ font-size: 13px;
522
+ font-weight: 500;
523
+ display: inline-flex;
524
+ align-items: center;
525
+ gap: 6px;
526
+ transition: all 0.2s ease;
527
+ }
528
+
529
+ .textarea-toggle:hover {
530
+ background: #f8fafd;
531
+ border-color: #1a73e8;
532
+ }
533
+
534
+ .modal-actions {
535
+ padding: 24px;
536
+ border-top: 1px solid #e8eaed;
537
+ display: flex;
538
+ justify-content: flex-end;
539
+ gap: 12px;
540
+ }
541
+
542
+ .recommendation-content {
543
+ background: #ffffff;
544
+ border: 1px solid #e0e0e0;
545
+ padding: 15px;
546
+ border-radius: 8px;
547
+ min-height: 80px;
548
+ overflow-y: auto;
549
+ max-height: 250px;
550
+ margin-bottom: 10px;
551
+ color: #333;
552
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
553
+ }
554
+
555
+ /* Dynamic RCA Modal */
556
+ .rca-modal {
557
+ display: none;
558
+ position: fixed;
559
+ top: 0;
560
+ left: 0;
561
+ width: 100%;
562
+ height: 100%;
563
+ background: rgba(0, 0, 0, 0.6);
564
+ z-index: 1001;
565
+ justify-content: center;
566
+ align-items: center;
567
+ padding: 20px;
568
+ backdrop-filter: blur(4px);
569
+ }
570
+
571
+ .rca-modal-content {
572
+ background: #ffffff;
573
+ border-radius: 12px;
574
+ max-width: 900px;
575
+ width: 100%;
576
+ max-height: 90vh;
577
+ overflow: hidden;
578
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
579
+ }
580
+
581
+ .rca-modal-header {
582
+ background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
583
+ color: white;
584
+ padding: 24px;
585
+ display: flex;
586
+ justify-content: space-between;
587
+ align-items: center;
588
+ }
589
+
590
+ .rca-modal-body {
591
+ padding: 24px;
592
+ max-height: 60vh;
593
+ overflow-y: auto;
594
+ }
595
+
596
+ .rca-loading {
597
+ text-align: center;
598
+ padding: 40px 20px;
599
+ color: #5f6368;
600
+ }
601
+
602
+ .rca-loading .loading-spinner {
603
+ width: 32px;
604
+ height: 32px;
605
+ margin-bottom: 16px;
606
+ border-width: 3px;
607
+ }
608
+
609
+ .question-container {
610
+ margin-bottom: 25px;
611
+ padding: 20px;
612
+ background: #f8f9fa;
613
+ border-radius: 8px;
614
+ border-left: 4px solid #ff9800;
615
+ }
616
+
617
+ .question-text {
618
+ font-weight: 600;
619
+ color: #333;
620
+ margin-bottom: 15px;
621
+ font-size: 16px;
622
+ line-height: 1.4;
623
+ }
624
+
625
+ .question-description {
626
+ color: #666;
627
+ font-size: 14px;
628
+ margin-bottom: 15px;
629
+ font-style: italic;
630
+ }
631
+
632
+ .answer-textarea {
633
+ width: 100%;
634
+ min-height: 120px;
635
+ padding: 15px;
636
+ border: 2px solid #e0e0e0;
637
+ border-radius: 8px;
638
+ font-family: inherit;
639
+ font-size: 14px;
640
+ resize: vertical;
641
+ transition: border-color 0.3s ease;
642
+ line-height: 1.5;
643
+ }
644
+
645
+ .answer-textarea:focus {
646
+ border-color: #ff9800;
647
+ outline: none;
648
+ box-shadow: 0 0 0 3px rgba(255, 152, 0, 0.1);
649
+ }
650
+
651
+ .answer-textarea::placeholder {
652
+ color: #999;
653
+ font-style: italic;
654
+ }
655
+
656
+ .question-navigation {
657
+ display: flex;
658
+ justify-content: space-between;
659
+ align-items: center;
660
+ margin-top: 20px;
661
+ padding-top: 20px;
662
+ border-top: 1px solid #e0e0e0;
663
+ }
664
+
665
+ .analysis-summary {
666
+ background: #e8f5e8;
667
+ border: 1px solid #c8e6c9;
668
+ border-radius: 8px;
669
+ padding: 20px;
670
+ margin-top: 20px;
671
+ }
672
+
673
+ .analysis-summary h3 {
674
+ color: #2e7d32;
675
+ margin-top: 0;
676
+ display: flex;
677
+ align-items: center;
678
+ gap: 8px;
679
+ }
680
+
681
+ .progress-indicator {
682
+ font-size: 14px;
683
+ color: #666;
684
+ background: #f0f0f0;
685
+ padding: 8px 16px;
686
+ border-radius: 20px;
687
+ }
688
+
689
+ /* Responsive */
690
+ @media (max-width: 768px) {
691
+ .container {
692
+ margin: 15px;
693
+ padding: 20px;
694
+ }
695
+
696
+ .banner {
697
+ height: 80px;
698
+ }
699
+
700
+ .main-header {
701
+ font-size: 24px;
702
+ }
703
+
704
+ .main-subtitle {
705
+ font-size: 16px;
706
+ }
707
+
708
+ .data-table th:nth-child(2),
709
+ .data-table td:nth-child(2) {
710
+ display: none;
711
+ }
712
+
713
+ .modal-content, .rca-modal-content {
714
+ width: 95%;
715
+ margin: 10px;
716
+ }
717
+
718
+ .modal-body, .rca-modal-body {
719
+ padding: 16px;
720
+ }
721
+
722
+ .modal-actions, .question-navigation {
723
+ padding: 16px;
724
+ flex-direction: column;
725
+ gap: 12px;
726
+ }
727
+
728
+ .answer-textarea {
729
+ min-height: 100px;
730
+ }
731
+ }
732
+ </style>
733
+ </head>
734
+ <body>
735
+ <div class="banner"></div>
736
+
737
+ <div class="container">
738
+ <div class="header-section">
739
+ <h1 class="main-header">Assistant VisiPilot pour Plan d'Actions IFS</h1>
740
+ <p class="main-subtitle">Génération intelligente de plans d'actions IFS Food 8 avec analyse de cause dynamique</p>
741
+ </div>
742
+
743
+ <div class="expander">
744
+ <div class="expander-header" id="howToUseHeader">
745
+ <span>Comment utiliser cette application</span>
746
+ </div>
747
+ <div class="expander-content" id="howToUseContent">
748
+ <p><strong>Étapes d'utilisation:</strong></p>
749
+ <ol>
750
+ <li><strong>Saisissez votre clé API Groq:</strong> Entrez votre clé API dans le champ dédié.</li>
751
+ <li><strong>Téléchargez votre plan d'actions IFS v8:</strong> Glissez-déposez votre fichier Excel.</li>
752
+ <li><strong>Deux modes de génération:</strong>
753
+ <ul>
754
+ <li><strong>Génération automatique en lot:</strong> Bouton vert pour générer toutes les recommandations automatiquement</li>
755
+ <li><strong>Analyse de cause dynamique:</strong> Boutons orange individuels pour une analyse guidée avec questions spécifiques générées par IA</li>
756
+ </ul>
757
+ </li>
758
+ <li><strong>Analyse de cause intelligente:</strong> L'IA génère 2-3 questions spécifiques à votre non-conformité pour une analyse approfondie.</li>
759
+ <li><strong>Affinez les propositions:</strong> Cliquez sur le numéro d'exigence pour éditer.</li>
760
+ <li><strong>Exportez en PDF:</strong> Générez un rapport complet.</li>
761
+ </ol>
762
+ </div>
763
+ </div>
764
+
765
+ <div class="form-group">
766
+ <label for="groqApiKey">Votre clé API Groq :</label>
767
+ <input type="password" id="groqApiKey" class="form-input" placeholder="Entrez votre clé API Groq">
768
+ </div>
769
+
770
+ <div class="form-group">
771
+ <label>Téléchargez votre plan d'action (fichier Excel) :</label>
772
+ <div class="file-drop-zone" id="fileDropZone">
773
+ <input type="file" id="fileInput" accept=".xlsx" style="display: none;">
774
+ <div class="file-drop-content">
775
+ <span class="material-icons file-drop-icon">upload_file</span>
776
+ <div>Glissez-déposez votre fichier Excel ici</div>
777
+ <div>ou cliquez pour sélectionner (.xlsx)</div>
778
+ </div>
779
+ </div>
780
+ </div>
781
+
782
+ <div class="message message-success" id="successMessage">
783
+ <span class="material-icons">check_circle</span>
784
+ <span id="successText"></span>
785
+ </div>
786
+
787
+ <div class="message message-error" id="errorMessage">
788
+ <span class="material-icons">error</span>
789
+ <span id="errorText"></span>
790
+ </div>
791
+
792
+ <div class="data-table-container" id="dataTableContainer">
793
+ <div class="data-table-header">
794
+ <h2 class="data-table-title">Plan d'Action IFS</h2>
795
+ <div style="display: flex; gap: 12px; align-items: center;">
796
+ <button class="btn btn-success" id="bulkGenerateBtn">
797
+ <span class="material-icons">auto_awesome</span>
798
+ Génération automatique
799
+ </button>
800
+ <button class="btn btn-danger" id="exportPdfBtn">
801
+ <span class="material-icons">picture_as_pdf</span>
802
+ Exporter PDF
803
+ </button>
804
+ </div>
805
+ </div>
806
+
807
+ <div class="bulk-generation-container">
808
+ <div style="margin-bottom: 12px;">
809
+ <strong>Génération automatique :</strong> Génère automatiquement toutes les recommandations manquantes sans analyse de cause.
810
+ </div>
811
+
812
+ <div class="generation-options">
813
+ <div style="display: flex; align-items: center; gap: 6px;">
814
+ <label for="delaySelect">Délai :</label>
815
+ <select class="option-select" id="delaySelect">
816
+ <option value="5">5 secondes</option>
817
+ <option value="10" selected>10 secondes</option>
818
+ <option value="15">15 secondes</option>
819
+ </select>
820
+ </div>
821
+ </div>
822
+
823
+ <div class="bulk-progress" id="bulkProgress">
824
+ <div class="progress-header">
825
+ <div>Génération en cours...</div>
826
+ <div class="progress-counter" id="progressCounter">0 / 0</div>
827
+ </div>
828
+ <div class="progress-bar-container">
829
+ <div class="progress-bar" id="progressBar"></div>
830
+ </div>
831
+ <div class="current-item" id="currentItem">En attente...</div>
832
+ </div>
833
+ </div>
834
+
835
+ <table class="data-table" id="dataTable">
836
+ <thead>
837
+ <tr>
838
+ <th>Numéro d'exigence</th>
839
+ <th>Exigence IFS Food 8</th>
840
+ <th>Constat détaillé</th>
841
+ <th>Actions</th>
842
+ </tr>
843
+ </thead>
844
+ <tbody id="dataTableBody">
845
+ </tbody>
846
+ </table>
847
+ </div>
848
+ </div>
849
+
850
+ <!-- Modal de détail -->
851
+ <div class="modal" id="detailModal">
852
+ <div class="modal-content">
853
+ <div class="modal-header">
854
+ <h2>Détail de la non-conformité</h2>
855
+ <button class="modal-close" id="modalClose">
856
+ <span class="material-icons">close</span>
857
+ </button>
858
+ </div>
859
+ <div class="modal-body">
860
+ <div class="modal-field">
861
+ <label class="modal-field-label">Numéro d'exigence:</label>
862
+ <div id="modalReqNum"></div>
863
+ </div>
864
+ <div class="modal-field">
865
+ <label class="modal-field-label">Exigence IFS Food 8:</label>
866
+ <div id="modalReqText"></div>
867
+ </div>
868
+ <div class="modal-field">
869
+ <label class="modal-field-label">Constat détaillé:</label>
870
+ <div id="modalExplanation"></div>
871
+ </div>
872
+ <div class="modal-field">
873
+ <label class="modal-field-label">Recommandation IA:</label>
874
+ <div class="recommendation-content" id="modalRenderedRecommendation"></div>
875
+ </div>
876
+ <div class="modal-field">
877
+ <label class="modal-field-label">Modifier la recommandation:</label>
878
+ <button class="textarea-toggle" id="textareaToggle">
879
+ <span class="material-icons">edit</span>
880
+ Modifier
881
+ </button>
882
+ <textarea class="modal-textarea" id="modalTextarea"></textarea>
883
+ </div>
884
+ </div>
885
+ <div class="modal-actions">
886
+ <button class="btn btn-primary" id="modalCancel">Fermer</button>
887
+ <button class="btn btn-success" id="modalGenerate">
888
+ <span class="material-icons">auto_awesome</span>
889
+ Générer/Améliorer
890
+ </button>
891
+ </div>
892
+ </div>
893
+ </div>
894
+
895
+ <!-- Modal d'analyse de cause dynamique -->
896
+ <div class="rca-modal" id="rcaModal">
897
+ <div class="rca-modal-content">
898
+ <div class="rca-modal-header">
899
+ <h2>Analyse de Cause Spécifique</h2>
900
+ <button class="modal-close" id="rcaModalClose">
901
+ <span class="material-icons">close</span>
902
+ </button>
903
+ </div>
904
+ <div class="rca-modal-body">
905
+ <div id="rcaContent">
906
+ <!-- Le contenu sera généré dynamiquement -->
907
+ </div>
908
+ <div class="question-navigation">
909
+ <button class="btn btn-primary" id="rcaPrevBtn" style="display: none;">
910
+ <span class="material-icons">arrow_back</span>
911
+ Précédent
912
+ </button>
913
+ <div class="progress-indicator" id="rcaProgress"></div>
914
+ <button class="btn btn-warning" id="rcaNextBtn">
915
+ <span class="material-icons">arrow_forward</span>
916
+ Suivant
917
+ </button>
918
+ </div>
919
+ </div>
920
+ </div>
921
+ </div>
922
+
923
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js"></script>
924
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
925
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.3/html2pdf.bundle.min.js"></script>
926
+
927
+ <script>
928
+ // Configuration
929
+ var CONFIG = {
930
+ GROQ_API_ENDPOINT: "https://api.groq.com/openai/v1/chat/completions",
931
+ // Modèle Groq remplacé par un modèle public et fonctionnel.
932
+ // Utilisez "llama3-8b-8192", "mixtral-8x7b-32768" ou "gemma-7b-it" selon vos besoins.
933
+ GROQ_MODEL: "openai/gpt-oss-20b",
934
+ GUIDE_CSV_URL: "https://raw.githubusercontent.com/M00N69/Action-planGroq/main/Guide%20Checklist_IFS%20Food%20V%208%20-%20CHECKLIST.csv",
935
+ EXPECTED_HEADERS: ["requirementNo", "requirementText", "requirementExplanation"],
936
+ HEADER_ROW_INDEX: 11,
937
+ DATA_START_INDEX: 13
938
+ };
939
+
940
+ // État global
941
+ var AppState = {
942
+ apiKey: '',
943
+ actionPlanData: [],
944
+ guideData: [],
945
+ recommendations: {},
946
+ currentModalIndex: -1,
947
+ isLoading: false,
948
+ bulkGeneration: {
949
+ isRunning: false,
950
+ currentIndex: 0,
951
+ total: 0,
952
+ completed: 0,
953
+ failed: []
954
+ },
955
+ rcaState: {
956
+ currentIndex: -1,
957
+ currentQuestionIndex: 0,
958
+ answers: {},
959
+ questions: [],
960
+ isActive: false,
961
+ isGeneratingQuestions: false
962
+ }
963
+ };
964
+
965
+ // Éléments DOM
966
+ var DOM = {
967
+ apiKeyInput: document.getElementById('groqApiKey'),
968
+ fileDropZone: document.getElementById('fileDropZone'),
969
+ fileInput: document.getElementById('fileInput'),
970
+ successMessage: document.getElementById('successMessage'),
971
+ errorMessage: document.getElementById('errorMessage'),
972
+ successText: document.getElementById('successText'),
973
+ errorText: document.getElementById('errorText'),
974
+ dataTableContainer: document.getElementById('dataTableContainer'),
975
+ dataTableBody: document.getElementById('dataTableBody'),
976
+ bulkGenerateBtn: document.getElementById('bulkGenerateBtn'),
977
+ bulkProgress: document.getElementById('bulkProgress'),
978
+ progressBar: document.getElementById('progressBar'),
979
+ progressCounter: document.getElementById('progressCounter'),
980
+ currentItem: document.getElementById('currentItem'),
981
+ delaySelect: document.getElementById('delaySelect'),
982
+ exportPdfBtn: document.getElementById('exportPdfBtn'),
983
+ detailModal: document.getElementById('detailModal'),
984
+ modalClose: document.getElementById('modalClose'),
985
+ modalCancel: document.getElementById('modalCancel'),
986
+ modalGenerate: document.getElementById('modalGenerate'),
987
+ modalReqNum: document.getElementById('modalReqNum'),
988
+ modalReqText: document.getElementById('modalReqText'),
989
+ modalExplanation: document.getElementById('modalExplanation'),
990
+ modalRenderedRecommendation: document.getElementById('modalRenderedRecommendation'),
991
+ modalTextarea: document.getElementById('modalTextarea'),
992
+ textareaToggle: document.getElementById('textareaToggle'),
993
+ howToUseHeader: document.getElementById('howToUseHeader'),
994
+ howToUseContent: document.getElementById('howToUseContent'),
995
+ // RCA Modal
996
+ rcaModal: document.getElementById('rcaModal'),
997
+ rcaModalClose: document.getElementById('rcaModalClose'),
998
+ rcaContent: document.getElementById('rcaContent'),
999
+ rcaPrevBtn: document.getElementById('rcaPrevBtn'),
1000
+ rcaNextBtn: document.getElementById('rcaNextBtn'),
1001
+ rcaProgress: document.getElementById('rcaProgress')
1002
+ };
1003
+
1004
+ // Utilitaires
1005
+ var Utils = {
1006
+ showMessage: function(type, text) {
1007
+ var messageEl = DOM[type + 'Message'];
1008
+ var textEl = DOM[type + 'Text'];
1009
+
1010
+ textEl.textContent = text;
1011
+ messageEl.classList.add('show');
1012
+
1013
+ setTimeout(function() {
1014
+ messageEl.classList.remove('show');
1015
+ }, 5000);
1016
+ },
1017
+
1018
+ parseCSV: function(csvText) {
1019
+ var lines = csvText.split('\n');
1020
+ var headers = lines[0].split(';').map(function(h) {
1021
+ return h.trim();
1022
+ });
1023
+ var data = [];
1024
+
1025
+ for (var i = 1; i < lines.length; i++) {
1026
+ var currentLine = lines[i].split(';');
1027
+ if (currentLine.length === headers.length) {
1028
+ var row = {};
1029
+ headers.forEach(function(header, index) {
1030
+ row[header] = currentLine[index] ? currentLine[index].trim() : '';
1031
+ });
1032
+ data.push(row);
1033
+ }
1034
+ }
1035
+ return data;
1036
+ }
1037
+ };
1038
+
1039
+ // Gestionnaire API
1040
+ var APIManager = {
1041
+ loadGuideData: function() {
1042
+ fetch(CONFIG.GUIDE_CSV_URL)
1043
+ .then(function(response) {
1044
+ if (!response.ok) throw new Error('HTTP ' + response.status);
1045
+ return response.text();
1046
+ })
1047
+ .then(function(csvText) {
1048
+ AppState.guideData = Utils.parseCSV(csvText);
1049
+ console.log('Guide IFS chargé:', AppState.guideData.length, 'entrées');
1050
+ })
1051
+ .catch(function(error) {
1052
+ console.error('Erreur chargement guide:', error);
1053
+ Utils.showMessage('error', 'Impossible de charger le guide IFS');
1054
+ });
1055
+ },
1056
+
1057
+ getGuideInfo: function(reqNumber) {
1058
+ if (!AppState.guideData.length || !reqNumber) return null;
1059
+ return AppState.guideData.find(function(row) {
1060
+ // Cherche une correspondance partielle ou exacte pour le numéro de l'exigence
1061
+ return row['NUM_REQ'] && String(row['NUM_REQ']).includes(String(reqNumber));
1062
+ });
1063
+ },
1064
+
1065
+ generateRCAQuestions: function(nonConformity) {
1066
+ return new Promise(function(resolve, reject) {
1067
+ if (!AppState.apiKey) {
1068
+ reject(new Error('Pas de clé API'));
1069
+ return;
1070
+ }
1071
+
1072
+ var guideInfo = APIManager.getGuideInfo(nonConformity["Numéro d'exigence"]);
1073
+
1074
+ var prompt = "En tant qu'expert en sécurité alimentaire et IFS Food 8, générez EXACTEMENT 3 questions spécifiques pour analyser la cause racine de cette non-conformité :\n\n" +
1075
+ "Numéro d'exigence: " + nonConformity["Numéro d'exigence"] + "\n" +
1076
+ "Description: " + nonConformity["Exigence IFS Food 8"] + "\n" +
1077
+ "Constat détaillé: " + nonConformity["Explication (par l'auditeur/l'évaluateur)"] + "\n\n";
1078
+
1079
+ if (guideInfo && guideInfo['Good practice']) {
1080
+ prompt += "Référence Guide IFS v8:\n" +
1081
+ "Bonnes pratiques: " + (guideInfo['Good practice'] || 'Non disponible') + "\n" +
1082
+ "Éléments à vérifier: " + (guideInfo['Elements to check'] || 'Non disponible') + "\n\n";
1083
+ }
1084
+
1085
+ prompt += "Générez EXACTEMENT 3 questions ouvertes spécifiques qui permettront d'identifier la cause racine. Chaque question doit :\n" +
1086
+ "- Être adaptée au contexte spécifique de cette non-conformité\n" +
1087
+ "- Permettre une réponse détaillée et élaborée\n" +
1088
+ "- Aider à identifier les causes profondes (pas seulement les symptômes)\n" +
1089
+ "- Couvrir différents aspects : technique, organisationnel, humain\n\n" +
1090
+ "Format de réponse requis (JSON strict) :\n" +
1091
+ "{\n" +
1092
+ ' "questions": [\n' +
1093
+ ' {\n' +
1094
+ ' "id": "q1",\n' +
1095
+ ' "text": "Question principale sur le processus ou la procédure",\n' +
1096
+ ' "description": "Explication de pourquoi cette question est importante pour cette non-conformité"\n' +
1097
+ ' },\n' +
1098
+ ' {\n' +
1099
+ ' "id": "q2",\n' +
1100
+ ' "text": "Question sur les facteurs humains ou organisationnels",\n' +
1101
+ ' "description": "Explication de l\'aspect analysé"\n' +
1102
+ ' },\n' +
1103
+ ' {\n' +
1104
+ ' "id": "q3",\n' +
1105
+ ' "text": "Question sur l\'environnement ou les conditions techniques",\n' +
1106
+ ' "description": "Explication de la dimension technique analysée"\n' +
1107
+ ' }\n' +
1108
+ ' ]\n' +
1109
+ '}\n\n' +
1110
+ "Répondez UNIQUEMENT avec le JSON, sans texte additionnel.";
1111
+
1112
+ var messages = [
1113
+ {
1114
+ role: "system",
1115
+ content: "Vous êtes un expert en analyse de causes racines dans le domaine de la sécurité alimentaire. Générez des questions d'analyse très spécifiques à chaque situation."
1116
+ },
1117
+ {
1118
+ role: "user",
1119
+ content: prompt
1120
+ }
1121
+ ];
1122
+
1123
+ fetch(CONFIG.GROQ_API_ENDPOINT, {
1124
+ method: 'POST',
1125
+ headers: {
1126
+ 'Content-Type': 'application/json',
1127
+ 'Authorization': 'Bearer ' + AppState.apiKey
1128
+ },
1129
+ body: JSON.stringify({
1130
+ messages: messages,
1131
+ model: CONFIG.GROQ_MODEL,
1132
+ temperature: 0.3,
1133
+ max_tokens: 1000
1134
+ })
1135
+ })
1136
+ .then(function(response) {
1137
+ if (!response.ok) {
1138
+ throw new Error('API Error ' + response.status + ': ' + response.statusText);
1139
+ }
1140
+ return response.json();
1141
+ })
1142
+ .then(function(data) {
1143
+ var content = data.choices[0].message.content.trim();
1144
+
1145
+ // Nettoyer le contenu pour extraire le JSON
1146
+ var jsonStart = content.indexOf('{');
1147
+ var jsonEnd = content.lastIndexOf('}') + 1;
1148
+
1149
+ if (jsonStart !== -1 && jsonEnd > jsonStart) {
1150
+ var jsonContent = content.substring(jsonStart, jsonEnd);
1151
+ try {
1152
+ var questions = JSON.parse(jsonContent);
1153
+ if (questions.questions && Array.isArray(questions.questions) && questions.questions.length === 3) {
1154
+ resolve(questions.questions);
1155
+ } else {
1156
+ throw new Error('Format de questions invalide (attendu 3 questions dans un tableau "questions")');
1157
+ }
1158
+ } catch (parseError) {
1159
+ console.error('Erreur parsing JSON:', parseError, 'Contenu brut:', jsonContent);
1160
+ reject(new Error('Impossible de parser les questions générées. Vérifiez le format JSON.'));
1161
+ }
1162
+ } else {
1163
+ reject(new Error('Réponse JSON non trouvée ou mal formée par l\'IA. Contenu brut: ' + content));
1164
+ }
1165
+ })
1166
+ .catch(function(error) {
1167
+ console.error('Erreur génération questions RCA:', error);
1168
+ reject(error);
1169
+ });
1170
+ });
1171
+ },
1172
+
1173
+ generateRecommendation: function(index, isRegeneration, rcaData) {
1174
+ return new Promise(function(resolve, reject) {
1175
+ if (!AppState.apiKey) {
1176
+ Utils.showMessage('error', 'Veuillez configurer votre clé API Groq');
1177
+ reject(new Error('Pas de clé API'));
1178
+ return;
1179
+ }
1180
+
1181
+ var nonConformity = AppState.actionPlanData[index];
1182
+ if (!nonConformity) {
1183
+ Utils.showMessage('error', 'Non-conformité introuvable');
1184
+ reject(new Error('Non-conformité introuvable'));
1185
+ return;
1186
+ }
1187
+
1188
+ var guideInfo = APIManager.getGuideInfo(nonConformity["Numéro d'exigence"]);
1189
+
1190
+ var prompt = "En tant qu'expert en sécurité alimentaire et IFS Food 8, analysez cette non-conformité :\n\n" +
1191
+ "Numéro d'exigence: " + nonConformity["Numéro d'exigence"] + "\n" +
1192
+ "Description: " + nonConformity["Exigence IFS Food 8"] + "\n" +
1193
+ "Constat détaillé: " + nonConformity["Explication (par l'auditeur/l'évaluateur)"] + "\n\n";
1194
+
1195
+ if (guideInfo && guideInfo['Good practice']) {
1196
+ prompt += "Référence Guide IFS v8:\n" +
1197
+ "Bonnes pratiques: " + (guideInfo['Good practice'] || 'Non disponible') + "\n" +
1198
+ "Éléments à vérifier: " + (guideInfo['Elements to check'] || 'Non disponible') + "\n\n";
1199
+ }
1200
+
1201
+ // Ajouter l'analyse de cause spécifique si disponible
1202
+ if (rcaData && rcaData.answers) {
1203
+ prompt += "ANALYSE DE CAUSE SPÉCIFIQUE RÉALISÉE:\n\n";
1204
+ rcaData.questions.forEach(function(question, qIndex) {
1205
+ var answer = rcaData.answers[question.id] || '';
1206
+ prompt += "Question " + (qIndex + 1) + ": " + question.text + "\n";
1207
+ prompt += "Réponse détaillée: " + answer + "\n\n";
1208
+ });
1209
+ prompt += "Basez impérativement vos recommandations sur cette analyse de cause spécifique.\n\n";
1210
+ }
1211
+
1212
+ prompt += "Fournissez une recommandation structurée, en langue française, en Markdown incluant:\n\n" +
1213
+ "## Correction Immédiate\n" +
1214
+ "## Analyse de la Cause Racine" + (rcaData ? " (Basée sur l'analyse spécifique)" : "") + "\n" +
1215
+ "## Actions Correctives Ciblées\n" +
1216
+ "## Type de Preuve Requise\n" +
1217
+ "## Plan de Surveillance\n" +
1218
+ "## Conclusion et Bonnes Pratiques";
1219
+
1220
+ var messages = [
1221
+ {
1222
+ role: "system",
1223
+ content: "Vous êtes un expert en sécurité alimentaire et en norme IFS Food 8. Fournissez des recommandations précises et actionables" +
1224
+ (rcaData ? ", en tenant compte spécifiquement de l'analyse de cause détaillée fournie." : ".")
1225
+ },
1226
+ {
1227
+ role: "user",
1228
+ content: prompt
1229
+ }
1230
+ ];
1231
+
1232
+ fetch(CONFIG.GROQ_API_ENDPOINT, {
1233
+ method: 'POST',
1234
+ headers: {
1235
+ 'Content-Type': 'application/json',
1236
+ 'Authorization': 'Bearer ' + AppState.apiKey
1237
+ },
1238
+ body: JSON.stringify({
1239
+ messages: messages,
1240
+ model: CONFIG.GROQ_MODEL,
1241
+ temperature: 0.7,
1242
+ max_tokens: 2000
1243
+ })
1244
+ })
1245
+ .then(function(response) {
1246
+ if (!response.ok) {
1247
+ throw new Error('API Error ' + response.status + ': ' + response.statusText);
1248
+ }
1249
+ return response.json();
1250
+ })
1251
+ .then(function(data) {
1252
+ var recommendation = data.choices[0].message.content;
1253
+ AppState.recommendations[index] = recommendation;
1254
+ // Mettre à jour la modal si elle est ouverte et correspond à l'élément
1255
+ if (AppState.currentModalIndex === index && DOM.detailModal.style.display === 'flex') {
1256
+ DOM.modalTextarea.value = recommendation;
1257
+ DOM.modalRenderedRecommendation.innerHTML = marked.parse(recommendation);
1258
+ }
1259
+ resolve(recommendation);
1260
+ })
1261
+ .catch(function(error) {
1262
+ console.error('Erreur génération recommandation:', error);
1263
+ Utils.showMessage('error', 'Erreur: ' + error.message);
1264
+ reject(error);
1265
+ });
1266
+ });
1267
+ }
1268
+ };
1269
+
1270
+ // Gestionnaire de fichiers
1271
+ var FileManager = {
1272
+ init: function() {
1273
+ DOM.apiKeyInput.addEventListener('input', function(e) {
1274
+ var key = e.target.value.trim();
1275
+ if (key) {
1276
+ AppState.apiKey = key;
1277
+ //Utils.showMessage('success', 'Clé API configurée avec succès'); // Uncomment if desired
1278
+ } else {
1279
+ AppState.apiKey = ''; // Clear API key if input is empty
1280
+ }
1281
+ });
1282
+ DOM.fileDropZone.addEventListener('click', function() {
1283
+ DOM.fileInput.click();
1284
+ });
1285
+ DOM.fileDropZone.addEventListener('dragover', FileManager.handleDragOver);
1286
+ DOM.fileDropZone.addEventListener('dragleave', FileManager.handleDragLeave);
1287
+ DOM.fileDropZone.addEventListener('drop', FileManager.handleDrop);
1288
+ DOM.fileInput.addEventListener('change', FileManager.handleFileSelect);
1289
+ },
1290
+
1291
+ handleDragOver: function(e) {
1292
+ e.preventDefault();
1293
+ DOM.fileDropZone.style.borderColor = '#1a73e8';
1294
+ DOM.fileDropZone.style.background = 'linear-gradient(135deg, #e8f0fe 0%, #f0f6ff 100%)';
1295
+ },
1296
+
1297
+ handleDragLeave: function(e) {
1298
+ e.preventDefault();
1299
+ DOM.fileDropZone.style.borderColor = '#dadce0';
1300
+ DOM.fileDropZone.style.background = 'linear-gradient(135deg, #fafbfc 0%, #f8fafd 100%)';
1301
+ },
1302
+
1303
+ handleDrop: function(e) {
1304
+ e.preventDefault();
1305
+ DOM.fileDropZone.style.borderColor = '#dadce0';
1306
+ DOM.fileDropZone.style.background = 'linear-gradient(135deg, #fafbfc 0%, #f8fafd 100%)';
1307
+
1308
+ var files = e.dataTransfer.files;
1309
+ if (files.length > 0) {
1310
+ FileManager.processFile(files[0]);
1311
+ }
1312
+ },
1313
+
1314
+ handleFileSelect: function(e) {
1315
+ var file = e.target.files[0];
1316
+ if (file && file.name.endsWith('.xlsx')) {
1317
+ FileManager.processFile(file);
1318
+ } else {
1319
+ Utils.showMessage('error', 'Veuillez sélectionner un fichier Excel (.xlsx)');
1320
+ }
1321
+ },
1322
+
1323
+ processFile: function(file) {
1324
+ var reader = new FileReader();
1325
+ reader.onload = function(e) {
1326
+ try {
1327
+ FileManager.parseExcelFile(e.target.result);
1328
+ } catch (error) {
1329
+ console.error('Erreur lecture fichier:', error);
1330
+ Utils.showMessage('error', 'Erreur lors de la lecture du fichier Excel');
1331
+ }
1332
+ };
1333
+ reader.readAsArrayBuffer(file);
1334
+ },
1335
+
1336
+ parseExcelFile: function(arrayBuffer) {
1337
+ var workbook = XLSX.read(arrayBuffer, { type: 'array' });
1338
+ var sheetName = workbook.SheetNames[0];
1339
+ var worksheet = workbook.Sheets[sheetName];
1340
+
1341
+ var allRows = XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: false });
1342
+
1343
+ if (allRows.length < CONFIG.DATA_START_INDEX + 1) {
1344
+ Utils.showMessage('error', 'Le fichier Excel ne contient pas suffisamment de données à partir de la ligne attendue.');
1345
+ return;
1346
+ }
1347
+
1348
+ var headerRow = allRows[CONFIG.HEADER_ROW_INDEX] || [];
1349
+ var columnMap = FileManager.mapColumns(headerRow);
1350
+ if (!columnMap) return;
1351
+
1352
+ var dataRows = allRows.slice(CONFIG.DATA_START_INDEX);
1353
+ var processedData = FileManager.processDataRows(dataRows, columnMap);
1354
+
1355
+ if (!processedData.length) {
1356
+ Utils.showMessage('error', 'Aucune donnée valide trouvée pour le traitement après le mappage des colonnes.');
1357
+ return;
1358
+ }
1359
+
1360
+ AppState.actionPlanData = processedData;
1361
+ DataTableManager.render();
1362
+ Utils.showMessage('success', 'Fichier chargé avec succès! ' + processedData.length + ' non-conformités trouvées.');
1363
+ },
1364
+
1365
+ mapColumns: function(headerRow) {
1366
+ var columnMap = { reqNumber: -1, reqText: -1, explanation: -1 };
1367
+
1368
+ headerRow.forEach(function(cell, index) {
1369
+ var cellValue = String(cell || '').trim();
1370
+ if (cellValue === CONFIG.EXPECTED_HEADERS[0]) {
1371
+ columnMap.reqNumber = index;
1372
+ } else if (cellValue === CONFIG.EXPECTED_HEADERS[1]) {
1373
+ columnMap.reqText = index;
1374
+ } else if (cellValue === CONFIG.EXPECTED_HEADERS[2]) {
1375
+ columnMap.explanation = index;
1376
+ }
1377
+ });
1378
+
1379
+ var missingColumns = [];
1380
+ if (columnMap.reqNumber === -1) missingColumns.push('requirementNo');
1381
+ if (columnMap.reqText === -1) missingColumns.push('requirementText');
1382
+ if (columnMap.explanation === -1) missingColumns.push('requirementExplanation');
1383
+
1384
+ if (missingColumns.length > 0) {
1385
+ Utils.showMessage('error', 'Colonnes manquantes ou mal nommées dans l\'en-tête du fichier Excel: ' + missingColumns.join(', ') + '. Assurez-vous que les en-têtes sont corrects (requirementNo, requirementText, requirementExplanation).');
1386
+ return null;
1387
+ }
1388
+
1389
+ return columnMap;
1390
+ },
1391
+
1392
+ processDataRows: function(dataRows, columnMap) {
1393
+ return dataRows
1394
+ .map(function(row) {
1395
+ return {
1396
+ "Numéro d'exigence": String(row[columnMap.reqNumber] || '').trim(),
1397
+ "Exigence IFS Food 8": String(row[columnMap.reqText] || '').trim(),
1398
+ "Explication (par l'auditeur/l'évaluateur)": String(row[columnMap.explanation] || '').trim()
1399
+ };
1400
+ })
1401
+ .filter(function(item) {
1402
+ // Filtrer les lignes qui ont au moins un numéro d'exigence et une exigence IFS
1403
+ return item["Numéro d'exigence"] && item["Exigence IFS Food 8"];
1404
+ });
1405
+ }
1406
+ };
1407
+
1408
+ // Gestionnaire de la table
1409
+ var DataTableManager = {
1410
+ render: function() {
1411
+ DOM.dataTableBody.innerHTML = '';
1412
+
1413
+ AppState.actionPlanData.forEach(function(item, index) {
1414
+ DataTableManager.createDataRow(item, index);
1415
+ });
1416
+
1417
+ DOM.dataTableContainer.style.display = 'block';
1418
+ },
1419
+
1420
+ createDataRow: function(item, index) {
1421
+ var row = document.createElement('tr');
1422
+ // Vérifier si une recommandation existe pour cet index
1423
+ var recommendationExists = !!AppState.recommendations[index];
1424
+
1425
+ var actionCellContent = '';
1426
+ if (recommendationExists) {
1427
+ // Si une recommandation existe, afficher un bouton pour la voir
1428
+ actionCellContent += '<button class="btn btn-primary btn-small view-recommendation-btn" data-index="' + index + '">' +
1429
+ '<span class="material-icons">visibility</span> Voir Recommandation</button>';
1430
+ actionCellContent += ' '; // Un petit espace entre les boutons
1431
+ }
1432
+ // Afficher toujours le bouton "Analyse spécifique"
1433
+ actionCellContent += '<button class="btn btn-warning btn-small rca-btn" data-index="' + index + '">' +
1434
+ '<span class="material-icons">psychology</span> Analyse spécifique</button>';
1435
+
1436
+
1437
+ row.innerHTML =
1438
+ '<td><a href="#" class="requirement-link" data-index="' + index + '">' +
1439
+ item["Numéro d'exigence"] + '</a></td>' +
1440
+ '<td>' + item["Exigence IFS Food 8"] + '</td>' +
1441
+ '<td>' + item["Explication (par l'auditeur/l'évaluateur)"] + '</td>' +
1442
+ '<td>' + actionCellContent + '</td>'; // Utiliser le contenu généré
1443
+
1444
+ DOM.dataTableBody.appendChild(row);
1445
+
1446
+ row.querySelector('.requirement-link').addEventListener('click', function(e) {
1447
+ e.preventDefault();
1448
+ ModalManager.open(index);
1449
+ });
1450
+
1451
+ // Ajouter l'écouteur d'événements pour le nouveau bouton 'Voir Recommandation' s'il existe
1452
+ var viewRecBtn = row.querySelector('.view-recommendation-btn');
1453
+ if (viewRecBtn) {
1454
+ viewRecBtn.addEventListener('click', function(e) {
1455
+ e.preventDefault();
1456
+ ModalManager.open(index); // Ouvrir le modal de détail
1457
+ });
1458
+ }
1459
+
1460
+ row.querySelector('.rca-btn').addEventListener('click', function() {
1461
+ RCAManager.start(index);
1462
+ });
1463
+ }
1464
+ };
1465
+
1466
+ // Gestionnaire de génération en lot
1467
+ var BulkGenerationManager = {
1468
+ init: function() {
1469
+ DOM.bulkGenerateBtn.addEventListener('click', function(event) {
1470
+ event.preventDefault();
1471
+ event.stopPropagation();
1472
+ BulkGenerationManager.startBulkGeneration();
1473
+ });
1474
+ },
1475
+
1476
+ startBulkGeneration: function() {
1477
+ if (!AppState.apiKey) {
1478
+ Utils.showMessage('error', 'Veuillez configurer votre clé API Groq');
1479
+ return;
1480
+ }
1481
+
1482
+ if (AppState.actionPlanData.length === 0) {
1483
+ Utils.showMessage('error', 'Aucun plan d\'action chargé');
1484
+ return;
1485
+ }
1486
+
1487
+ var itemsToProcess = AppState.actionPlanData
1488
+ .map(function(item, index) {
1489
+ return { item: item, index: index };
1490
+ })
1491
+ .filter(function(obj) {
1492
+ // Ne générer que pour les éléments qui n'ont pas encore de recommandation
1493
+ return !AppState.recommendations[obj.index];
1494
+ });
1495
+
1496
+ if (itemsToProcess.length === 0) {
1497
+ Utils.showMessage('success', 'Toutes les recommandations ont déjà été générées !');
1498
+ return;
1499
+ }
1500
+
1501
+ var delay = parseInt(DOM.delaySelect.value) * 1000;
1502
+ // Estimer le temps avec une marge pour l'appel API
1503
+ var estimatedTimeSec = (itemsToProcess.length * (delay / 1000 + 5)); // 5s estimées par appel API
1504
+ var estimatedTimeMin = Math.ceil(estimatedTimeSec / 60); // Arrondir au-dessus
1505
+
1506
+ var confirmed = confirm(
1507
+ 'Vous allez générer automatiquement ' + itemsToProcess.length + ' recommandations.\n' +
1508
+ 'Délai entre chaque génération: ' + (delay/1000) + 's\n' +
1509
+ 'Temps estimé: ' + estimatedTimeMin + ' minute(s)\n\n' +
1510
+ 'Cette génération automatique ne comprend pas d\'analyse de cause spécifique.\n' +
1511
+ 'Pour une analyse approfondie, utilisez les boutons "Analyse spécifique" individuels.\n\n' +
1512
+ 'Continuer ?'
1513
+ );
1514
+
1515
+ if (!confirmed) return;
1516
+
1517
+ AppState.bulkGeneration = {
1518
+ isRunning: true,
1519
+ currentIndex: 0,
1520
+ total: itemsToProcess.length,
1521
+ completed: 0,
1522
+ failed: [],
1523
+ itemsToProcess: itemsToProcess // Stocker les éléments à traiter
1524
+ };
1525
+
1526
+ BulkGenerationManager.updateUI(true);
1527
+ BulkGenerationManager.processBulk(itemsToProcess, delay);
1528
+ },
1529
+
1530
+ processBulk: function(itemsToProcess, delay) {
1531
+ var processNext = function(i) {
1532
+ if (i >= itemsToProcess.length || !AppState.bulkGeneration.isRunning) {
1533
+ BulkGenerationManager.completeBulkGeneration();
1534
+ return;
1535
+ }
1536
+
1537
+ var obj = itemsToProcess[i];
1538
+ AppState.bulkGeneration.currentIndex = i;
1539
+
1540
+ BulkGenerationManager.updateProgress();
1541
+ BulkGenerationManager.updateCurrentItem(obj.item["Numéro d'exigence"]);
1542
+
1543
+ // Génération automatique sans analyse de cause spécifique
1544
+ APIManager.generateRecommendation(obj.index, false, null)
1545
+ .then(function(recommendation) {
1546
+ if (recommendation) {
1547
+ AppState.bulkGeneration.completed++;
1548
+ } else {
1549
+ AppState.bulkGeneration.failed.push(obj.item["Numéro d'exigence"]);
1550
+ }
1551
+ })
1552
+ .catch(function(error) {
1553
+ console.error('Erreur pour ' + obj.item["Numéro d'exigence"] + ':', error);
1554
+ AppState.bulkGeneration.failed.push(obj.item["Numéro d'exigence"]);
1555
+ })
1556
+ .finally(function() {
1557
+ // Assurez-vous de passer à l'élément suivant même en cas d'erreur
1558
+ if (AppState.bulkGeneration.isRunning && i < itemsToProcess.length - 1) {
1559
+ setTimeout(function() {
1560
+ processNext(i + 1);
1561
+ }, delay);
1562
+ } else {
1563
+ processNext(i + 1); // Pour terminer proprement après le dernier élément
1564
+ }
1565
+ });
1566
+ };
1567
+
1568
+ processNext(0);
1569
+ },
1570
+
1571
+ updateUI: function(isRunning) {
1572
+ DOM.bulkGenerateBtn.disabled = isRunning;
1573
+ DOM.delaySelect.disabled = isRunning; // Désactiver la sélection du délai pendant la génération
1574
+ DOM.bulkProgress.classList.toggle('show', isRunning);
1575
+
1576
+ if (isRunning) {
1577
+ DOM.bulkGenerateBtn.innerHTML = '<span class="loading-spinner"></span> Génération en cours...';
1578
+ } else {
1579
+ DOM.bulkGenerateBtn.innerHTML = '<span class="material-icons">auto_awesome</span> Génération automatique';
1580
+ }
1581
+ },
1582
+
1583
+ updateProgress: function() {
1584
+ var current = AppState.bulkGeneration.currentIndex;
1585
+ var total = AppState.bulkGeneration.total;
1586
+ var percentage = total > 0 ? ((current + 1) / total) * 100 : 0;
1587
+
1588
+ DOM.progressBar.style.width = percentage + '%';
1589
+ DOM.progressCounter.textContent = (current + 1) + ' / ' + total;
1590
+ },
1591
+
1592
+ updateCurrentItem: function(reqNumber) {
1593
+ DOM.currentItem.innerHTML =
1594
+ '<span class="loading-spinner"></span> Génération automatique pour l\'exigence ' + reqNumber + '...';
1595
+ },
1596
+
1597
+ completeBulkGeneration: function() {
1598
+ var completed = AppState.bulkGeneration.completed;
1599
+ var failed = AppState.bulkGeneration.failed;
1600
+ var total = AppState.bulkGeneration.total;
1601
+
1602
+ AppState.bulkGeneration.isRunning = false;
1603
+ BulkGenerationManager.updateUI(false);
1604
+
1605
+ // IMPORTANT : Rafraîchir le tableau pour afficher les nouvelles recommandations
1606
+ DataTableManager.render();
1607
+
1608
+ var message = 'Génération automatique terminée : ' + completed + '/' + total + ' recommandations générées';
1609
+ if (failed.length > 0) {
1610
+ message += '\nÉchecs : ' + failed.join(', ');
1611
+ Utils.showMessage('error', message);
1612
+ } else {
1613
+ message += ' avec succès !';
1614
+ Utils.showMessage('success', message);
1615
+ }
1616
+
1617
+ DOM.currentItem.innerHTML =
1618
+ '<span class="material-icons" style="color: #34a853;">check_circle</span> Génération terminée !';
1619
+
1620
+ setTimeout(function() {
1621
+ DOM.bulkProgress.classList.remove('show');
1622
+ }, 3000);
1623
+ }
1624
+ };
1625
+
1626
+ // Gestionnaire de modal
1627
+ var ModalManager = {
1628
+ init: function() {
1629
+ DOM.modalClose.addEventListener('click', ModalManager.close);
1630
+ DOM.modalCancel.addEventListener('click', ModalManager.close);
1631
+ DOM.modalGenerate.addEventListener('click', ModalManager.generateRecommendation);
1632
+ DOM.textareaToggle.addEventListener('click', ModalManager.toggleTextarea);
1633
+
1634
+ DOM.detailModal.addEventListener('click', function(e) {
1635
+ if (e.target === DOM.detailModal) ModalManager.close();
1636
+ });
1637
+ },
1638
+
1639
+ toggleTextarea: function() {
1640
+ var isVisible = DOM.modalTextarea.style.display !== 'none';
1641
+ if (isVisible) {
1642
+ DOM.modalTextarea.style.display = 'none';
1643
+ DOM.textareaToggle.innerHTML = '<span class="material-icons">edit</span> Modifier';
1644
+ // Re-render markdown if textarea was visible and user might have typed something
1645
+ DOM.modalRenderedRecommendation.innerHTML = marked.parse(DOM.modalTextarea.value);
1646
+ } else {
1647
+ DOM.modalTextarea.style.display = 'block';
1648
+ DOM.textareaToggle.innerHTML = '<span class="material-icons">close</span> Fermer';
1649
+ DOM.modalTextarea.focus();
1650
+ }
1651
+ },
1652
+
1653
+ open: function(index) {
1654
+ AppState.currentModalIndex = index;
1655
+ var item = AppState.actionPlanData[index];
1656
+
1657
+ DOM.modalReqNum.textContent = item["Numéro d'exigence"];
1658
+ DOM.modalReqText.textContent = item["Exigence IFS Food 8"];
1659
+ DOM.modalExplanation.textContent = item["Explication (par l'auditeur/l'évaluateur)"];
1660
+
1661
+ var recommendation = AppState.recommendations[index];
1662
+ if (recommendation) {
1663
+ DOM.modalTextarea.value = recommendation;
1664
+ DOM.modalRenderedRecommendation.innerHTML = marked.parse(recommendation);
1665
+ } else {
1666
+ DOM.modalTextarea.value = '';
1667
+ DOM.modalRenderedRecommendation.innerHTML = '<p style="color: #666; font-style: italic;">Aucune recommandation générée. Utilisez la génération automatique ou l\'analyse spécifique.</p>';
1668
+ }
1669
+
1670
+ DOM.modalTextarea.style.display = 'none';
1671
+ DOM.textareaToggle.innerHTML = '<span class="material-icons">edit</span> Modifier';
1672
+ DOM.detailModal.style.display = 'flex';
1673
+ },
1674
+
1675
+ close: function() {
1676
+ DOM.detailModal.style.display = 'none';
1677
+ AppState.currentModalIndex = -1;
1678
+ },
1679
+
1680
+ generateRecommendation: function() {
1681
+ if (AppState.currentModalIndex === -1) return;
1682
+
1683
+ // Si le textarea est visible et non vide, on utilise son contenu
1684
+ var userProvidedText = DOM.modalTextarea.style.display !== 'none' && DOM.modalTextarea.value.trim() !== '' ? DOM.modalTextarea.value.trim() : null;
1685
+
1686
+ // Afficher un état de chargement
1687
+ DOM.modalRenderedRecommendation.innerHTML = '<p style="text-align: center;"><span class="loading-spinner"></span> Génération en cours...</p>';
1688
+
1689
+ APIManager.generateRecommendation(AppState.currentModalIndex, userProvidedText, null)
1690
+ .then(function(recommendation) {
1691
+ if (recommendation) {
1692
+ DOM.modalTextarea.value = recommendation;
1693
+ DOM.modalRenderedRecommendation.innerHTML = marked.parse(recommendation);
1694
+ Utils.showMessage('success', 'Recommandation générée/améliorée avec succès!');
1695
+ // Close textarea after generation
1696
+ DOM.modalTextarea.style.display = 'none';
1697
+ DOM.textareaToggle.innerHTML = '<span class="material-icons">edit</span> Modifier';
1698
+ // Rafraîchir la ligne du tableau pour ajouter le bouton "Voir Recommandation" si ce n'est pas déjà fait
1699
+ DataTableManager.render();
1700
+ }
1701
+ })
1702
+ .catch(function(error) {
1703
+ console.error('Erreur génération modal:', error);
1704
+ DOM.modalRenderedRecommendation.innerHTML = '<p style="color: red;">Erreur lors de la génération de la recommandation. Veuillez réessayer.</p>';
1705
+ });
1706
+ }
1707
+ };
1708
+
1709
+ // Gestionnaire d'export PDF
1710
+ var PDFManager = {
1711
+ init: function() {
1712
+ DOM.exportPdfBtn.addEventListener('click', PDFManager.exportToPDF);
1713
+ },
1714
+
1715
+ exportToPDF: function() {
1716
+ if (AppState.actionPlanData.length === 0) {
1717
+ Utils.showMessage('error', 'Aucune donnée à exporter. Veuillez charger un fichier Excel.');
1718
+ return;
1719
+ }
1720
+ var itemsWithRecommendations = AppState.actionPlanData.filter(function(item, index) {
1721
+ return AppState.recommendations[index];
1722
+ });
1723
+
1724
+ if (itemsWithRecommendations.length === 0) {
1725
+ Utils.showMessage('error', 'Aucune recommandation générée pour l\'exportation. Veuillez générer des recommandations d\'abord.');
1726
+ return;
1727
+ }
1728
+
1729
+ var pdfContainer = document.createElement('div');
1730
+ pdfContainer.style.fontFamily = 'Arial, sans-serif';
1731
+ pdfContainer.style.fontSize = '11px';
1732
+ pdfContainer.style.padding = '15px';
1733
+ pdfContainer.style.background = 'white';
1734
+
1735
+ var title = document.createElement('h1');
1736
+ title.style.textAlign = 'center';
1737
+ title.style.color = '#004080';
1738
+ title.style.marginBottom = '20px';
1739
+ title.style.fontSize = '18px';
1740
+ title.textContent = 'Rapport de Plan d\'Actions IFS avec Recommandations IA';
1741
+ pdfContainer.appendChild(title);
1742
+
1743
+ itemsWithRecommendations.forEach(function(item, index) { // Itérer sur les éléments filtrés
1744
+ var recommendation = AppState.recommendations[index]; // Récupérer la recommandation par l'index d'origine
1745
+ if (!recommendation) return; // Double vérification, ne devrait pas arriver avec le filtre
1746
+
1747
+ var section = document.createElement('div');
1748
+ section.style.marginBottom = '25px';
1749
+ section.style.border = '1px solid #e0e0e0';
1750
+ section.style.borderRadius = '8px';
1751
+ section.style.padding = '15px';
1752
+ section.style.background = '#fafafa';
1753
+ section.style.pageBreakInside = 'avoid'; // Empêcher la coupure de section sur plusieurs pages
1754
+
1755
+ var header = document.createElement('div');
1756
+ header.style.background = 'linear-gradient(135deg, #004080, #1a73e8)';
1757
+ header.style.color = 'white';
1758
+ header.style.padding = '10px 15px';
1759
+ header.style.margin = '-15px -15px 15px -15px';
1760
+ header.style.borderRadius = '7px 7px 0 0';
1761
+ header.innerHTML = '<h2 style="margin: 0; font-size: 14px;">Exigence ' +
1762
+ item["Numéro d'exigence"] + ' - ' + item["Exigence IFS Food 8"] + '</h2>';
1763
+ section.appendChild(header);
1764
+
1765
+ var constatDiv = document.createElement('div');
1766
+ constatDiv.style.marginBottom = '15px';
1767
+ constatDiv.style.padding = '10px';
1768
+ constatDiv.style.background = 'white';
1769
+ constatDiv.style.borderLeft = '4px solid #ea4335';
1770
+ constatDiv.innerHTML = '<strong style="color: #ea4335;">Constat:</strong><br>' +
1771
+ item["Explication (par l'auditeur/l'évaluateur)"];
1772
+ section.appendChild(constatDiv);
1773
+
1774
+ var recommendationDiv = document.createElement('div');
1775
+ recommendationDiv.style.background = 'white';
1776
+ recommendationDiv.style.padding = '15px';
1777
+ recommendationDiv.style.borderRadius = '6px';
1778
+ recommendationDiv.style.border = '1px solid #d0d0d0';
1779
+ // Convertir le markdown en HTML. Utiliser un div pour assurer le rendu correct
1780
+ recommendationDiv.innerHTML =
1781
+ '<h3 style="color: #1a73e8; margin-top: 0; font-size: 12px;">Recommandation IA</h3>' +
1782
+ '<div style="font-size: 10px;">' + marked.parse(recommendation) + '</div>';
1783
+ section.appendChild(recommendationDiv);
1784
+
1785
+ pdfContainer.appendChild(section);
1786
+ });
1787
+
1788
+ var options = {
1789
+ margin: [10, 10, 10, 10],
1790
+ filename: 'Plan_Action_IFS_VisiPilot_' + new Date().toISOString().split('T')[0] + '.pdf',
1791
+ image: { type: 'jpeg', quality: 0.95 },
1792
+ html2canvas: { scale: 1.5, logging: false, useCORS: true },
1793
+ jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
1794
+ };
1795
+
1796
+ Utils.showMessage('success', 'Génération du PDF en cours...');
1797
+ html2pdf().from(pdfContainer).set(options).save()
1798
+ .then(function() {
1799
+ Utils.showMessage('success', 'PDF généré et téléchargé avec succès!');
1800
+ })
1801
+ .catch(function(error) {
1802
+ console.error('Erreur export PDF:', error);
1803
+ Utils.showMessage('error', 'Erreur lors de la génération du PDF: ' + error.message);
1804
+ });
1805
+ }
1806
+ };
1807
+
1808
+ // Gestionnaire UI
1809
+ var UIManager = {
1810
+ init: function() {
1811
+ // Event listener for API key input is now in FileManager.init to group related inputs
1812
+ DOM.howToUseHeader.addEventListener('click', function() {
1813
+ var isExpanded = DOM.howToUseContent.style.display === 'block';
1814
+ DOM.howToUseContent.style.display = isExpanded ? 'none' : 'block';
1815
+
1816
+ if (isExpanded) {
1817
+ DOM.howToUseHeader.classList.remove('expanded');
1818
+ } else {
1819
+ DOM.howToUseHeader.classList.add('expanded');
1820
+ }
1821
+ });
1822
+ }
1823
+ };
1824
+
1825
+ // Gestionnaire d'analyse de cause dynamique
1826
+ var RCAManager = {
1827
+ init: function() {
1828
+ DOM.rcaModalClose.addEventListener('click', RCAManager.close);
1829
+ DOM.rcaPrevBtn.addEventListener('click', RCAManager.previousQuestion);
1830
+ DOM.rcaNextBtn.addEventListener('click', RCAManager.nextQuestion);
1831
+
1832
+ DOM.rcaModal.addEventListener('click', function(e) {
1833
+ if (e.target === DOM.rcaModal) RCAManager.close();
1834
+ });
1835
+ },
1836
+
1837
+ start: function(index) {
1838
+ AppState.rcaState = {
1839
+ currentIndex: index,
1840
+ currentQuestionIndex: 0,
1841
+ answers: {},
1842
+ questions: [],
1843
+ isActive: true,
1844
+ isGeneratingQuestions: true
1845
+ };
1846
+
1847
+ DOM.rcaModal.style.display = 'flex';
1848
+ RCAManager.showLoading();
1849
+
1850
+ // Générer les questions spécifiques
1851
+ var nonConformity = AppState.actionPlanData[index];
1852
+ APIManager.generateRCAQuestions(nonConformity)
1853
+ .then(function(questions) {
1854
+ AppState.rcaState.questions = questions;
1855
+ AppState.rcaState.isGeneratingQuestions = false;
1856
+ RCAManager.renderCurrentQuestion();
1857
+ })
1858
+ .catch(function(error) {
1859
+ console.error('Erreur génération questions RCA:', error);
1860
+ Utils.showMessage('error', 'Impossible de générer les questions d\'analyse: ' + error.message);
1861
+ RCAManager.close();
1862
+ });
1863
+ },
1864
+
1865
+ showLoading: function() {
1866
+ DOM.rcaContent.innerHTML =
1867
+ '<div class="rca-loading">' +
1868
+ '<div class="loading-spinner"></div>' +
1869
+ '<h3>Génération des questions d\'analyse spécifiques...</h3>' +
1870
+ '<p>L\'IA analyse votre non-conformité pour créer des questions ciblées.</p>' +
1871
+ '</div>';
1872
+
1873
+ DOM.rcaPrevBtn.style.display = 'none';
1874
+ DOM.rcaNextBtn.style.display = 'none';
1875
+ DOM.rcaProgress.textContent = 'Préparation...';
1876
+ },
1877
+
1878
+ close: function() {
1879
+ DOM.rcaModal.style.display = 'none';
1880
+ AppState.rcaState.isActive = false;
1881
+ },
1882
+
1883
+ renderCurrentQuestion: function() {
1884
+ if (AppState.rcaState.isGeneratingQuestions) {
1885
+ RCAManager.showLoading();
1886
+ return;
1887
+ }
1888
+
1889
+ var currentQ = AppState.rcaState.currentQuestionIndex;
1890
+ var totalQ = AppState.rcaState.questions.length;
1891
+ var question = AppState.rcaState.questions[currentQ];
1892
+
1893
+ if (!question) {
1894
+ // Si on arrive ici sans question, c'est que l'analyse est terminée ou qu'il y a eu un problème.
1895
+ // On peut appeler generateAnalysisAndRecommendation directement.
1896
+ RCAManager.generateAnalysisAndRecommendation();
1897
+ return;
1898
+ }
1899
+
1900
+ // Mise à jour de la progression
1901
+ DOM.rcaProgress.textContent = 'Question ' + (currentQ + 1) + ' sur ' + totalQ;
1902
+
1903
+ // Boutons navigation
1904
+ DOM.rcaPrevBtn.style.display = currentQ > 0 ? 'block' : 'none';
1905
+ DOM.rcaNextBtn.style.display = 'block';
1906
+ DOM.rcaNextBtn.innerHTML = currentQ === totalQ - 1 ?
1907
+ '<span class="material-icons">check</span> Terminer l\'analyse' :
1908
+ '<span class="material-icons">arrow_forward</span> Suivant';
1909
+
1910
+ // Contenu de la question
1911
+ var currentAnswer = AppState.rcaState.answers[question.id] || '';
1912
+ var html = '<div class="question-container">';
1913
+ html += '<div class="question-text">' + question.text + '</div>';
1914
+
1915
+ if (question.description) {
1916
+ html += '<div class="question-description">' + question.description + '</div>';
1917
+ }
1918
+
1919
+ html += '<textarea class="answer-textarea" id="answer_' + question.id + '" ' +
1920
+ 'placeholder="Décrivez en détail votre analyse pour cette question. Plus votre réponse sera précise, plus les recommandations seront ciblées...">' +
1921
+ currentAnswer + '</textarea>';
1922
+
1923
+ html += '</div>';
1924
+ DOM.rcaContent.innerHTML = html;
1925
+
1926
+ // Focus sur le textarea
1927
+ var textarea = document.getElementById('answer_' + question.id);
1928
+ if (textarea) {
1929
+ textarea.focus();
1930
+ }
1931
+ },
1932
+
1933
+ previousQuestion: function() {
1934
+ RCAManager.saveCurrentAnswer();
1935
+ if (AppState.rcaState.currentQuestionIndex > 0) {
1936
+ AppState.rcaState.currentQuestionIndex--;
1937
+ RCAManager.renderCurrentQuestion();
1938
+ }
1939
+ },
1940
+
1941
+ nextQuestion: function() {
1942
+ RCAManager.saveCurrentAnswer();
1943
+
1944
+ var currentQ = AppState.rcaState.currentQuestionIndex;
1945
+ var question = AppState.rcaState.questions[currentQ];
1946
+
1947
+ // Vérifier si une réponse est donnée et si elle a une longueur minimale
1948
+ if (!AppState.rcaState.answers[question.id] || AppState.rcaState.answers[question.id].trim().length < 10) {
1949
+ Utils.showMessage('error', 'Veuillez fournir une réponse détaillée (au moins 10 caractères) avant de continuer');
1950
+ return;
1951
+ }
1952
+
1953
+ if (currentQ === AppState.rcaState.questions.length - 1) {
1954
+ // Dernière question, générer l'analyse
1955
+ RCAManager.generateAnalysisAndRecommendation();
1956
+ } else {
1957
+ AppState.rcaState.currentQuestionIndex++;
1958
+ RCAManager.renderCurrentQuestion();
1959
+ }
1960
+ },
1961
+
1962
+ saveCurrentAnswer: function() {
1963
+ var currentQ = AppState.rcaState.currentQuestionIndex;
1964
+ var question = AppState.rcaState.questions[currentQ];
1965
+
1966
+ if (question) {
1967
+ var textarea = document.getElementById('answer_' + question.id);
1968
+ if (textarea) {
1969
+ AppState.rcaState.answers[question.id] = textarea.value.trim();
1970
+ }
1971
+ }
1972
+ },
1973
+
1974
+ generateAnalysisAndRecommendation: function() {
1975
+ var rcaData = {
1976
+ questions: AppState.rcaState.questions,
1977
+ answers: AppState.rcaState.answers
1978
+ };
1979
+
1980
+ // Afficher un résumé de l'analyse
1981
+ var summaryHtml = '<div class="analysis-summary">';
1982
+ summaryHtml += '<h3><span class="material-icons">analytics</span> Analyse de cause terminée</h3>';
1983
+ summaryHtml += '<p><strong>Questions analysées:</strong> ' + AppState.rcaState.questions.length + '</p>';
1984
+ summaryHtml += '<p><strong>Réponses détaillées:</strong></p><ul>';
1985
+
1986
+ AppState.rcaState.questions.forEach(function(question, qIndex) {
1987
+ var answer = AppState.rcaState.answers[question.id] || '';
1988
+ var preview = answer.length > 100 ? answer.substring(0, 100) + '...' : answer;
1989
+ summaryHtml += '<li><strong>Q' + (qIndex + 1) + ':</strong> ' + question.text + '<br><em>Réponse:</em> ' + preview + '</li>';
1990
+ });
1991
+
1992
+ summaryHtml += '</ul></div>';
1993
+
1994
+ summaryHtml += '<div style="text-align: center; margin-top: 20px;">';
1995
+ summaryHtml += '<button class="btn btn-success" onclick="RCAManager.proceedWithRecommendation()">';
1996
+ summaryHtml += '<span class="material-icons">auto_awesome</span> Générer la recommandation personnalisée';
1997
+ summaryHtml += '</button></div>';
1998
+
1999
+ DOM.rcaContent.innerHTML = summaryHtml;
2000
+ DOM.rcaPrevBtn.style.display = 'none';
2001
+ DOM.rcaNextBtn.style.display = 'none';
2002
+ DOM.rcaProgress.textContent = 'Analyse terminée';
2003
+ },
2004
+
2005
+ proceedWithRecommendation: function() {
2006
+ var rcaData = {
2007
+ questions: AppState.rcaState.questions,
2008
+ answers: AppState.rcaState.answers
2009
+ };
2010
+ var index = AppState.rcaState.currentIndex;
2011
+
2012
+ RCAManager.close();
2013
+
2014
+ Utils.showMessage('success', 'Génération de la recommandation basée sur votre analyse spécifique...');
2015
+
2016
+ APIManager.generateRecommendation(index, true, rcaData)
2017
+ .then(function(recommendation) {
2018
+ if (recommendation) {
2019
+ Utils.showMessage('success', 'Recommandation personnalisée générée avec succès!');
2020
+ // Ouvrir le modal de détail pour afficher la recommandation générée
2021
+ ModalManager.open(index);
2022
+ // IMPORTANT : Rafraîchir le tableau pour mettre à jour la ligne avec la nouvelle recommandation
2023
+ DataTableManager.render();
2024
+ }
2025
+ })
2026
+ .catch(function(error) {
2027
+ console.error('Erreur génération avec RCA:', error);
2028
+ Utils.showMessage('error', 'Erreur lors de la génération de la recommandation personnalisée: ' + error.message);
2029
+ });
2030
+ }
2031
+ };
2032
+
2033
+ // Initialisation
2034
+ document.addEventListener('DOMContentLoaded', function() {
2035
+ console.log('Initialisation Assistant VisiPilot IFS avec Analyse de Cause Dynamique');
2036
+
2037
+ marked.setOptions({
2038
+ gfm: true,
2039
+ breaks: true,
2040
+ sanitize: false
2041
+ });
2042
+
2043
+ UIManager.init();
2044
+ FileManager.init();
2045
+ ModalManager.init();
2046
+ PDFManager.init();
2047
+ BulkGenerationManager.init();
2048
+ RCAManager.init();
2049
+
2050
+ APIManager.loadGuideData();
2051
+
2052
+ console.log('Application initialisée avec analyse de cause dynamique par IA');
2053
+ });
2054
+
2055
+ // Exposer les fonctions nécessaires globalement (si besoin pour débogage ou appels externes)
2056
+ window.RCAManager = RCAManager;
2057
+ </script>
2058
+ </body>
2059
+ </html>