File size: 26,927 Bytes
a15a6e8 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 |
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PRD v4.0 (Final) - Plateforme d'Analyse de CV</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&family=Google+Sans:wght@400;500;700&display=swap');
:root {
--google-blue: #1a73e8;
--google-red: #ea4335;
--google-green: #34a853;
--google-yellow: #fbbc05;
--text-primary: #202124;
--text-secondary: #5f6368;
--bg-light: #f8f9fa;
--bg-medium: #e8f0fe;
--border-color: #dadce0;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Roboto', 'Google Sans', sans-serif;
display: flex;
background-color: #fff;
color: var(--text-primary);
line-height: 1.7;
}
.sidebar {
flex: 0 0 280px;
background-color: var(--bg-light);
border-right: 1px solid var(--border-color);
height: 100vh;
position: fixed;
top: 0;
left: 0;
padding: 24px;
overflow-y: auto;
}
.sidebar-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.sidebar-header .icon { font-size: 24px; }
.sidebar-header h2 { font-size: 18px; color: var(--google-blue); font-family: 'Google Sans', sans-serif; border: none; padding: 0; margin: 0; }
.sidebar nav ul { list-style: none; }
.sidebar nav li a {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-secondary);
text-decoration: none;
padding: 12px 15px;
border-radius: 20px;
margin-bottom: 8px;
font-weight: 500;
transition: background-color 0.2s ease, color 0.2s ease;
}
.sidebar nav li a:hover { background-color: #e9ecef; color: var(--text-primary); }
.sidebar nav li a.active { background-color: var(--bg-medium); color: var(--google-blue); font-weight: 700; }
.main-content { margin-left: 280px; flex-grow: 1; padding: 32px 64px; }
section { padding-top: 60px; margin-bottom: 40px; scroll-margin-top: 60px; }
#header { padding-top: 0; }
h1 { font-family: 'Google Sans', sans-serif; font-size: 32px; margin-bottom: 8px; font-weight: 700; }
h1 + p { color: var(--text-secondary); font-size: 18px; margin-bottom: 24px; }
h2 { font-family: 'Google Sans', sans-serif; font-size: 24px; font-weight: 500; color: var(--text-primary); border-bottom: 2px solid var(--google-blue); padding-bottom: 8px; margin-bottom: 24px; display: flex; align-items: center; gap: 10px; }
h3 { font-family: 'Google Sans', sans-serif; font-size: 20px; margin-top: 24px; margin-bottom: 12px; color: var(--text-primary); }
h4 { font-family: 'Roboto', sans-serif; font-size: 18px; font-weight: 500; color: var(--text-primary); margin-top: 16px; display: flex; align-items: center; gap: 8px; }
blockquote { border-left: 4px solid var(--google-yellow); padding: 16px 24px; margin: 24px 0; background-color: #fffbe6; font-size: 16px; font-style: italic; font-weight: 400; border-radius: 0 8px 8px 0; }
table { width: 100%; border-collapse: collapse; margin-top: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); border: 1px solid var(--border-color); border-radius: 8px; overflow: hidden; }
th, td { text-align: left; padding: 16px; border-bottom: 1px solid var(--border-color); }
th { background-color: var(--bg-light); font-weight: 500; font-size: 14px; text-transform: uppercase; color: var(--text-secondary); }
tr:last-child td { border-bottom: none; }
td ul { padding-left: 20px; margin-top: 5px; }
.collapsible { background-color: #fff; color: var(--text-primary); cursor: pointer; padding: 18px; width: 100%; border: 1px solid var(--border-color); text-align: left; outline: none; font-size: 18px; font-weight: 500; transition: background-color 0.2s; margin-top: 16px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; font-family: 'Google Sans', sans-serif; }
.collapsible:hover, .collapsible.active { background-color: var(--bg-light); border-color: var(--google-blue); }
.collapsible::after { content: '▼'; color: var(--google-blue); font-weight: bold; transition: transform 0.2s ease-in-out; }
.collapsible.active::after { transform: rotate(-180deg); }
.content { padding: 0 18px; max-height: 0; overflow: hidden; transition: max-height 0.4s ease-out; background-color: white; border: 1px solid var(--border-color); border-top: none; border-radius: 0 0 8px 8px; }
.content > * { margin-top: 24px; margin-bottom: 24px; }
.new-feature-badge { background-color: var(--google-yellow); color: var(--text-primary); font-size: 12px; font-weight: 700; padding: 4px 10px; border-radius: 12px; margin-left: 10px; }
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
margin-top: 24px;
}
.grid-item {
background-color: var(--bg-light);
padding: 24px;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.grid-item ul {
list-style-type: none;
padding: 0;
margin-top: 10px;
}
.grid-item li {
margin-bottom: 10px;
}
footer { margin-top: 60px; padding-top: 24px; border-top: 1px solid var(--border-color); font-size: 14px; color: var(--text-secondary); text-align: center; }
</style>
</head>
<body>
<aside class="sidebar">
<div class="sidebar-header">
<span class="icon">📄</span>
<h2>Analyse de CV</h2>
</div>
<nav>
<ul>
<li><a href="#header" class="active">🏠 Mission & Guide</a></li>
<li><a href="#portee">⚙️ Portée Fonctionnelle</a></li>
<li><a href="#architecture">🛠️ Architecture Technique</a></li>
<li><a href="#non-fonctionnel">🛡️ Exigences Non-Fonctionnelles</a></li>
<li><a href="#roadmap">🗺️ Roadmap</a></li>
<li><a href="#kpis">📈 KPIs & Succès</a></li>
</ul>
</nav>
</aside>
<main class="main-content">
<section id="header">
<h1>Plateforme d’Analyse Automatisée des CV Auditeurs</h1>
<p>PRD Version 4.0 (Final) – Mis à jour le 28 juillet 2025</p>
<h2>1. Mission de l'Outil & Guide d'Utilisation</h2>
<p>La mission de cet outil est de fournir aux directeurs de site un moyen **simple, rapide et standardisé** pour effectuer une première évaluation de CV. Il vise à traduire les exigences complexes des référentiels en un rapport clair et actionnable, facilitant la communication avec le siège et accélérant le processus de recrutement.</p>
<h3>Comment utiliser cet outil ?</h3>
<div class="grid-container">
<div class="grid-item">
<h4><span style="color:var(--google-blue);">🔧</span> Configuration</h4>
<ul>
<li><strong>Clé API :</strong> Saisissez votre clé API Groq personnelle dans le panneau latéral.</li>
<li><strong>Modèle d'IA :</strong> Sélectionnez le moteur d'IA à utiliser en fonction du besoin (précision vs. vitesse).</li>
</ul>
</div>
<div class="grid-item">
<h4><span style="color:var(--google-red);">🔍</span> Analyse</h4>
<ul>
<li><strong>Téléchargement :</strong> Uploadez un CV au format PDF.</li>
<li><strong>Sélection :</strong> Choisissez le référentiel GFSI applicable pour ce candidat.</li>
<li><strong>Lancement :</strong> Cliquez sur le bouton "Analyser le CV".</li>
</ul>
</div>
<div class="grid-item">
<h4><span style="color:var(--google-green);">📊</span> Résultats</h4>
<ul>
<li><strong>Consultation :</strong> Visualisez le rapport détaillé directement dans l'interface.</li>
<li><strong>Exploration :</strong> Naviguez par section (Qualifications, Expérience, etc.) pour voir les détails.</li>
<li><strong>Export :</strong> Téléchargez un rapport complet et professionnel au format HTML.</li>
</ul>
</div>
</div>
<blockquote><strong>Astuce :</strong> Plus le document est clairement formaté (sans images superflues, avec des titres de section clairs), meilleure et plus rapide sera l'analyse de l'IA.</blockquote>
</section>
<section id="portee">
<h2><span style="color: var(--google-blue);">⚙️</span>Portée Fonctionnelle Détaillée</h2>
<p>La plateforme s'articule autour de quatre modules interdépendants, formant un écosystème complet.</p>
<button type="button" class="collapsible">Module 1 : Interface Utilisateur & Expérience</button>
<div class="content">
<h4>1.1. Barre Latérale de Configuration Dynamique</h4>
<ul>
<li><strong>Gestion de l'API :</strong> Champ de saisie sécurisé (type `password`) pour la clé API Groq. La clé est stockée dans l'état de la session pour la durée d'utilisation, elle n'est jamais sauvegardée en clair.</li>
<li><strong>Sélecteur de Modèle d'IA :</strong> Menu déroulant permettant de choisir entre plusieurs modèles d'IA (ex: `Llama 3.3 70B` pour la précision, `Llama 3.1 8B` pour la vitesse), offrant un contrôle direct sur le compromis performance/coût.</li>
<li><strong>Options Techniques :</strong> Case à cocher pour un "Mode Debug" qui affiche les charges utiles JSON brutes envoyées et reçues de l'API, essentiel pour le dépannage.</li>
</ul>
<h4>1.2. Panneau d'Analyse Principal</h4>
<ul>
<li><strong>Upload de Fichiers :</strong> Zone de téléversement claire pour les CV au format PDF, avec des aides contextuelles sur les formats supportés et la taille maximale.</li>
<li><strong>Sélecteur de Référentiel :</strong> Menu déroulant peuplé dynamiquement à partir des fichiers de configuration JSON présents sur le serveur.</li>
<li><strong>Internationalisation (I18N) <span class="new-feature-badge">Nouveau</span> :</strong> L'ensemble des textes de l'interface est géré via un dictionnaire de traduction (FR/EN), permettant une utilisation fluide dans un contexte international.</li>
</ul>
</div>
<button type="button" class="collapsible">Module 2 : Moteur d'Analyse à Double Logique</button>
<div class="content">
<p>Le cœur de la plateforme est capable d'interpréter les référentiels de deux manières distinctes pour allier précision moderne et compatibilité avec l'existant.</p>
<h4>2.1. Analyse Granulaire (Logique par défaut)</h4>
<p>Ce mode utilise un "mega-prompt" sophistiqué pour forcer l'IA à agir comme un expert en conformité méticuleux.</p>
<ul>
<li><strong>Instruction de Rôle :</strong> Le prompt assigne à l'IA le rôle d' "Expert en conformité GFSI".</li>
<li><strong>Format de Sortie Imposé :</strong> La réponse doit obligatoirement être un JSON structuré, minimisant les hallucinations et facilitant le parsing.</li>
<li><strong>Analyse Exigence par Exigence :</strong> Le prompt liste chaque exigence du référentiel sélectionné, forçant l'IA à fournir une évaluation pour chacune.</li>
<li><strong>Exigence de Justification :</strong> Pour chaque évaluation, l'IA est instruite de trouver et de **citer textuellement** les passages pertinents du CV (<code>candidate_evidence</code>).</li>
<li><strong>Auto-Évaluation de l'IA :</strong> Le modèle doit fournir un <code>confidence_score</code> (de 0.0 à 1.0) sur sa propre évaluation, permettant de mesurer la fiabilité de chaque point.</li>
</ul>
<h4>2.2. Analyse Traditionnelle (Mode de Compatibilité)</h4>
<p>Pour les anciens fichiers de référentiels ne suivant pas la nouvelle structure, un prompt plus simple est utilisé, assurant que l'application ne tombe pas en panne et peut continuer à analyser ces cas de figure.</p>
</div>
<button type="button" class="collapsible">Module 3 : Reporting et Visualisation des Données</button>
<div class="content">
<h4>3.1. Affichage à l'Écran (Post-Analyse)</h4>
<ul>
<li><strong>Tableau de Bord Synthétique :</strong> Affichage en haut de page de métriques clés (Total, Conformes, Partiels, Non-conformes) et d'un taux de conformité global.</li>
<li><strong>Verdict Visuel :</strong> Une boîte de recommandation finale (ex: "RECOMMANDÉ") utilise des couleurs vives (vert, jaune, rouge) pour un impact immédiat.</li>
<li><strong>Analyse Qualitative :</strong> Trois colonnes distinctes pour les "Forces Principales", les "Lacunes Critiques" et les "Opportunités de Développement".</li>
<li><strong>Exploration Détaillée :</strong> Les exigences sont regroupées par catégories dans des onglets. Au sein de chaque onglet, chaque exigence est présentée dans une boîte extensible ("expander"), permettant à l'utilisateur de se concentrer sur les points d'intérêt.</li>
</ul>
<h4>3.2. Rapport Exportable</h4>
<ul>
<li><strong>Génération HTML :</strong> Création d'un fichier HTML complet, autonome et stylisé, pouvant être facilement partagé, imprimé ou archivé.</li>
<li><strong>Branding et Clarté :</strong> Le rapport inclut un en-tête professionnel, la date de l'analyse, et une mise en page claire utilisant des tableaux et des codes couleur pour une lisibilité maximale.</li>
</ul>
</div>
<button type="button" class="collapsible">Module 4 : Administration et Gestion de Contenu <span class="new-feature-badge">Mise à jour majeure</span></button>
<div class="content">
<p>Cette section, protégée par mot de passe, transforme l'application d'un outil statique à une plateforme de connaissance vivante.</p>
<h4>4.1. Assistant de Création de Référentiels via IA</h4>
<p>C'est la fonctionnalité la plus innovante, permettant une autonomie complète du siège :</p>
<ol>
<li>L'administrateur **colle le texte brut** d'un nouveau standard dans une zone de texte.</li>
<li>Il clique sur <strong>"Générer le JSON"</strong>.</li>
<li>L'application envoie ce texte à l'API Groq avec un prompt spécifique lui demandant de **structurer l'information** selon le format JSON requis par l'application (avec catégories, poids, exigences critiques, etc.).</li>
<li>L'IA retourne un objet JSON propre et formaté.</li>
</ol>
<h4>4.2. Système de Gestion de Contenu (CMS) Simplifié</h4>
<ul>
<li><strong>Prévisualisation :</strong> Le JSON généré est affiché à l'écran pour validation par l'administrateur.</li>
<li><strong>Sauvegarde en un Clic :</strong> L'administrateur nomme le fichier et clique sur "Sauvegarder". Le fichier est alors écrit sur le disque du serveur.</li>
<li><strong>Rechargement à la Volée :</strong> L'application est conçue pour recharger dynamiquement la liste des référentiels disponibles, rendant le nouveau standard immédiatement utilisable sans redémarrage ni intervention technique.</li>
</ul>
</div>
</section>
<section id="architecture">
<h2><span style="color: var(--google-red);">🛠️</span>Architecture Technique et Options d'Évolution</h2>
<h3>Architecture Actuelle (MVP Streamlit)</h3>
<p>L'application est un monolithe intelligent basé sur Streamlit qui gère à la fois le frontend et le backend.</p>
<pre style="background-color: var(--bg-light); padding: 15px; border-radius: 8px; font-size: 14px;"><code>
Utilisateur (Directeur/Admin)
|
|-- Navigateur Web
|
[ Serveur / Machine Virtuelle ]
|
|-- Application Streamlit (Python)
| |
| |-- 1. Gestion UI & Session State
| |
| |-- 2. Logique Métier (extract_text, build_prompt)
| | |
| | '-----> [ API Externe : Groq Cloud ]
| | |-- LLM (Llama, etc.)
| |
| '-- 3. Accès Fichiers Locaux
| |
| '-----> /referentiels/*.json (Lecture/Écriture)
</code></pre>
<h3>Options d'Évolution pour la Scalabilité</h3>
<table>
<thead>
<tr><th>Option</th><th>Description de la Stack</th><th>Avantages</th><th>Inconvénients</th><th>Cas d'usage Idéal</th></tr>
</thead>
<tbody>
<tr>
<td><strong>A. MVP Actuel</strong><br>(Streamlit)</td>
<td>Monolithe Python avec Streamlit.</td>
<td><ul><li>Vitesse de développement imbattable</li><li>Écosystème unique (Python)</li><li>Faible coût initial</li></ul></td>
<td><ul><li>Scalabilité horizontale limitée</li><li>Personnalisation de l'UI contrainte</li><li>Gestion fine des droits complexe</li></ul></td>
<td>Proof of Concept, déploiement interne rapide pour < 50 utilisateurs.</td>
</tr>
<tr>
<td><strong>B. Scale-Up API-First</strong><br>(Découplé)</td>
<td>Backend: <strong>FastAPI/Flask</strong> (Python)<br>Frontend: <strong>Vue.js/React</strong> (JavaScript)</td>
<td><ul><li>API REST robuste et réutilisable</li><li>Expérience utilisateur riche et sur-mesure</li><li>Scalabilité horizontale claire</li><li>Équipes spécialisées (front/back)</li></ul></td>
<td><ul><li>Développement plus long et complexe</li><li>Nécessite des compétences multiples (Py/JS)</li><li>Déploiement en deux parties</li></ul></td>
<td>Besoin d'intégrations tierces (SIRH), application mobile future, UI complexe, > 50 utilisateurs.</td>
</tr>
<tr>
<td><strong>C. Enterprise "Tout-en-Un"</strong><br>(Django)</td>
<td>Framework <strong>Django</strong> (Python) avec son système de templates et son ORM.</td>
<td><ul><li>Sécurité et robustesse éprouvées</li><li>Interface d'admin générée automatiquement</li><li>ORM puissant pour la BDD</li><li>"Piles incluses" (authentification, etc.)</li></ul></td>
<td><ul><li>Plus rigide et "cadré"</li><li>Le frontend est moins dynamique qu'avec un framework JS dédié</li><li>Plus lourd pour une application simple</li></ul></td>
<td>L'application devient un système de gestion central avec des règles métier complexes et une forte dépendance à une base de données relationnelle.</td>
</tr>
</tbody>
</table>
</section>
<section id="non-fonctionnel">
<h2><span style="color: var(--google-yellow);">🛡️</span>Exigences Non-Fonctionnelles</h2>
<table>
<thead><tr><th>Catégorie</th><th>Spécification Détaillée (déduite du code v25.12)</th></tr></thead>
<tbody>
<tr><td><strong>Performance</strong></td><td>Temps de réponse interactif grâce à des spinners Streamlit. L'analyse principale, dépendante de l'API, doit viser un retour en moins de 30 secondes pour une expérience fluide.</td></tr>
<tr><td><strong>Sécurité</strong></td><td><strong>Confidentialité API :</strong> Utilisation de `st.session_state` et input de type `password` pour la clé. <strong>Confidentialité Données :</strong> Instruction de l'IA via le prompt pour l'anonymisation. <strong>Contrôle d'accès :</strong> Section Admin protégée par un mot de passe non-stocké.</td></tr>
<tr><td><strong>Maintenabilité</strong></td><td>Code modulaire avec des fonctions dédiées (extraction, analyse, affichage, rapport). La logique des référentiels est externalisée dans des fichiers JSON, permettant des mises à jour sans toucher au code source.</td></tr>
<tr><td><strong>Internationalisation (I18N)</strong></td><td>Présence d'un dictionnaire `TRANSLATIONS` pour le support du Français et de l'Anglais, rendant l'application prête pour un déploiement global.</td></tr>
<tr><td><strong>Robustesse & Gestion d'Erreurs</strong></td><td>Le code gère explicitement : les PDF invalides (vides, protégés), les clés API incorrectes, les erreurs de connexion à l'API Groq, et les échecs de parsing JSON de la réponse, affichant des messages d'erreur clairs à l'utilisateur.</td></tr>
<tr><td><strong>Configurabilité & Extensibilité</strong></td><td>L'application est hautement configurable : l'utilisateur choisit le modèle d'IA. Elle est extensible : les administrateurs peuvent ajouter de nouveaux référentiels de manière autonome via l'interface, sans intervention de l'équipe de développement.</td></tr>
</tbody>
</table>
</section>
<section id="roadmap">
<h2><span style="color: var(--google-green);">🗺️</span>Roadmap d'Évolution</h2>
<table>
<thead>
<tr><th>Phase</th><th>Durée</th><th>Livrables Clés</th></tr>
</thead>
<tbody>
<tr>
<td><strong>1 – MVP Avancé</strong><br>(Statut : Terminé)</td>
<td>0-3 mois</td>
<td>Workflow complet d'analyse, support PDF, moteur d'analyse granulaire, rapport HTML, <strong>mode admin avec assistant IA, support multilingue (FR/EN)</strong>.</td>
</tr>
<tr>
<td><strong>2 – Améliorations</strong><br>(À venir)</td>
<td>4-9 mois</td>
<td>Tableau de bord de statistiques pour les admins, support des formats <code>.docx</code> et <code>.txt</code>, fonctionnalité d'analyse comparative (1 CV vs plusieurs référentiels), historisation des analyses dans une base de données.</td>
</tr>
<tr>
<td><strong>3 – Évolution</strong><br>(Vision à long terme)</td>
<td>10-18 mois</td>
<td>Mise en place d'une API REST pour intégration avec des SIRH, module de suivi du plan de formation pour les candidats recrutés, gestion avancée des droits et des équipes.</td>
</tr>
</tbody>
</table>
</section>
<section id="kpis">
<h2><span style="color: var(--google-blue);">📈</span>KPIs & Critères de Succès</h2>
<ol>
<li><strong>Taux d'Adoption Utilisateur :</strong> Pourcentage de directeurs de site utilisant activement l'outil chaque mois. <strong>Cible : > 80%</strong>.</li>
<li><strong>Efficacité du Processus :</strong> Réduction du temps moyen entre la réception d'un CV et la décision de pré-qualification par le siège. <strong>Cible : -75%</strong>.</li>
<li><strong>Fiabilité de l'Analyse :</strong> Pourcentage de rapports générés jugés "fiables et utiles" (ne nécessitant pas de reprise manuelle complète) par les experts du siège. <strong>Cible : > 90%</strong>.</li>
<li><strong>Satisfaction Utilisateur (NPS) :</strong> Score Net Promoter Score collecté auprès des directeurs de site et des administrateurs. <strong>Cible : > +40</strong>.</li>
</ol>
</section>
<footer>
<p>Fin du document PRD v4.0 (Final)</p>
</footer>
</main>
<script>
const collapsibles = document.querySelectorAll(".collapsible");
collapsibles.forEach(item => {
item.addEventListener("click", function() {
this.classList.toggle("active");
const content = this.nextElementSibling;
if (content.style.maxHeight) {
content.style.maxHeight = null;
} else {
content.style.maxHeight = content.scrollHeight + 40 + "px";
}
});
});
const sections = document.querySelectorAll("section");
const navLinks = document.querySelectorAll(".sidebar nav a");
window.addEventListener("scroll", () => {
let current = "";
sections.forEach(section => {
const sectionTop = section.offsetTop;
if (pageYOffset >= sectionTop - 70) {
current = section.getAttribute("id");
}
});
navLinks.forEach(a => {
a.classList.remove("active");
if (a.getAttribute("href") === "#" + current) {
a.classList.add("active");
}
});
});
</script>
</body>
</html> |