Spaces:
Running
Running
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Assistant VisiPilot pour Plan d'Actions IFS</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> | |
| <style> | |
| /* General Styles */ | |
| body { | |
| font-family: 'Inter', 'Roboto', sans-serif; | |
| margin: 0; | |
| padding: 0; | |
| background-color: #f8fafd; | |
| color: #333; | |
| line-height: 1.6; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 25px auto; | |
| padding: 30px; | |
| background-color: #ffffff; | |
| border-radius: 12px; | |
| box-shadow: 0 5px 25px rgba(0, 0, 0, 0.1); | |
| } | |
| /* Banner */ | |
| .banner { | |
| background-image: url('https://raw.githubusercontent.com/M00N69/BUSCAR/main/logo%2002%20copie.jpg'); | |
| background-size: cover; | |
| height: 120px; | |
| background-position: center; | |
| margin-bottom: 0; | |
| border-radius: 10px 10px 0 0; | |
| position: relative; | |
| background-blend-mode: overlay; | |
| background-color: rgba(26, 115, 232, 0.05); | |
| } | |
| .banner-overlay { | |
| display: none; | |
| } | |
| /* Header sous la bannière */ | |
| .header-section { | |
| background: linear-gradient(135deg, #1a73e8 0%, #004080 100%); | |
| color: white; | |
| text-align: center; | |
| padding: 25px 20px; | |
| margin-bottom: 30px; | |
| border-radius: 0 0 10px 10px; | |
| } | |
| .main-header { | |
| font-size: 32px; | |
| font-weight: 700; | |
| color: white; | |
| margin-bottom: 8px; | |
| letter-spacing: -0.5px; | |
| } | |
| .main-subtitle { | |
| color: rgba(255, 255, 255, 0.9); | |
| font-size: 18px; | |
| margin: 0; | |
| font-weight: 400; | |
| } | |
| /* Expander */ | |
| .expander-header { | |
| background-color: #e8f0fe; | |
| border: 1px solid #c5dafc; | |
| border-radius: 8px; | |
| padding: 18px 25px; | |
| cursor: pointer; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| font-weight: 500; | |
| color: #1a73e8; | |
| transition: background-color 0.3s ease, box-shadow 0.2s ease; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); | |
| } | |
| .expander-header:hover { | |
| background-color: #d2e3fc; | |
| box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); | |
| } | |
| .expander-header::after { | |
| content: '▼'; | |
| margin-left: 15px; | |
| transition: transform 0.3s ease; | |
| font-size: 0.9em; | |
| } | |
| .expander-header.expanded::after { | |
| content: '▲'; | |
| transform: rotate(180deg); | |
| } | |
| .expander-content { | |
| background-color: #f0f6ff; | |
| border: 1px solid #d2e3fc; | |
| border-top: none; | |
| border-radius: 0 0 8px 8px; | |
| padding: 25px; | |
| margin-top: -15px; | |
| display: none; | |
| overflow: hidden; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.03); | |
| } | |
| .expander-content ol { | |
| padding-left: 25px; | |
| margin-top: 15px; | |
| } | |
| .expander-content li { | |
| margin-bottom: 10px; | |
| } | |
| .expander-content strong { | |
| color: #004080; | |
| } | |
| /* Form Elements */ | |
| .form-group { | |
| margin-bottom: 25px; | |
| padding: 15px; | |
| background-color: #f7f9fc; | |
| border-radius: 8px; | |
| border: 1px solid #e0e0e0; | |
| } | |
| .form-group label { | |
| display: block; | |
| margin-bottom: 10px; | |
| font-weight: 500; | |
| color: #5f6368; | |
| font-size: 1.1em; | |
| } | |
| .form-input { | |
| width: calc(100% - 24px); | |
| padding: 12px; | |
| border: 1px solid #dadce0; | |
| border-radius: 8px; | |
| font-size: 16px; | |
| box-sizing: border-box; | |
| transition: border-color 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| .form-input:focus { | |
| border-color: #1a73e8; | |
| box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2); | |
| outline: none; | |
| } | |
| /* File Drop Zone */ | |
| .file-drop-zone { | |
| border: 2px dashed #dadce0; | |
| border-radius: 12px; | |
| padding: 32px; | |
| text-align: center; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| background: linear-gradient(135deg, #fafbfc 0%, #f8fafd 100%); | |
| } | |
| .file-drop-zone:hover { | |
| border-color: #1a73e8; | |
| background: linear-gradient(135deg, #e8f0fe 0%, #f0f6ff 100%); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); | |
| } | |
| .file-drop-icon { | |
| font-size: 48px; | |
| color: #1a73e8; | |
| opacity: 0.7; | |
| margin-bottom: 16px; | |
| } | |
| /* Buttons */ | |
| .btn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 14px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| min-height: 44px; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); | |
| } | |
| .btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); | |
| } | |
| .btn:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| transform: none ; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, #004080 0%, #1a73e8 100%); | |
| color: white; | |
| } | |
| .btn-success { | |
| background: linear-gradient(135deg, #34a853 0%, #2e8b4e 100%); | |
| color: white; | |
| } | |
| .btn-danger { | |
| background: linear-gradient(135deg, #ea4335 0%, #d93025 100%); | |
| color: white; | |
| } | |
| .btn-small { | |
| padding: 8px 16px; | |
| font-size: 12px; | |
| min-height: 36px; | |
| } | |
| /* Messages */ | |
| .message { | |
| padding: 16px 20px; | |
| border-radius: 8px; | |
| margin-bottom: 20px; | |
| display: none; | |
| align-items: center; | |
| gap: 12px; | |
| font-weight: 500; | |
| } | |
| .message.show { | |
| display: flex; | |
| } | |
| .message-success { | |
| background: #e6f4ea; | |
| color: #1e8e3e; | |
| border: 1px solid #c8e6c9; | |
| } | |
| .message-error { | |
| background: #fce8e6; | |
| color: #c5221f; | |
| border: 1px solid #f9bdbb; | |
| } | |
| /* Data Table */ | |
| .data-table-container { | |
| background: #ffffff; | |
| border-radius: 12px; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); | |
| overflow: hidden; | |
| margin-top: 35px; | |
| display: none; | |
| } | |
| .data-table-header { | |
| background: linear-gradient(135deg, #004080 0%, #1a73e8 100%); | |
| color: white; | |
| padding: 20px 24px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| } | |
| .data-table-title { | |
| font-size: 20px; | |
| font-weight: 600; | |
| } | |
| .data-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| .data-table th, | |
| .data-table td { | |
| border: 1px solid #e8eaed; | |
| padding: 15px; | |
| text-align: left; | |
| vertical-align: top; | |
| font-size: 15px; | |
| } | |
| .data-table th { | |
| background-color: #f0f4f7; | |
| color: #3c4043; | |
| font-weight: 500; | |
| } | |
| .data-table tbody tr:nth-child(even) { | |
| background-color: #fcfdff; | |
| } | |
| .data-table tbody tr:hover { | |
| background-color: #f5f8fc; | |
| } | |
| .requirement-link { | |
| color: #1a73e8; | |
| text-decoration: none; | |
| font-weight: 500; | |
| transition: color 0.2s ease; | |
| } | |
| .requirement-link:hover { | |
| color: #004080; | |
| text-decoration: underline; | |
| } | |
| /* Bulk Generation */ | |
| .bulk-generation-container { | |
| margin-top: 16px; | |
| padding: 16px; | |
| background: linear-gradient(135deg, #e8f0fe 0%, #f0f6ff 100%); | |
| border: 1px solid #1a73e8; | |
| border-radius: 8px; | |
| text-align: center; | |
| } | |
| .generation-options { | |
| display: flex; | |
| gap: 12px; | |
| align-items: center; | |
| justify-content: center; | |
| margin-top: 12px; | |
| flex-wrap: wrap; | |
| } | |
| .option-select { | |
| padding: 4px 8px; | |
| border: 1px solid #dadce0; | |
| border-radius: 4px; | |
| font-size: 11px; | |
| background: #ffffff; | |
| } | |
| .bulk-progress { | |
| display: none; | |
| margin-top: 16px; | |
| padding: 16px; | |
| background: #ffffff; | |
| border-radius: 8px; | |
| border: 1px solid #e8eaed; | |
| } | |
| .bulk-progress.show { | |
| display: block; | |
| } | |
| .progress-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 12px; | |
| } | |
| .progress-counter { | |
| background: #1a73e8; | |
| color: white; | |
| padding: 4px 12px; | |
| border-radius: 12px; | |
| font-size: 12px; | |
| font-weight: 500; | |
| } | |
| .progress-bar-container { | |
| background: #e0e0e0; | |
| border-radius: 8px; | |
| height: 8px; | |
| overflow: hidden; | |
| margin-bottom: 12px; | |
| } | |
| .progress-bar { | |
| background: linear-gradient(90deg, #004080 0%, #1a73e8 100%); | |
| height: 100%; | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| } | |
| .current-item { | |
| font-size: 13px; | |
| color: #5f6368; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .loading-spinner { | |
| display: inline-block; | |
| width: 16px; | |
| height: 16px; | |
| border: 2px solid rgba(26, 115, 232, 0.3); | |
| border-radius: 50%; | |
| border-top-color: #1a73e8; | |
| animation: spin 1s ease-in-out infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| /* Modal */ | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.6); | |
| z-index: 1000; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 20px; | |
| backdrop-filter: blur(4px); | |
| } | |
| .modal-content { | |
| background: #ffffff; | |
| border-radius: 12px; | |
| max-width: 900px; | |
| width: 100%; | |
| max-height: 90vh; | |
| overflow: hidden; | |
| box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2); | |
| } | |
| .modal-header { | |
| background: linear-gradient(135deg, #004080 0%, #1a73e8 100%); | |
| color: white; | |
| padding: 24px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .modal-close { | |
| background: none; | |
| border: none; | |
| color: white; | |
| font-size: 24px; | |
| cursor: pointer; | |
| padding: 4px; | |
| border-radius: 50%; | |
| transition: background-color 0.2s ease; | |
| } | |
| .modal-close:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| } | |
| .modal-body { | |
| padding: 24px; | |
| max-height: 60vh; | |
| overflow-y: auto; | |
| } | |
| .modal-field { | |
| margin-bottom: 20px; | |
| } | |
| .modal-field-label { | |
| font-weight: 600; | |
| color: #3c4043; | |
| margin-bottom: 8px; | |
| display: block; | |
| } | |
| .modal-textarea { | |
| width: 100%; | |
| min-height: 120px; | |
| padding: 16px; | |
| border: 2px solid #dadce0; | |
| border-radius: 8px; | |
| font-family: inherit; | |
| font-size: 14px; | |
| resize: vertical; | |
| display: none; | |
| transition: border-color 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| .modal-textarea:focus { | |
| border-color: #1a73e8; | |
| box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1); | |
| outline: none; | |
| } | |
| .textarea-toggle { | |
| background: #ffffff; | |
| border: 1px solid #dadce0; | |
| border-radius: 6px; | |
| padding: 8px 12px; | |
| cursor: pointer; | |
| color: #1a73e8; | |
| font-size: 13px; | |
| font-weight: 500; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| transition: all 0.2s ease; | |
| } | |
| .textarea-toggle:hover { | |
| background: #f8fafd; | |
| border-color: #1a73e8; | |
| } | |
| .modal-actions { | |
| padding: 24px; | |
| border-top: 1px solid #e8eaed; | |
| display: flex; | |
| justify-content: flex-end; | |
| gap: 12px; | |
| } | |
| .recommendation-content { | |
| background: #ffffff; | |
| border: 1px solid #e0e0e0; | |
| padding: 15px; | |
| border-radius: 8px; | |
| min-height: 80px; | |
| overflow-y: auto; | |
| max-height: 250px; | |
| margin-bottom: 10px; | |
| color: #333; | |
| box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .container { | |
| margin: 15px; | |
| padding: 20px; | |
| } | |
| .banner { | |
| height: 80px; | |
| } | |
| .main-header { | |
| font-size: 24px; | |
| } | |
| .main-subtitle { | |
| font-size: 16px; | |
| } | |
| .data-table th:nth-child(2), | |
| .data-table td:nth-child(2) { | |
| display: none; | |
| } | |
| .modal-content { | |
| width: 95%; | |
| } | |
| .modal-body { | |
| padding: 16px; | |
| } | |
| .modal-actions { | |
| padding: 16px; | |
| flex-direction: column; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="banner"></div> | |
| <div class="container"> | |
| <div class="header-section"> | |
| <h1 class="main-header">Assistant VisiPilot pour Plan d'Actions IFS</h1> | |
| <p class="main-subtitle">Génération intelligente de plans d'actions IFS Food 8</p> | |
| </div> | |
| <div class="expander"> | |
| <div class="expander-header" id="howToUseHeader"> | |
| <span>Comment utiliser cette application</span> | |
| </div> | |
| <div class="expander-content" id="howToUseContent"> | |
| <p><strong>Étapes d'utilisation:</strong></p> | |
| <ol> | |
| <li><strong>Saisissez votre clé API Groq:</strong> Entrez votre clé API dans le champ dédié.</li> | |
| <li><strong>Téléchargez votre plan d'actions IFS v8:</strong> Glissez-déposez votre fichier Excel.</li> | |
| <li><strong>Générez des recommandations:</strong> Utilisez les boutons individuels ou la génération en lot.</li> | |
| <li><strong>Affinez les propositions:</strong> Cliquez sur le numéro d'exigence pour éditer.</li> | |
| <li><strong>Exportez en PDF:</strong> Générez un rapport complet.</li> | |
| </ol> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label for="groqApiKey">Votre clé API Groq :</label> | |
| <input type="password" id="groqApiKey" class="form-input" placeholder="Entrez votre clé API Groq"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Téléchargez votre plan d'action (fichier Excel) :</label> | |
| <div class="file-drop-zone" id="fileDropZone"> | |
| <input type="file" id="fileInput" accept=".xlsx" style="display: none;"> | |
| <div class="file-drop-content"> | |
| <span class="material-icons file-drop-icon">upload_file</span> | |
| <div>Glissez-déposez votre fichier Excel ici</div> | |
| <div>ou cliquez pour sélectionner (.xlsx)</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="message message-success" id="successMessage"> | |
| <span class="material-icons">check_circle</span> | |
| <span id="successText"></span> | |
| </div> | |
| <div class="message message-error" id="errorMessage"> | |
| <span class="material-icons">error</span> | |
| <span id="errorText"></span> | |
| </div> | |
| <div class="data-table-container" id="dataTableContainer"> | |
| <div class="data-table-header"> | |
| <h2 class="data-table-title">Plan d'Action IFS</h2> | |
| <div style="display: flex; gap: 12px; align-items: center;"> | |
| <button class="btn btn-success" id="bulkGenerateBtn"> | |
| <span class="material-icons">auto_awesome</span> | |
| Générer toutes les recommandations | |
| </button> | |
| <button class="btn btn-danger" id="exportPdfBtn"> | |
| <span class="material-icons">picture_as_pdf</span> | |
| Exporter PDF | |
| </button> | |
| </div> | |
| </div> | |
| <div class="bulk-generation-container"> | |
| <div style="margin-bottom: 12px;"> | |
| <strong>Génération en lot :</strong> Générez automatiquement toutes les recommandations manquantes. | |
| </div> | |
| <div class="generation-options"> | |
| <div style="display: flex; align-items: center; gap: 6px;"> | |
| <label for="delaySelect">Délai :</label> | |
| <select class="option-select" id="delaySelect"> | |
| <option value="5">5 secondes</option> | |
| <option value="10" selected>10 secondes</option> | |
| <option value="15">15 secondes</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="bulk-progress" id="bulkProgress"> | |
| <div class="progress-header"> | |
| <div>Génération en cours...</div> | |
| <div class="progress-counter" id="progressCounter">0 / 0</div> | |
| </div> | |
| <div class="progress-bar-container"> | |
| <div class="progress-bar" id="progressBar"></div> | |
| </div> | |
| <div class="current-item" id="currentItem">En attente...</div> | |
| </div> | |
| </div> | |
| <table class="data-table" id="dataTable"> | |
| <thead> | |
| <tr> | |
| <th>Numéro d'exigence</th> | |
| <th>Exigence IFS Food 8</th> | |
| <th>Constat détaillé</th> | |
| <th>Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody id="dataTableBody"> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div class="modal" id="detailModal"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h2>Détail de la non-conformité</h2> | |
| <button class="modal-close" id="modalClose"> | |
| <span class="material-icons">close</span> | |
| </button> | |
| </div> | |
| <div class="modal-body"> | |
| <div class="modal-field"> | |
| <label class="modal-field-label">Numéro d'exigence:</label> | |
| <div id="modalReqNum"></div> | |
| </div> | |
| <div class="modal-field"> | |
| <label class="modal-field-label">Exigence IFS Food 8:</label> | |
| <div id="modalReqText"></div> | |
| </div> | |
| <div class="modal-field"> | |
| <label class="modal-field-label">Constat détaillé:</label> | |
| <div id="modalExplanation"></div> | |
| </div> | |
| <div class="modal-field"> | |
| <label class="modal-field-label">Recommandation IA:</label> | |
| <div class="recommendation-content" id="modalRenderedRecommendation"></div> | |
| </div> | |
| <div class="modal-field"> | |
| <label class="modal-field-label">Modifier la recommandation:</label> | |
| <button class="textarea-toggle" id="textareaToggle"> | |
| <span class="material-icons">edit</span> | |
| Modifier | |
| </button> | |
| <textarea class="modal-textarea" id="modalTextarea"></textarea> | |
| </div> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="btn btn-primary" id="modalCancel">Fermer</button> | |
| <button class="btn btn-success" id="modalGenerate"> | |
| <span class="material-icons">auto_awesome</span> | |
| Générer/Améliorer | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.3/html2pdf.bundle.min.js"></script> | |
| <script> | |
| // Configuration | |
| var CONFIG = { | |
| GROQ_API_ENDPOINT: "https://api.groq.com/openai/v1/chat/completions", | |
| GROQ_MODEL: "openai/gpt-oss-120b", | |
| GUIDE_CSV_URL: "https://raw.githubusercontent.com/M00N69/Action-planGroq/main/Guide%20Checklist_IFS%20Food%20V%208%20-%20CHECKLIST.csv", | |
| EXPECTED_HEADERS: ["requirementNo", "requirementText", "requirementExplanation"], | |
| HEADER_ROW_INDEX: 11, | |
| DATA_START_INDEX: 13 | |
| }; | |
| // État global | |
| var AppState = { | |
| apiKey: '', | |
| actionPlanData: [], | |
| guideData: [], | |
| recommendations: {}, | |
| currentModalIndex: -1, | |
| isLoading: false, | |
| bulkGeneration: { | |
| isRunning: false, | |
| currentIndex: 0, | |
| total: 0, | |
| completed: 0, | |
| failed: [] | |
| } | |
| }; | |
| // Éléments DOM | |
| var DOM = { | |
| apiKeyInput: document.getElementById('groqApiKey'), | |
| fileDropZone: document.getElementById('fileDropZone'), | |
| fileInput: document.getElementById('fileInput'), | |
| successMessage: document.getElementById('successMessage'), | |
| errorMessage: document.getElementById('errorMessage'), | |
| successText: document.getElementById('successText'), | |
| errorText: document.getElementById('errorText'), | |
| dataTableContainer: document.getElementById('dataTableContainer'), | |
| dataTableBody: document.getElementById('dataTableBody'), | |
| bulkGenerateBtn: document.getElementById('bulkGenerateBtn'), | |
| bulkProgress: document.getElementById('bulkProgress'), | |
| progressBar: document.getElementById('progressBar'), | |
| progressCounter: document.getElementById('progressCounter'), | |
| currentItem: document.getElementById('currentItem'), | |
| delaySelect: document.getElementById('delaySelect'), | |
| exportPdfBtn: document.getElementById('exportPdfBtn'), | |
| detailModal: document.getElementById('detailModal'), | |
| modalClose: document.getElementById('modalClose'), | |
| modalCancel: document.getElementById('modalCancel'), | |
| modalGenerate: document.getElementById('modalGenerate'), | |
| modalReqNum: document.getElementById('modalReqNum'), | |
| modalReqText: document.getElementById('modalReqText'), | |
| modalExplanation: document.getElementById('modalExplanation'), | |
| modalRenderedRecommendation: document.getElementById('modalRenderedRecommendation'), | |
| modalTextarea: document.getElementById('modalTextarea'), | |
| textareaToggle: document.getElementById('textareaToggle'), | |
| howToUseHeader: document.getElementById('howToUseHeader'), | |
| howToUseContent: document.getElementById('howToUseContent') | |
| }; | |
| // Utilitaires | |
| var Utils = { | |
| showMessage: function(type, text) { | |
| var messageEl = DOM[type + 'Message']; | |
| var textEl = DOM[type + 'Text']; | |
| textEl.textContent = text; | |
| messageEl.classList.add('show'); | |
| setTimeout(function() { | |
| messageEl.classList.remove('show'); | |
| }, 5000); | |
| }, | |
| parseCSV: function(csvText) { | |
| var lines = csvText.split('\n'); | |
| var headers = lines[0].split(';').map(function(h) { | |
| return h.trim(); | |
| }); | |
| var data = []; | |
| for (var i = 1; i < lines.length; i++) { | |
| var currentLine = lines[i].split(';'); | |
| if (currentLine.length === headers.length) { | |
| var row = {}; | |
| headers.forEach(function(header, index) { | |
| row[header] = currentLine[index] ? currentLine[index].trim() : ''; | |
| }); | |
| data.push(row); | |
| } | |
| } | |
| return data; | |
| } | |
| }; | |
| // Gestionnaire API | |
| var APIManager = { | |
| loadGuideData: function() { | |
| fetch(CONFIG.GUIDE_CSV_URL) | |
| .then(function(response) { | |
| if (!response.ok) throw new Error('HTTP ' + response.status); | |
| return response.text(); | |
| }) | |
| .then(function(csvText) { | |
| AppState.guideData = Utils.parseCSV(csvText); | |
| console.log('Guide IFS chargé:', AppState.guideData.length, 'entrées'); | |
| }) | |
| .catch(function(error) { | |
| console.error('Erreur chargement guide:', error); | |
| Utils.showMessage('error', 'Impossible de charger le guide IFS'); | |
| }); | |
| }, | |
| getGuideInfo: function(reqNumber) { | |
| if (!AppState.guideData.length || !reqNumber) return null; | |
| return AppState.guideData.find(function(row) { | |
| return row['NUM_REQ'] && reqNumber && String(row['NUM_REQ']).includes(String(reqNumber)); | |
| }); | |
| }, | |
| generateRecommendation: function(index, isRegeneration) { | |
| return new Promise(function(resolve, reject) { | |
| if (!AppState.apiKey) { | |
| Utils.showMessage('error', 'Veuillez configurer votre clé API Groq'); | |
| reject(new Error('Pas de clé API')); | |
| return; | |
| } | |
| var nonConformity = AppState.actionPlanData[index]; | |
| if (!nonConformity) { | |
| Utils.showMessage('error', 'Non-conformité introuvable'); | |
| reject(new Error('Non-conformité introuvable')); | |
| return; | |
| } | |
| var guideInfo = APIManager.getGuideInfo(nonConformity["Numéro d'exigence"]); | |
| var prompt = "En tant qu'expert en sécurité alimentaire et IFS Food 8, analysez cette non-conformité :\n\n" + | |
| "Numéro d'exigence: " + nonConformity["Numéro d'exigence"] + "\n" + | |
| "Description: " + nonConformity["Exigence IFS Food 8"] + "\n" + | |
| "Constat détaillé: " + nonConformity["Explication (par l'auditeur/l'évaluateur)"] + "\n\n"; | |
| if (guideInfo && guideInfo['Good practice']) { | |
| prompt += "Référence Guide IFS v8:\n" + | |
| "Bonnes pratiques: " + (guideInfo['Good practice'] || 'Non disponible') + "\n" + | |
| "Éléments à vérifier: " + (guideInfo['Elements to check'] || 'Non disponible') + "\n\n"; | |
| } | |
| prompt += "Fournissez une recommandation structurée en Markdown incluant:\n\n" + | |
| "## Correction Immédiate\n" + | |
| "## Type de Preuve Requise\n" + | |
| "## Cause Probable\n" + | |
| "## Actions Correctives\n" + | |
| "## Conclusion et Bonnes Pratiques"; | |
| var messages = [ | |
| { | |
| role: "system", | |
| content: "Vous êtes un expert en sécurité alimentaire et en norme IFS Food 8. Fournissez des recommandations précises et actionables." | |
| }, | |
| { | |
| role: "user", | |
| content: prompt | |
| } | |
| ]; | |
| fetch(CONFIG.GROQ_API_ENDPOINT, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': 'Bearer ' + AppState.apiKey | |
| }, | |
| body: JSON.stringify({ | |
| messages: messages, | |
| model: CONFIG.GROQ_MODEL, | |
| temperature: 0.7, | |
| max_tokens: 2000 | |
| }) | |
| }) | |
| .then(function(response) { | |
| if (!response.ok) { | |
| throw new Error('API Error ' + response.status); | |
| } | |
| return response.json(); | |
| }) | |
| .then(function(data) { | |
| var recommendation = data.choices[0].message.content; | |
| AppState.recommendations[index] = recommendation; | |
| resolve(recommendation); | |
| }) | |
| .catch(function(error) { | |
| console.error('Erreur génération recommandation:', error); | |
| Utils.showMessage('error', 'Erreur: ' + error.message); | |
| reject(error); | |
| }); | |
| }); | |
| } | |
| }; | |
| // Gestionnaire de fichiers | |
| var FileManager = { | |
| init: function() { | |
| DOM.fileDropZone.addEventListener('click', function() { | |
| DOM.fileInput.click(); | |
| }); | |
| DOM.fileDropZone.addEventListener('dragover', FileManager.handleDragOver); | |
| DOM.fileDropZone.addEventListener('dragleave', FileManager.handleDragLeave); | |
| DOM.fileDropZone.addEventListener('drop', FileManager.handleDrop); | |
| DOM.fileInput.addEventListener('change', FileManager.handleFileSelect); | |
| }, | |
| handleDragOver: function(e) { | |
| e.preventDefault(); | |
| DOM.fileDropZone.style.borderColor = '#1a73e8'; | |
| DOM.fileDropZone.style.background = 'linear-gradient(135deg, #e8f0fe 0%, #f0f6ff 100%)'; | |
| }, | |
| handleDragLeave: function(e) { | |
| e.preventDefault(); | |
| DOM.fileDropZone.style.borderColor = '#dadce0'; | |
| DOM.fileDropZone.style.background = 'linear-gradient(135deg, #fafbfc 0%, #f8fafd 100%)'; | |
| }, | |
| handleDrop: function(e) { | |
| e.preventDefault(); | |
| DOM.fileDropZone.style.borderColor = '#dadce0'; | |
| DOM.fileDropZone.style.background = 'linear-gradient(135deg, #fafbfc 0%, #f8fafd 100%)'; | |
| var files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| FileManager.processFile(files[0]); | |
| } | |
| }, | |
| handleFileSelect: function(e) { | |
| var file = e.target.files[0]; | |
| if (file && file.name.endsWith('.xlsx')) { | |
| FileManager.processFile(file); | |
| } else { | |
| Utils.showMessage('error', 'Veuillez sélectionner un fichier Excel (.xlsx)'); | |
| } | |
| }, | |
| processFile: function(file) { | |
| var reader = new FileReader(); | |
| reader.onload = function(e) { | |
| try { | |
| FileManager.parseExcelFile(e.target.result); | |
| } catch (error) { | |
| console.error('Erreur lecture fichier:', error); | |
| Utils.showMessage('error', 'Erreur lors de la lecture du fichier Excel'); | |
| } | |
| }; | |
| reader.readAsArrayBuffer(file); | |
| }, | |
| parseExcelFile: function(arrayBuffer) { | |
| var workbook = XLSX.read(arrayBuffer, { type: 'array' }); | |
| var sheetName = workbook.SheetNames[0]; | |
| var worksheet = workbook.Sheets[sheetName]; | |
| var allRows = XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: false }); | |
| if (allRows.length < CONFIG.DATA_START_INDEX + 1) { | |
| Utils.showMessage('error', 'Le fichier Excel ne contient pas suffisamment de données'); | |
| return; | |
| } | |
| var headerRow = allRows[CONFIG.HEADER_ROW_INDEX] || []; | |
| var columnMap = FileManager.mapColumns(headerRow); | |
| if (!columnMap) return; | |
| var dataRows = allRows.slice(CONFIG.DATA_START_INDEX); | |
| var processedData = FileManager.processDataRows(dataRows, columnMap); | |
| if (!processedData.length) { | |
| Utils.showMessage('error', 'Aucune donnée valide trouvée'); | |
| return; | |
| } | |
| AppState.actionPlanData = processedData; | |
| DataTableManager.render(); | |
| Utils.showMessage('success', 'Fichier chargé avec succès! ' + processedData.length + ' non-conformités trouvées.'); | |
| }, | |
| mapColumns: function(headerRow) { | |
| var columnMap = { reqNumber: -1, reqText: -1, explanation: -1 }; | |
| headerRow.forEach(function(cell, index) { | |
| var cellValue = String(cell || '').trim(); | |
| if (cellValue === CONFIG.EXPECTED_HEADERS[0]) { | |
| columnMap.reqNumber = index; | |
| } else if (cellValue === CONFIG.EXPECTED_HEADERS[1]) { | |
| columnMap.reqText = index; | |
| } else if (cellValue === CONFIG.EXPECTED_HEADERS[2]) { | |
| columnMap.explanation = index; | |
| } | |
| }); | |
| var missingColumns = []; | |
| if (columnMap.reqNumber === -1) missingColumns.push('requirementNo'); | |
| if (columnMap.reqText === -1) missingColumns.push('requirementText'); | |
| if (columnMap.explanation === -1) missingColumns.push('requirementExplanation'); | |
| if (missingColumns.length > 0) { | |
| Utils.showMessage('error', 'Colonnes manquantes: ' + missingColumns.join(', ')); | |
| return null; | |
| } | |
| return columnMap; | |
| }, | |
| processDataRows: function(dataRows, columnMap) { | |
| return dataRows | |
| .map(function(row) { | |
| return { | |
| "Numéro d'exigence": String(row[columnMap.reqNumber] || '').trim(), | |
| "Exigence IFS Food 8": String(row[columnMap.reqText] || '').trim(), | |
| "Explication (par l'auditeur/l'évaluateur)": String(row[columnMap.explanation] || '').trim() | |
| }; | |
| }) | |
| .filter(function(item) { | |
| return item["Numéro d'exigence"] && item["Exigence IFS Food 8"]; | |
| }); | |
| } | |
| }; | |
| // Gestionnaire de la table | |
| var DataTableManager = { | |
| render: function() { | |
| DOM.dataTableBody.innerHTML = ''; | |
| AppState.actionPlanData.forEach(function(item, index) { | |
| DataTableManager.createDataRow(item, index); | |
| }); | |
| DOM.dataTableContainer.style.display = 'block'; | |
| }, | |
| createDataRow: function(item, index) { | |
| var row = document.createElement('tr'); | |
| row.innerHTML = | |
| '<td><a href="#" class="requirement-link" data-index="' + index + '">' + | |
| item["Numéro d'exigence"] + '</a></td>' + | |
| '<td>' + item["Exigence IFS Food 8"] + '</td>' + | |
| '<td>' + item["Explication (par l'auditeur/l'évaluateur)"] + '</td>' + | |
| '<td><button class="btn btn-primary btn-small generate-btn" data-index="' + index + '">' + | |
| '<span class="material-icons">auto_awesome</span> Générer</button></td>'; | |
| DOM.dataTableBody.appendChild(row); | |
| row.querySelector('.requirement-link').addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| ModalManager.open(index); | |
| }); | |
| row.querySelector('.generate-btn').addEventListener('click', function() { | |
| DataTableManager.generateRecommendation(index); | |
| }); | |
| }, | |
| generateRecommendation: function(index) { | |
| APIManager.generateRecommendation(index) | |
| .then(function(recommendation) { | |
| if (recommendation) { | |
| Utils.showMessage('success', 'Recommandation générée avec succès!'); | |
| } | |
| }) | |
| .catch(function(error) { | |
| console.error('Erreur génération:', error); | |
| }); | |
| } | |
| }; | |
| // Gestionnaire de génération en lot | |
| var BulkGenerationManager = { | |
| init: function() { | |
| DOM.bulkGenerateBtn.addEventListener('click', function(event) { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| BulkGenerationManager.startBulkGeneration(); | |
| }); | |
| }, | |
| startBulkGeneration: function() { | |
| if (!AppState.apiKey) { | |
| Utils.showMessage('error', 'Veuillez configurer votre clé API Groq'); | |
| return; | |
| } | |
| if (AppState.actionPlanData.length === 0) { | |
| Utils.showMessage('error', 'Aucun plan d\'action chargé'); | |
| return; | |
| } | |
| var itemsToProcess = AppState.actionPlanData | |
| .map(function(item, index) { | |
| return { item: item, index: index }; | |
| }) | |
| .filter(function(obj) { | |
| return !AppState.recommendations[obj.index]; | |
| }); | |
| if (itemsToProcess.length === 0) { | |
| Utils.showMessage('success', 'Toutes les recommandations ont déjà été générées !'); | |
| return; | |
| } | |
| var delay = parseInt(DOM.delaySelect.value) * 1000; | |
| var estimatedTime = Math.round((itemsToProcess.length * (delay + 5000)) / 60000); | |
| var confirmed = confirm( | |
| 'Vous allez générer ' + itemsToProcess.length + ' recommandations.\n' + | |
| 'Délai entre chaque génération: ' + (delay/1000) + 's\n' + | |
| 'Temps estimé: ' + estimatedTime + ' minutes\n\n' + | |
| 'Continuer ?' | |
| ); | |
| if (!confirmed) return; | |
| AppState.bulkGeneration = { | |
| isRunning: true, | |
| currentIndex: 0, | |
| total: itemsToProcess.length, | |
| completed: 0, | |
| failed: [], | |
| itemsToProcess: itemsToProcess | |
| }; | |
| BulkGenerationManager.updateUI(true); | |
| BulkGenerationManager.processBulk(itemsToProcess, delay); | |
| }, | |
| processBulk: function(itemsToProcess, delay) { | |
| var processNext = function(i) { | |
| if (i >= itemsToProcess.length || !AppState.bulkGeneration.isRunning) { | |
| BulkGenerationManager.completeBulkGeneration(); | |
| return; | |
| } | |
| var obj = itemsToProcess[i]; | |
| AppState.bulkGeneration.currentIndex = i; | |
| BulkGenerationManager.updateProgress(); | |
| BulkGenerationManager.updateCurrentItem(obj.item["Numéro d'exigence"]); | |
| APIManager.generateRecommendation(obj.index) | |
| .then(function(recommendation) { | |
| if (recommendation) { | |
| AppState.bulkGeneration.completed++; | |
| } else { | |
| AppState.bulkGeneration.failed.push(obj.item["Numéro d'exigence"]); | |
| } | |
| }) | |
| .catch(function(error) { | |
| console.error('Erreur pour ' + obj.item["Numéro d'exigence"] + ':', error); | |
| AppState.bulkGeneration.failed.push(obj.item["Numéro d'exigence"]); | |
| }) | |
| .finally(function() { | |
| if (i < itemsToProcess.length - 1) { | |
| setTimeout(function() { | |
| processNext(i + 1); | |
| }, delay); | |
| } else { | |
| processNext(i + 1); | |
| } | |
| }); | |
| }; | |
| processNext(0); | |
| }, | |
| updateUI: function(isRunning) { | |
| DOM.bulkGenerateBtn.disabled = isRunning; | |
| DOM.bulkProgress.classList.toggle('show', isRunning); | |
| if (isRunning) { | |
| DOM.bulkGenerateBtn.innerHTML = '<span class="loading-spinner"></span> Génération en cours...'; | |
| } else { | |
| DOM.bulkGenerateBtn.innerHTML = '<span class="material-icons">auto_awesome</span> Générer toutes les recommandations'; | |
| } | |
| }, | |
| updateProgress: function() { | |
| var current = AppState.bulkGeneration.currentIndex; | |
| var total = AppState.bulkGeneration.total; | |
| var percentage = total > 0 ? ((current + 1) / total) * 100 : 0; | |
| DOM.progressBar.style.width = percentage + '%'; | |
| DOM.progressCounter.textContent = (current + 1) + ' / ' + total; | |
| }, | |
| updateCurrentItem: function(reqNumber) { | |
| DOM.currentItem.innerHTML = | |
| '<span class="loading-spinner"></span> Génération pour l\'exigence ' + reqNumber + '...'; | |
| }, | |
| completeBulkGeneration: function() { | |
| var completed = AppState.bulkGeneration.completed; | |
| var failed = AppState.bulkGeneration.failed; | |
| var total = AppState.bulkGeneration.total; | |
| AppState.bulkGeneration.isRunning = false; | |
| BulkGenerationManager.updateUI(false); | |
| var message = 'Génération terminée : ' + completed + '/' + total + ' recommandations générées'; | |
| if (failed.length > 0) { | |
| message += '\nÉchecs : ' + failed.join(', '); | |
| Utils.showMessage('error', message); | |
| } else { | |
| message += ' avec succès !'; | |
| Utils.showMessage('success', message); | |
| } | |
| DOM.currentItem.innerHTML = | |
| '<span class="material-icons" style="color: #34a853;">check_circle</span> Génération terminée !'; | |
| setTimeout(function() { | |
| DOM.bulkProgress.classList.remove('show'); | |
| }, 3000); | |
| } | |
| }; | |
| // Gestionnaire de modal | |
| var ModalManager = { | |
| init: function() { | |
| DOM.modalClose.addEventListener('click', ModalManager.close); | |
| DOM.modalCancel.addEventListener('click', ModalManager.close); | |
| DOM.modalGenerate.addEventListener('click', ModalManager.generateRecommendation); | |
| DOM.textareaToggle.addEventListener('click', ModalManager.toggleTextarea); | |
| DOM.detailModal.addEventListener('click', function(e) { | |
| if (e.target === DOM.detailModal) ModalManager.close(); | |
| }); | |
| }, | |
| toggleTextarea: function() { | |
| var isVisible = DOM.modalTextarea.style.display !== 'none'; | |
| if (isVisible) { | |
| DOM.modalTextarea.style.display = 'none'; | |
| DOM.textareaToggle.innerHTML = '<span class="material-icons">edit</span> Modifier'; | |
| } else { | |
| DOM.modalTextarea.style.display = 'block'; | |
| DOM.textareaToggle.innerHTML = '<span class="material-icons">close</span> Fermer'; | |
| DOM.modalTextarea.focus(); | |
| } | |
| }, | |
| open: function(index) { | |
| AppState.currentModalIndex = index; | |
| var item = AppState.actionPlanData[index]; | |
| DOM.modalReqNum.textContent = item["Numéro d'exigence"]; | |
| DOM.modalReqText.textContent = item["Exigence IFS Food 8"]; | |
| DOM.modalExplanation.textContent = item["Explication (par l'auditeur/l'évaluateur)"]; | |
| var recommendation = AppState.recommendations[index]; | |
| if (recommendation) { | |
| DOM.modalTextarea.value = recommendation; | |
| DOM.modalRenderedRecommendation.innerHTML = marked.parse(recommendation); | |
| } else { | |
| DOM.modalTextarea.value = ''; | |
| DOM.modalRenderedRecommendation.innerHTML = '<p>Aucune recommandation générée.</p>'; | |
| } | |
| DOM.modalTextarea.style.display = 'none'; | |
| DOM.textareaToggle.innerHTML = '<span class="material-icons">edit</span> Modifier'; | |
| DOM.detailModal.style.display = 'flex'; | |
| }, | |
| close: function() { | |
| DOM.detailModal.style.display = 'none'; | |
| AppState.currentModalIndex = -1; | |
| }, | |
| generateRecommendation: function() { | |
| if (AppState.currentModalIndex === -1) return; | |
| APIManager.generateRecommendation(AppState.currentModalIndex, DOM.modalTextarea.value.trim() !== '') | |
| .then(function(recommendation) { | |
| if (recommendation) { | |
| DOM.modalTextarea.value = recommendation; | |
| DOM.modalRenderedRecommendation.innerHTML = marked.parse(recommendation); | |
| Utils.showMessage('success', 'Recommandation générée/améliorée avec succès!'); | |
| } | |
| }) | |
| .catch(function(error) { | |
| console.error('Erreur génération modal:', error); | |
| }); | |
| } | |
| }; | |
| // Gestionnaire d'export PDF | |
| var PDFManager = { | |
| init: function() { | |
| DOM.exportPdfBtn.addEventListener('click', PDFManager.exportToPDF); | |
| }, | |
| exportToPDF: function() { | |
| var pdfContainer = document.createElement('div'); | |
| pdfContainer.style.fontFamily = 'Arial, sans-serif'; | |
| pdfContainer.style.fontSize = '11px'; | |
| pdfContainer.style.padding = '15px'; | |
| pdfContainer.style.background = 'white'; | |
| var title = document.createElement('h1'); | |
| title.style.textAlign = 'center'; | |
| title.style.color = '#004080'; | |
| title.style.marginBottom = '20px'; | |
| title.style.fontSize = '18px'; | |
| title.textContent = 'Rapport de Plan d\'Actions IFS avec Recommandations IA'; | |
| pdfContainer.appendChild(title); | |
| AppState.actionPlanData.forEach(function(item, index) { | |
| var recommendation = AppState.recommendations[index]; | |
| if (!recommendation) return; | |
| var section = document.createElement('div'); | |
| section.style.marginBottom = '25px'; | |
| section.style.border = '1px solid #e0e0e0'; | |
| section.style.borderRadius = '8px'; | |
| section.style.padding = '15px'; | |
| section.style.background = '#fafafa'; | |
| var header = document.createElement('div'); | |
| header.style.background = 'linear-gradient(135deg, #004080, #1a73e8)'; | |
| header.style.color = 'white'; | |
| header.style.padding = '10px 15px'; | |
| header.style.margin = '-15px -15px 15px -15px'; | |
| header.style.borderRadius = '7px 7px 0 0'; | |
| header.innerHTML = '<h2 style="margin: 0; font-size: 14px;">Exigence ' + | |
| item["Numéro d'exigence"] + ' - ' + item["Exigence IFS Food 8"] + '</h2>'; | |
| section.appendChild(header); | |
| var constatDiv = document.createElement('div'); | |
| constatDiv.style.marginBottom = '15px'; | |
| constatDiv.style.padding = '10px'; | |
| constatDiv.style.background = 'white'; | |
| constatDiv.style.borderLeft = '4px solid #ea4335'; | |
| constatDiv.innerHTML = '<strong style="color: #ea4335;">Constat:</strong><br>' + | |
| item["Explication (par l'auditeur/l'évaluateur)"]; | |
| section.appendChild(constatDiv); | |
| var recommendationDiv = document.createElement('div'); | |
| recommendationDiv.style.background = 'white'; | |
| recommendationDiv.style.padding = '15px'; | |
| recommendationDiv.style.borderRadius = '6px'; | |
| recommendationDiv.style.border = '1px solid #d0d0d0'; | |
| recommendationDiv.innerHTML = | |
| '<h3 style="color: #1a73e8; margin-top: 0; font-size: 12px;">Recommandation IA</h3>' + | |
| '<div style="font-size: 10px;">' + marked.parse(recommendation) + '</div>'; | |
| section.appendChild(recommendationDiv); | |
| pdfContainer.appendChild(section); | |
| }); | |
| var options = { | |
| margin: [10, 10, 10, 10], | |
| filename: 'Plan_Action_IFS_' + new Date().toISOString().split('T')[0] + '.pdf', | |
| image: { type: 'jpeg', quality: 0.95 }, | |
| html2canvas: { scale: 1.5, logging: false, useCORS: true }, | |
| jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } | |
| }; | |
| Utils.showMessage('success', 'Génération du PDF en cours...'); | |
| html2pdf().from(pdfContainer).set(options).save() | |
| .then(function() { | |
| Utils.showMessage('success', 'PDF généré et téléchargé avec succès!'); | |
| }) | |
| .catch(function(error) { | |
| console.error('Erreur export PDF:', error); | |
| Utils.showMessage('error', 'Erreur lors de la génération du PDF'); | |
| }); | |
| } | |
| }; | |
| // Gestionnaire UI | |
| var UIManager = { | |
| init: function() { | |
| DOM.apiKeyInput.addEventListener('input', function(e) { | |
| var key = e.target.value.trim(); | |
| if (key) { | |
| AppState.apiKey = key; | |
| Utils.showMessage('success', 'Clé API configurée avec succès'); | |
| } | |
| }); | |
| DOM.howToUseHeader.addEventListener('click', function() { | |
| var isExpanded = DOM.howToUseContent.style.display === 'block'; | |
| DOM.howToUseContent.style.display = isExpanded ? 'none' : 'block'; | |
| if (isExpanded) { | |
| DOM.howToUseHeader.classList.remove('expanded'); | |
| } else { | |
| DOM.howToUseHeader.classList.add('expanded'); | |
| } | |
| }); | |
| } | |
| }; | |
| // Initialisation | |
| document.addEventListener('DOMContentLoaded', function() { | |
| console.log('Initialisation Assistant VisiPilot IFS'); | |
| marked.setOptions({ | |
| gfm: true, | |
| breaks: true, | |
| sanitize: false | |
| }); | |
| UIManager.init(); | |
| FileManager.init(); | |
| ModalManager.init(); | |
| PDFManager.init(); | |
| BulkGenerationManager.init(); | |
| APIManager.loadGuideData(); | |
| console.log('Application initialisée'); | |
| }); | |
| </script> | |
| </body> | |
| </html> |