Connexion · Créer un compte

SKYNET

par henoc Gnaoule · mis à jour il y a 2 h

Skynet est un projet qui pour but de developper une Intelligence artificielle générale

JavaScript Licence MIT 13 fichiers

360 lignes · 14,9 Ko

/* ==========================================================================
   Panneau CV — Eliora
   Quand l'utilisateur choisit son CV dans un champ <input type="file"> de
   la page (ici #ecv-fichier, le champ de eliora_cv_widget.php), le fichier
   s'ouvre dans un panneau à droite de la conversation.
   Formats affichés : PDF, Word (.docx), texte (.txt), PNG et JPG.
   L'envoi du CV vers eliora_cv.php n'est pas modifié : ce script se
   contente d'afficher le fichier, il n'envoie rien.

   Appel direct possible depuis un autre script :
       ElioraCV.ouvrir(fichier);   // fichier = objet File
       ElioraCV.fermer();

   Styles : css/cv_panneau.css
   ========================================================================== */
(function () {
	'use strict';

	var CONFIG = {
		// Lecteurs chargés seulement au premier CV ouvert. Pour ne pas dépendre
		// des CDN, copier ces fichiers dans js/ et remplacer les adresses.
		pdfJs: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js',
		pdfWorker: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js',
		mammoth: 'https://cdn.jsdelivr.net/npm/mammoth@1.6.0/mammoth.browser.min.js', // lecteur Word
		tailleMax: 3 * 1024 * 1024,  // même limite que le widget (3 Mo) : au-delà, le panneau ne s'ouvre pas
		pagesMax: 20,                // nombre maximal de pages PDF affichées
		largeurMin: 360,             // largeur minimale du panneau (px)
		conversationMin: 480,        // largeur minimale laissée à la conversation (px)
		seuilPleinEcran: 900         // sous cette largeur de fenêtre, le panneau prend tout l'écran
	};
	var ZOOMS = [0.5, 0.75, 1, 1.25, 1.5, 2];
	var CLE_LARGEUR = 'eliora-cv-largeur';

	var panneau, scene, pages, note, nomEl, metaEl, zoomEl, btnMoins, btnPlus, btnFermer, poignee, onglet;
	var courant = null;      // document affiché
	var indexZoom = 2;
	var largeurVoulue = null;
	var scriptsCharges = {};

	/* ---------- Construction du panneau ---------- */
	function construire() {
		panneau = document.createElement('div');
		panneau.id = 'cv-panneau';
		panneau.setAttribute('role', 'complementary');
		panneau.setAttribute('aria-label', 'Aperçu du CV');
		panneau.innerHTML =
			'<div id="cv-poignee" role="separator" aria-orientation="vertical" aria-label="Largeur du panneau CV" tabindex="0"></div>' +
			'<div id="cv-entete">' +
				'<div id="cv-infos"><span id="cv-nom"></span><span id="cv-meta"></span></div>' +
				'<div id="cv-outils">' +
					'<button type="button" class="cv-bouton" id="cv-moins" aria-label="Réduire">&minus;</button>' +
					'<span id="cv-zoom" aria-live="polite">100 %</span>' +
					'<button type="button" class="cv-bouton" id="cv-plus" aria-label="Agrandir">+</button>' +
					'<button type="button" class="cv-bouton" id="cv-fermer" aria-label="Fermer l\'aperçu du CV">&times;</button>' +
				'</div>' +
			'</div>' +
			'<div id="cv-scene"><p id="cv-note"></p><div id="cv-pages"></div></div>';
		document.body.appendChild(panneau);

		onglet = document.createElement('button');
		onglet.type = 'button';
		onglet.id = 'cv-onglet';
		onglet.innerHTML =
			'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="18" height="18" aria-hidden="true">' +
			'<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"></path><path d="M14 3v5h5M9 13h6M9 17h4"></path></svg>' +
			'Voir mon CV';
		document.body.appendChild(onglet);

		scene = document.getElementById('cv-scene');
		pages = document.getElementById('cv-pages');
		note = document.getElementById('cv-note');
		nomEl = document.getElementById('cv-nom');
		metaEl = document.getElementById('cv-meta');
		zoomEl = document.getElementById('cv-zoom');
		btnMoins = document.getElementById('cv-moins');
		btnPlus = document.getElementById('cv-plus');
		btnFermer = document.getElementById('cv-fermer');
		poignee = document.getElementById('cv-poignee');

		btnFermer.addEventListener('click', fermer);
		onglet.addEventListener('click', function () { montrer(); btnFermer.focus(); });
		btnMoins.addEventListener('click', function () { if (indexZoom > 0) { indexZoom--; appliquerZoom(); } });
		btnPlus.addEventListener('click', function () { if (indexZoom < ZOOMS.length - 1) { indexZoom++; appliquerZoom(); } });
		document.addEventListener('keydown', function (e) {
			if (e.key === 'Escape' && document.body.classList.contains('cv-ouvert')) { fermer(); }
		});
		brancherPoignee();
		window.addEventListener('resize', ajusterLargeur);

		try {
			var memo = parseInt(localStorage.getItem(CLE_LARGEUR), 10);
			if (memo > 0) { largeurVoulue = memo; }
		} catch (e) {}
		appliquerZoom();
		ajusterLargeur();
	}

	/* ---------- Ouverture / fermeture ---------- */
	function montrer() { document.body.classList.add('cv-ouvert'); ajusterLargeur(); }
	function fermer() {
		if (!document.body.classList.contains('cv-ouvert')) { return; }
		document.body.classList.remove('cv-ouvert');
		if (courant) { onglet.focus(); }
	}
	function ecrireNote(texte) { note.textContent = texte || ''; note.style.display = texte ? 'block' : 'none'; }

	function appliquerZoom() {
		var z = ZOOMS[indexZoom];
		pages.style.setProperty('--cv-zoom', z);
		zoomEl.textContent = Math.round(z * 100) + ' %';
		btnMoins.disabled = indexZoom === 0;
		btnPlus.disabled = indexZoom === ZOOMS.length - 1;
	}

	/* ---------- Largeur du panneau ---------- */
	function ajusterLargeur() {
		var fenetre = window.innerWidth;
		var plein = fenetre < CONFIG.seuilPleinEcran;
		document.body.classList.toggle('cv-plein', plein);
		if (plein) { return; }
		var voulue = largeurVoulue || Math.round(fenetre * 0.46);
		var max = Math.max(CONFIG.largeurMin, fenetre - CONFIG.conversationMin);
		var w = Math.max(CONFIG.largeurMin, Math.min(voulue, max));
		document.documentElement.style.setProperty('--cv-w', w + 'px');
		poignee.setAttribute('aria-valuenow', Math.round(w / fenetre * 100));
	}
	function memoriserLargeur() { try { localStorage.setItem(CLE_LARGEUR, String(largeurVoulue)); } catch (e) {} }

	function brancherPoignee() {
		var actif = false;
		poignee.addEventListener('pointerdown', function (e) {
			actif = true; poignee.setPointerCapture(e.pointerId);
			document.body.classList.add('cv-redim'); e.preventDefault();
		});
		poignee.addEventListener('pointermove', function (e) {
			if (!actif) { return; }
			largeurVoulue = Math.round(window.innerWidth - e.clientX);
			ajusterLargeur();
		});
		function fin() {
			if (!actif) { return; }
			actif = false; document.body.classList.remove('cv-redim'); memoriserLargeur();
		}
		poignee.addEventListener('pointerup', fin);
		poignee.addEventListener('pointercancel', fin);
		poignee.addEventListener('keydown', function (e) {
			if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') { return; }
			e.preventDefault();
			largeurVoulue = Math.round(panneau.getBoundingClientRect().width + (e.key === 'ArrowLeft' ? 32 : -32));
			ajusterLargeur(); memoriserLargeur();
		});
	}

	/* ---------- Lecture du fichier ---------- */
	function typeDe(fichier) {
		var n = (fichier.name || '').toLowerCase();
		if (fichier.type === 'application/pdf' || /\.pdf$/.test(n)) { return 'pdf'; }
		if (/\.docx$/.test(n)) { return 'docx'; }
		if (/\.txt$/.test(n)) { return 'texte'; }
		if (fichier.type === 'image/png' || fichier.type === 'image/jpeg' || /\.(png|jpe?g)$/.test(n)) { return 'image'; }
		return null;
	}
	function taille(octets) {
		if (octets < 1024 * 1024) { return Math.max(1, Math.round(octets / 1024)) + ' ko'; }
		return (octets / (1024 * 1024)).toFixed(1).replace('.', ',') + ' Mo';
	}

	// Charge un script externe une seule fois et renvoie l'objet global qu'il définit.
	function chargerScript(adresse, nomGlobal) {
		if (window[nomGlobal]) { return Promise.resolve(window[nomGlobal]); }
		if (!scriptsCharges[adresse]) {
			scriptsCharges[adresse] = new Promise(function (ok, echec) {
				var s = document.createElement('script');
				s.src = adresse;
				s.onload = function () {
					if (window[nomGlobal]) { ok(window[nomGlobal]); }
					else { delete scriptsCharges[adresse]; echec(new Error('LECTEUR')); }
				};
				s.onerror = function () { delete scriptsCharges[adresse]; echec(new Error('LECTEUR')); };
				document.head.appendChild(s);
			});
		}
		return scriptsCharges[adresse];
	}
	function chargerPdfJs() {
		return chargerScript(CONFIG.pdfJs, 'pdfjsLib').then(function (pdfjs) {
			pdfjs.GlobalWorkerOptions.workerSrc = pdfjs.GlobalWorkerOptions.workerSrc || CONFIG.pdfWorker;
			return pdfjs;
		});
	}

	function ajouterPage(doc, noeud) {
		if (courant !== doc) { return; } // un autre CV a été choisi entre-temps
		pages.appendChild(noeud);
	}

	function afficherPdf(fichier, doc) {
		return chargerPdfJs().then(function (pdfjs) {
			return fichier.arrayBuffer().then(function (tampon) {
				return pdfjs.getDocument({ data: new Uint8Array(tampon), isEvalSupported: false }).promise;
			});
		}).then(function (pdf) {
			var total = pdf.numPages, affichees = Math.min(total, CONFIG.pagesMax);
			var dpr = Math.min(window.devicePixelRatio || 1, 2);
			var cible = Math.min(1800, Math.max(1000, (scene.clientWidth || 700) * dpr * 1.25));
			doc.pages = total; doc.tronque = total > affichees;
			var suite = Promise.resolve();
			for (var i = 1; i <= affichees; i++) {
				(function (numero) {
					suite = suite.then(function () {
						if (courant !== doc) { return null; }
						return pdf.getPage(numero).then(function (page) {
							var base = page.getViewport({ scale: 1 });
							var vue = page.getViewport({ scale: cible / base.width });
							var c = document.createElement('canvas');
							c.className = 'cv-page';
							c.width = Math.round(vue.width); c.height = Math.round(vue.height);
							c.setAttribute('role', 'img');
							c.setAttribute('aria-label', 'Page ' + numero + ' sur ' + total);
							return page.render({ canvasContext: c.getContext('2d'), viewport: vue }).promise.then(function () {
								ajouterPage(doc, c);
							});
						});
					});
				})(i);
			}
			return suite;
		});
	}

	function afficherImage(fichier, doc) {
		return new Promise(function (ok, echec) {
			var img = new Image();
			img.className = 'cv-page';
			img.alt = 'CV : ' + fichier.name;
			img.onload = function () { doc.pages = 1; ajouterPage(doc, img); ok(); };
			img.onerror = function () { echec(new Error('IMAGE')); };
			img.src = URL.createObjectURL(fichier);
		});
	}

	function feuille() {
		var f = document.createElement('div');
		f.className = 'cv-page cv-feuille';
		return f;
	}

	function afficherTexte(fichier, doc) {
		return fichier.text().then(function (texte) {
			var f = feuille();
			f.classList.add('cv-texte');
			f.textContent = texte;
			ajouterPage(doc, f);
		});
	}

	// Le HTML produit à partir du fichier Word n'est pas inséré tel quel : on ne garde
	// que des balises de mise en forme, sans attribut, sans lien et sans script.
	var BALISES_OK = { P: 1, H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1, UL: 1, OL: 1, LI: 1, STRONG: 1, EM: 1, B: 1, I: 1, U: 1, S: 1,
		SUP: 1, SUB: 1, BR: 1, TABLE: 1, THEAD: 1, TBODY: 1, TR: 1, TD: 1, TH: 1, IMG: 1, SPAN: 1, DIV: 1 };
	function nettoyer(source, cible) {
		for (var i = 0; i < source.childNodes.length; i++) {
			var n = source.childNodes[i];
			if (n.nodeType === 3) { cible.appendChild(document.createTextNode(n.nodeValue)); continue; }
			if (n.nodeType !== 1) { continue; }
			var nom = n.tagName.toUpperCase();
			if (nom === 'SCRIPT' || nom === 'STYLE' || nom === 'IFRAME' || nom === 'OBJECT' || nom === 'EMBED') { continue; }
			if (!BALISES_OK[nom]) { nettoyer(n, cible); continue; } // balise inconnue (lien, etc.) : on garde seulement son texte
			var copie = document.createElement(nom);
			if (nom === 'IMG') {
				var src = n.getAttribute('src') || '';
				if (!/^data:image\/(png|jpe?g|gif);base64,/i.test(src)) { continue; }
				copie.setAttribute('src', src);
				copie.setAttribute('alt', '');
			}
			if (nom === 'TD' || nom === 'TH') {
				var c = parseInt(n.getAttribute('colspan'), 10), r = parseInt(n.getAttribute('rowspan'), 10);
				if (c > 1) { copie.setAttribute('colspan', c); }
				if (r > 1) { copie.setAttribute('rowspan', r); }
			}
			nettoyer(n, copie);
			cible.appendChild(copie);
		}
	}

	function afficherDocx(fichier, doc) {
		return chargerScript(CONFIG.mammoth, 'mammoth').then(function (mammoth) {
			return fichier.arrayBuffer().then(function (tampon) {
				return mammoth.convertToHtml({ arrayBuffer: tampon });
			});
		}).then(function (resultat) {
			var brut = new DOMParser().parseFromString('<div>' + (resultat.value || '') + '</div>', 'text/html');
			var f = feuille();
			nettoyer(brut.body, f);
			ajouterPage(doc, f);
		});
	}

	function messageErreur(err) {
		if (err && err.message === 'LECTEUR') { return 'Le lecteur de documents ne s\'est pas chargé. Vérifiez votre connexion puis choisissez à nouveau votre CV.'; }
		if (err && err.name === 'PasswordException') { return 'Ce PDF est protégé par un mot de passe : l\'aperçu n\'est pas possible.'; }
		return 'L\'aperçu de ce fichier n\'est pas possible : il est illisible ou endommagé.';
	}

	/* ---------- Point d'entrée ---------- */
	function ouvrir(fichier) {
		if (!fichier) { return false; }
		var type = typeDe(fichier);
		if (!type) { return false; } // format sans aperçu : on ne touche à rien
		if (fichier.size > CONFIG.tailleMax) { return false; } // refusé par le widget : pas de panneau

		var doc = { nom: fichier.name, pages: 0, tronque: false };
		courant = doc;
		document.body.classList.add('cv-charge');
		pages.replaceChildren();
		nomEl.textContent = fichier.name;
		metaEl.textContent = taille(fichier.size);
		scene.scrollTop = 0; scene.scrollLeft = 0;
		montrer();

		ecrireNote('Ouverture du CV…');

		var rendu = type === 'pdf' ? afficherPdf(fichier, doc)
			: type === 'docx' ? afficherDocx(fichier, doc)
			: type === 'texte' ? afficherTexte(fichier, doc)
			: afficherImage(fichier, doc);
		rendu.then(function () {
			if (courant !== doc) { return; }
			ecrireNote('');
			var detail = type === 'pdf' ? doc.pages + (doc.pages > 1 ? ' pages' : ' page')
				: type === 'docx' ? 'Word'
				: type === 'texte' ? 'Texte' : 'Image';
			metaEl.textContent = detail + ' · ' + taille(fichier.size) +
				(doc.tronque ? ' · ' + CONFIG.pagesMax + ' premières pages affichées' : '') +
				(type === 'docx' ? ' · aperçu simplifié, la mise en page peut différer' : '');
		}).catch(function (err) {
			if (courant !== doc) { return; }
			pages.replaceChildren();
			ecrireNote(messageErreur(err));
		});
		return true;
	}

	function demarrer() {
		construire();
		// Écoute tous les champs « fichier » de la page, y compris ceux ajoutés plus tard.
		document.addEventListener('change', function (e) {
			var champ = e.target;
			if (champ && champ.tagName === 'INPUT' && champ.type === 'file' && champ.files && champ.files[0]) {
				ouvrir(champ.files[0]);
			}
		}, true);
	}

	window.ElioraCV = { ouvrir: ouvrir, fermer: fermer };

	if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', demarrer); }
	else { demarrer(); }
})();