Connexion · Créer un compte

← SKYNET · historique

Premier dépôt du projet #1

henoc Gnaoule · il y a 3 h

4 fichiers concernés

cv_panneau.css Ajouté

+107 −0

…
1+/* ==========================================================================
2+ Panneau CV — Eliora
3+ Ouvre le CV de l'utilisateur dans un panneau à droite de la conversation.
4+ Fonctionne avec js/script_cv_panneau.js.
5+ --cv-w = largeur du panneau, réglée par le script (poignée de redimensionnement).
6+ ========================================================================== */
7+
8+:root { --cv-w: 46vw; }
9+
10+/* ---------- Le panneau ---------- */
11+#cv-panneau {
12+ position: fixed; top: 0; right: 0; bottom: 0;
13+ width: var(--cv-w);
14+ z-index: 1000;
15+ display: none; flex-direction: column;
16+ background-color: #ffffff;
17+ border-left: 1px solid #dcdce0;
18+ box-shadow: rgba(9, 30, 66, 0.12) -4px 0 12px -4px;
19+ font-family: Helvetica, Arial, sans-serif;
20+ box-sizing: border-box;
21+}
22+body.cv-ouvert #cv-panneau { display: flex; }
23+body.cv-plein #cv-panneau { width: 100%; border-left: none; }
24+
25+#cv-entete {
26+ display: flex; align-items: center; gap: 12px;
27+ padding: 10px 14px 10px 18px;
28+ border-bottom: 1px solid #e5e5e7;
29+ flex: none;
30+}
31+#cv-infos { flex: 1; min-width: 0; display: flex; flex-direction: column; }
32+#cv-nom { font-size: 15px; font-weight: bold; color: #222; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
33+#cv-meta { font-size: 12px; color: gray; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
34+
35+#cv-outils { display: flex; align-items: center; gap: 4px; flex: none; }
36+.cv-bouton {
37+ min-width: 34px; height: 34px; padding: 0 8px;
38+ border: none; border-radius: 50%;
39+ background-color: transparent; color: #333;
40+ font-family: Helvetica, Arial, sans-serif; font-size: 18px;
41+ cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
42+}
43+.cv-bouton:hover { background-color: #e5e5e7; }
44+.cv-bouton:disabled { opacity: 0.35; cursor: default; background-color: transparent; }
45+.cv-bouton:focus-visible, #cv-onglet:focus-visible, #cv-poignee:focus-visible { outline: 2px solid dodgerblue; outline-offset: 2px; }
46+#cv-zoom { min-width: 48px; text-align: center; font-size: 13px; color: gray; }
47+#cv-fermer { margin-left: 6px; background-color: #e5e5e7; }
48+#cv-fermer:hover { background-color: #d6d6da; }
49+
50+#cv-scene { flex: 1; min-height: 0; overflow: auto; background-color: #F0F0F2; padding: 18px 16px; }
51+#cv-pages { display: flex; flex-direction: column; gap: 14px; align-items: flex-start; --cv-zoom: 1; }
52+.cv-page {
53+ display: block; flex: none;
54+ width: calc(var(--cv-zoom) * 100%); max-width: none; height: auto;
55+ margin-left: auto; margin-right: auto;
56+ background-color: #ffffff;
57+ box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.08) 0px 0px 0px 1px;
58+}
59+/* Aperçu des fichiers Word et texte : une feuille blanche, le zoom agit sur la taille du texte */
60+.cv-feuille {
61+ width: 100%; box-sizing: border-box; padding: 36px 40px;
62+ font-family: Helvetica, Arial, sans-serif; font-size: calc(15px * var(--cv-zoom)); line-height: 1.5; color: #222;
63+ overflow-wrap: anywhere;
64+}
65+.cv-texte { white-space: pre-wrap; }
66+.cv-feuille p { margin: 0 0 0.7em; }
67+.cv-feuille h1, .cv-feuille h2, .cv-feuille h3, .cv-feuille h4 { margin: 1em 0 0.4em; line-height: 1.25; }
68+.cv-feuille h1 { font-size: 1.6em; } .cv-feuille h2 { font-size: 1.3em; } .cv-feuille h3, .cv-feuille h4 { font-size: 1.1em; }
69+.cv-feuille ul, .cv-feuille ol { margin: 0 0 0.7em; padding-left: 1.4em; }
70+.cv-feuille table { border-collapse: collapse; width: 100%; margin: 0 0 0.7em; }
71+.cv-feuille td, .cv-feuille th { border: 1px solid #dcdce0; padding: 4px 8px; vertical-align: top; text-align: left; }
72+.cv-feuille img { max-width: 100%; height: auto; }
73+
74+#cv-note { margin: 0; padding: 30px 10px; text-align: center; font-size: 14px; color: gray; }
75+
76+/* ---------- Poignée de redimensionnement (bord gauche du panneau) ---------- */
77+#cv-poignee { position: absolute; top: 0; bottom: 0; left: -5px; width: 10px; cursor: col-resize; touch-action: none; }
78+#cv-poignee:hover, body.cv-redim #cv-poignee { background-color: rgba(30, 144, 255, 0.35); }
79+body.cv-redim { cursor: col-resize; user-select: none; }
80+body.cv-plein #cv-poignee { display: none; }
81+
82+/* ---------- Onglet « Voir mon CV » quand le panneau est fermé ---------- */
83+#cv-onglet {
84+ position: fixed; right: 0; top: 80px; z-index: 900;
85+ display: none; align-items: center; gap: 8px;
86+ padding: 10px 14px 10px 12px;
87+ border: none; border-radius: 20px 0 0 20px;
88+ background-color: #ffffff; color: #333;
89+ font-family: Helvetica, Arial, sans-serif; font-size: 14px;
90+ box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.08) 0px 0px 0px 1px;
91+ cursor: pointer;
92+}
93+#cv-onglet:hover { color: dodgerblue; }
94+body.cv-charge:not(.cv-ouvert) #cv-onglet { display: inline-flex; }
95+
96+/* ---------- La conversation se pousse à gauche quand le panneau est ouvert ---------- */
97+body.cv-ouvert:not(.cv-plein) #visio { width: calc(100% - var(--cv-w)) !important; margin: 0 !important; }
98+body.cv-ouvert:not(.cv-plein) #affiche_message { max-width: calc(100% - 40px); }
99+body.cv-ouvert:not(.cv-plein) #boto,
100+body.cv-ouvert:not(.cv-plein) #label_text {
101+ left: calc((100% - var(--cv-w)) / 2) !important;
102+ max-width: calc(100% - var(--cv-w) - 40px);
103+}
104+body.cv-ouvert:not(.cv-plein) #boto textarea { max-width: 100%; }
105+body.cv-ouvert:not(.cv-plein) #pied-eliora { width: calc(100% - var(--cv-w)) !important; }
106+body.cv-ouvert:not(.cv-plein) #pied-eliora span { left: calc((100% - var(--cv-w)) / 2) !important; }
107+
eliora_chat_1.php Ajouté

+208 −0

…
1+<?php
2+error_reporting(0);
3+
4+$mobile = $_SERVER['HTTP_USER_AGENT'];
5+
6+if(preg_match("(iPhone|BlackBerry|Android|HTC|LG|MOT|Nokia|Palm|SAMSUNG|SonyEricsson)",$mobile)){
7+ header('Location:/eliora_chat_mobile.php');}
8+?>
9+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
10+<html xmlns="http://www.w3.org/1999/xhtml">
11+<head>
12+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
13+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
14+<title>Eliora AI</title>
15+ <link rel="icon" type="image/png" href="image/apv.ico">
16+ <link rel="shortcut icon" href="image/apvx.png"/>
17+ <link rel="stylesheet" type="text/css" href="css/style.css">
18+ <link rel="stylesheet" type="text/css" href="css/message_form.css">
19+ <link rel="stylesheet" type="text/css" href="css/cv_panneau.css">
20+ <script type="text/javascript" src="js/jquery.js"></script>
21+ <script type="text/javascript" src="js/script_form.js"></script>
22+</head>
23+<body style="background-color: #F0F0F2">
24+
25+ <div class="box" style="position:; background-color:#F0F0F2">
26+ <img src="image/apvx.png" width="30px" style="margin-left:20px; margin-top:10px; background-color:#F0F0F2; display:inline-block; float:left"/>
27+ <button style="width:50px; height:50px; margin-top:15px; font-size:25px; font-family:Helvetica; border-radius:17px; border:none; cursor:pointer;
28+ background-color:#F0F0F2; margin-left:5px; margin-top:5px">Eliora</button>
29+
30+ </div>
31+
32+
33+<div id="visio" style="width: 100%; height:700px; border:none; margin: 0 auto; background-color:#F0F0F2; position:relative; overflow:auto">
34+ <div id="affiche_message" style="width: 730px; height:auto; border:none; margin: 0 auto; padding-bottom: 200px; padding-top:70px; background-color:#F0F0F2; position:relative; overflow:auto; display:flex; flex-direction: column">
35+ </div>
36+
37+ </div>
38+
39+ <div id="cv-statut" style="display:none; font-family:Helvetica; font-size:14px; color:gray; margin-top:8px;"></div>
40+ <div id="label_text" style="width:500px; height:60px; border:none; position:fixed; background-color:#F0F0F2; bottom:62%; left: 50%;
41+ transform: translateX(-50%); border-radius:20px; text-align:center">
42+ <span id="text_chang" style="font-family:Helvetica; font-size:30px">Hello <span style="font-family:Helvetica; font-size:30px;"><?php echo htmlspecialchars((string) ($_SESSION['prenom'] ?? ''), ENT_QUOTES, 'UTF-8'); ?></span></span>
43+ </div>
44+
45+ <div id="boto" style="width:750px; height:; border:none; position: fixed; background-color:white; bottom:45%; left: 50%;
46+ transform: translateX(-50%); border-radius:20px; z-index:10;
47+ box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.08) 0px 0px 0px 1px">
48+
49+
50+ <form method="POST" action="donnees.php" id="formulaire" >
51+
52+ <button type="button" id="bouton-menu" onclick="toggleMenu()" style="position:absolute; left:10px; bottom:10px; width:36px; height:36px; border-radius:50%; border:none; background-color:#e5e5e7; color:#333; cursor:pointer; display:flex; align-items:center; justify-content:center; transition: transform 0.25s ease, background-color 0.2s;" aria-label="Ouvrir le menu des options">
53+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="18" height="18" id="icone-menu">
54+ <line x1="12" y1="5" x2="12" y2="19"></line>
55+ <line x1="5" y1="12" x2="19" y2="12"></line>
56+ </svg>
57+ </button>
58+ <label for="mess" style="position:absolute; left:-9999px;">Poser une question</label>
59+ <textarea id="mess" name="message" placeholder="Comment puis-je vous aider ?" maxlength="" style="width:720px; height:100px;
60+ border-radius:20px; border:none; padding-top:20px; padding-left:20px; font-size:16px; font-family:Helvetica;
61+ box-sizing:border-box;line-height:20px;overflow:hidden; padding-bottom:47px; display:block" ></textarea>
62+
63+
64+ <button type="submit" id="publier" disabled onclick="gre()" style="position:absolute; right:10px; bottom:10px; width:36px; height:36px; border-radius:50%; border:none; background-color:dodgerblue; color:gray; cursor:pointer; display:flex; align-items:center; justify-content:center; transition: background-color 0.2s;" aria-label="Envoyer le message">
65+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="18" height="18">
66+ <line x1="12" y1="19" x2="12" y2="5"></line>
67+ <polyline points="5 12 12 5 19 12"></polyline>
68+ </svg>
69+ </button>
70+ <div id="menu-options" style="display:none; position:absolute; left:10px; bottom:55px; background:#fff; border:1px solid #ddd; border-radius:10px; box-shadow:0 2px 10px rgba(0,0,0,0.1); padding:8px; min-width:160px; z-index:20;">
71+ <div class="option-menu" style="padding:8px 12px; cursor:pointer; border-radius:6px; font-size:14px; font-family:helvetica" onclick="professionnel()">Professionnel</div>
72+ <div class="option-menu" style="padding:8px 12px; cursor:pointer; border-radius:6px; font-size:14px; font-family:helvetica" onclick="finance()">Finance</div>
73+ </div>
74+ </form>
75+
76+<form method="POST" action="donnees_professionnelles.php" id="formulairepro" style="display:none" >
77+
78+ <button type="button" id="pro-menu" onclick="proMenu()" style="position:absolute; left:10px; bottom:10px; width:36px; height:36px; border-radius:50%; border:none; background-color:#e5e5e7; color:#333; cursor:pointer; display:flex; align-items:center; justify-content:center; transition: transform 0.25s ease, background-color 0.2s;" aria-label="Ouvrir le menu des options">
79+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="18" height="18" id="icone-menu-pro">
80+ <line x1="12" y1="5" x2="12" y2="19"></line>
81+ <line x1="5" y1="12" x2="19" y2="12"></line>
82+ </svg>
83+ </button>
84+ <label for="professionnel" style="position:absolute; left:-9999px;">Rechercher un professionnel</label>
85+ <textarea id="professionnel" name="messagepro" placeholder="Rechercher un professionnel" maxlength="" style="width:720px; height:100px;
86+ border-radius:20px; border:none; padding-top:20px; padding-left:20px; font-size:16px; font-family:Helvetica;
87+ box-sizing:border-box;line-height:20px;overflow:hidden; padding-bottom:47px; display:block" ></textarea>
88+
89+
90+ <button type="submit" id="envoipro" disabled onclick="gre()" style="position:absolute; right:10px; bottom:10px; width:36px; height:36px; border-radius:50%; border:none; background-color:dodgerblue; color:gray; cursor:pointer; display:flex; align-items:center; justify-content:center; transition: background-color 0.2s;" aria-label="Envoyer la recherche">
91+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="18" height="18">
92+ <line x1="12" y1="19" x2="12" y2="5"></line>
93+ <polyline points="5 12 12 5 19 12"></polyline>
94+ </svg>
95+ </button>
96+ <div id="pro-options" style="display:none; position:absolute; left:10px; bottom:55px; background:#fff; border:1px solid #ddd; border-radius:10px; box-shadow:0 2px 10px rgba(0,0,0,0.1); padding:8px; min-width:160px; z-index:20;">
97+ <div class="option-menu" style="padding:8px 12px; cursor:pointer; border-radius:6px; font-size:14px; font-weight:bold; font-family:helvetica" onclick="professionnel()">Professionnel</div>
98+ <div class="option-menu" style="padding:8px 12px; cursor:pointer; border-radius:6px; font-size:14px; font-family:helvetica" onclick="finance()">Finance</div>
99+ </div>
100+ <button id="profe-pro" style="width:130px; height:30px; font-family:Helvetica; border-radius:20px; margin-left:60px; position:fixed; bottom:5px; border:1.5px solid #F0F0F2; background-color:transparent; color:gray; cursor:pointer" disabled>
101+ professionnel
102+ <img src="image/ferm.png" width="20" style="margin-left:px; margin-top:px; display:inline-block;vertical-align: middle" onclick="ferm_pro()"/>
103+ </button>
104+ </form>
105+
106+<form method="POST" action="donnees_finances.php" id="formulairefin" style="display:none" >
107+
108+ <button type="button" id="fin-menu" onclick="finMenu()" style="position:absolute; left:10px; bottom:10px; width:36px; height:36px; border-radius:50%; border:none; background-color:#e5e5e7; color:#333; cursor:pointer; display:flex; align-items:center; justify-content:center; transition: transform 0.25s ease, background-color 0.2s;" aria-label="Ouvrir le menu des options">
109+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="18" height="18" id="icone-menu-fin">
110+ <line x1="12" y1="5" x2="12" y2="19"></line>
111+ <line x1="5" y1="12" x2="19" y2="12"></line>
112+ </svg>
113+ </button>
114+ <label for="finance" style="position:absolute; left:-9999px;">Entrer votre revenu</label>
115+ <textarea id="finance" name="messagefin" placeholder="Entrer votre revenu" maxlength="" style="width:720px; height:100px;
116+ border-radius:20px; border:none; padding-top:20px; padding-left:20px; font-size:16px; font-family:Helvetica;
117+ box-sizing:border-box;line-height:20px;overflow:hidden; padding-bottom:47px; display:block" ></textarea>
118+
119+
120+ <button type="submit" id="okfin" disabled onclick="gre()" style="position:absolute; right:10px; bottom:10px; width:36px; height:36px; border-radius:50%; border:none; background-color:dodgerblue; color:gray; cursor:pointer; display:flex; align-items:center; justify-content:center; transition: background-color 0.2s;" aria-label="Envoyer">
121+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="18" height="18">
122+ <line x1="12" y1="19" x2="12" y2="5"></line>
123+ <polyline points="5 12 12 5 19 12"></polyline>
124+ </svg>
125+ </button>
126+ <div id="fin-options" style="display:none; position:absolute; left:10px; bottom:55px; background:#fff; border:1px solid #ddd; border-radius:10px; box-shadow:0 2px 10px rgba(0,0,0,0.1); padding:8px; min-width:160px; z-index:20;">
127+ <div class="option-menu" style="padding:8px 12px; cursor:pointer; border-radius:6px; font-size:14px; font-family:helvetica" onclick="professionnel()">Professionnel</div>
128+ <div class="option-menu" style="padding:8px 12px; cursor:pointer; border-radius:6px; font-size:14px; font-weight:bold; font-family:helvetica" onclick="finance()">Finance</div>
129+ </div>
130+
131+
132+ <button id="profe-fin" style="width:155px; height:30px; font-family:Helvetica; border-radius:20px; margin-left:60px; position:fixed; bottom:5px; border:1.5px solid #F0F0F2; background-color:transparent; color:gray; cursor:pointer" onmouseover="ax()" onmouseout="az()" disabled>
133+ budget & analyse
134+ <img src="image/ferm.png" width="20" style="margin-left:px; margin-top:px; display:inline-block;vertical-align: middle" onclick="ferm_fin()"/>
135+ </button>
136+
137+ </form>
138+
139+ </div>
140+ <div id="pied-eliora" style="width:100%; height:40px; position:fixed; bottom:0px; background-color:#F0F0F2; color:; font-family:Helvetica; border:none">
141+ <center><span class="" style="font-family:Helvetica; font-size:12px; position:fixed; bottom:10px;
142+ left: 50%; transform: translateX(-50%); ">En utilisant Eliora, vous acceptez les&nbsp;<a href="conditioniaweb.php" style="text-decoration:none;
143+ color:skyblue">conditions d'utilisation.</a></span></center>
144+ </div>
145+
146+ <script type="text/javascript" src="js/script_bouton_ok.js"></script>
147+ <script type="text/javascript" src="js/script_cv.js"></script>
148+
149+ <script>
150+ $(document).ready(function () {
151+
152+ // --- Validation factorisée : une seule fonction réutilisée
153+ // pour les 3 champs (message général, professionnel, finance)
154+ // au lieu de 3 blocs quasi identiques dupliqués.
155+ function brancherValidation(champId, boutonId) {
156+ var minuteur = null;
157+
158+ function verifierSaisie() {
159+ var input = $('#' + champId).val();
160+ $.ajax({
161+ method: "POST",
162+ url: 'testervaleur.php',
163+ data: { gen: input },
164+ dataType: "text",
165+ success: function (result) {
166+ var ok = (result.trim() === "true");
167+ $('#' + boutonId).prop('disabled', !ok);
168+ $('#' + boutonId).css({
169+ backgroundColor: ok ? "skyblue" : "#F0F0F2",
170+ color: ok ? "white" : "gray"
171+ });
172+ }
173+ });
174+ }
175+
176+ $('#' + champId).on('input', function () {
177+ clearTimeout(minuteur);
178+ minuteur = setTimeout(verifierSaisie, 250);
179+ });
180+
181+ // Une <textarea> ne soumet pas le formulaire sur Entrée par défaut
182+ // (contrairement à un <input type="text">) — elle insère un saut de ligne.
183+ // On intercepte donc la touche Entrée pour déclencher l'envoi à la place.
184+ $('#' + champId).on('keydown', function (e) {
185+ if (e.key === 'Enter' && !e.shiftKey) {
186+ e.preventDefault();
187+ gre();
188+ if (!$('#' + boutonId).prop('disabled')) {
189+ $('#' + champId).closest('form').trigger('submit');
190+ }
191+ }
192+ });
193+ }
194+
195+ brancherValidation('mess', 'publier');
196+ brancherValidation('professionnel', 'envoipro');
197+ brancherValidation('finance', 'okfin');
198+
199+ });
200+ </script>
201+
202+<?php include __DIR__ . '/eliora_cv_widget.php'; ?>
203+
204+ <!-- Panneau CV : ouvre le CV choisi dans un panneau à droite de la conversation -->
205+ <script type="text/javascript" src="js/script_cv_panneau.js"></script>
206+</body>
207+</html>
208+
README.md Ajouté

+4 −0

…
1+# SKYNET
2+
3+Skynet est un projet qui pour but de developper une Intelligence artificielle générale
4+
script_cv_panneau.js Ajouté

+360 −0

…
1+/* ==========================================================================
2+ Panneau CV — Eliora
3+ Quand l'utilisateur choisit son CV dans un champ <input type="file"> de
4+ la page (ici #ecv-fichier, le champ de eliora_cv_widget.php), le fichier
5+ s'ouvre dans un panneau à droite de la conversation.
6+ Formats affichés : PDF, Word (.docx), texte (.txt), PNG et JPG.
7+ L'envoi du CV vers eliora_cv.php n'est pas modifié : ce script se
8+ contente d'afficher le fichier, il n'envoie rien.
9+
10+ Appel direct possible depuis un autre script :
11+ ElioraCV.ouvrir(fichier); // fichier = objet File
12+ ElioraCV.fermer();
13+
14+ Styles : css/cv_panneau.css
15+ ========================================================================== */
16+(function () {
17+ 'use strict';
18+
19+ var CONFIG = {
20+ // Lecteurs chargés seulement au premier CV ouvert. Pour ne pas dépendre
21+ // des CDN, copier ces fichiers dans js/ et remplacer les adresses.
22+ pdfJs: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js',
23+ pdfWorker: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js',
24+ mammoth: 'https://cdn.jsdelivr.net/npm/mammoth@1.6.0/mammoth.browser.min.js', // lecteur Word
25+ tailleMax: 3 * 1024 * 1024, // même limite que le widget (3 Mo) : au-delà, le panneau ne s'ouvre pas
26+ pagesMax: 20, // nombre maximal de pages PDF affichées
27+ largeurMin: 360, // largeur minimale du panneau (px)
28+ conversationMin: 480, // largeur minimale laissée à la conversation (px)
29+ seuilPleinEcran: 900 // sous cette largeur de fenêtre, le panneau prend tout l'écran
30+ };
31+ var ZOOMS = [0.5, 0.75, 1, 1.25, 1.5, 2];
32+ var CLE_LARGEUR = 'eliora-cv-largeur';
33+
34+ var panneau, scene, pages, note, nomEl, metaEl, zoomEl, btnMoins, btnPlus, btnFermer, poignee, onglet;
35+ var courant = null; // document affiché
36+ var indexZoom = 2;
37+ var largeurVoulue = null;
38+ var scriptsCharges = {};
39+
40+ /* ---------- Construction du panneau ---------- */
41+ function construire() {
42+ panneau = document.createElement('div');
43+ panneau.id = 'cv-panneau';
44+ panneau.setAttribute('role', 'complementary');
45+ panneau.setAttribute('aria-label', 'Aperçu du CV');
46+ panneau.innerHTML =
47+ '<div id="cv-poignee" role="separator" aria-orientation="vertical" aria-label="Largeur du panneau CV" tabindex="0"></div>' +
48+ '<div id="cv-entete">' +
49+ '<div id="cv-infos"><span id="cv-nom"></span><span id="cv-meta"></span></div>' +
50+ '<div id="cv-outils">' +
51+ '<button type="button" class="cv-bouton" id="cv-moins" aria-label="Réduire">&minus;</button>' +
52+ '<span id="cv-zoom" aria-live="polite">100 %</span>' +
53+ '<button type="button" class="cv-bouton" id="cv-plus" aria-label="Agrandir">+</button>' +
54+ '<button type="button" class="cv-bouton" id="cv-fermer" aria-label="Fermer l\'aperçu du CV">&times;</button>' +
55+ '</div>' +
56+ '</div>' +
57+ '<div id="cv-scene"><p id="cv-note"></p><div id="cv-pages"></div></div>';
58+ document.body.appendChild(panneau);
59+
60+ onglet = document.createElement('button');
61+ onglet.type = 'button';
62+ onglet.id = 'cv-onglet';
63+ onglet.innerHTML =
64+ '<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">' +
65+ '<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>' +
66+ 'Voir mon CV';
67+ document.body.appendChild(onglet);
68+
69+ scene = document.getElementById('cv-scene');
70+ pages = document.getElementById('cv-pages');
71+ note = document.getElementById('cv-note');
72+ nomEl = document.getElementById('cv-nom');
73+ metaEl = document.getElementById('cv-meta');
74+ zoomEl = document.getElementById('cv-zoom');
75+ btnMoins = document.getElementById('cv-moins');
76+ btnPlus = document.getElementById('cv-plus');
77+ btnFermer = document.getElementById('cv-fermer');
78+ poignee = document.getElementById('cv-poignee');
79+
80+ btnFermer.addEventListener('click', fermer);
81+ onglet.addEventListener('click', function () { montrer(); btnFermer.focus(); });
82+ btnMoins.addEventListener('click', function () { if (indexZoom > 0) { indexZoom--; appliquerZoom(); } });
83+ btnPlus.addEventListener('click', function () { if (indexZoom < ZOOMS.length - 1) { indexZoom++; appliquerZoom(); } });
84+ document.addEventListener('keydown', function (e) {
85+ if (e.key === 'Escape' && document.body.classList.contains('cv-ouvert')) { fermer(); }
86+ });
87+ brancherPoignee();
88+ window.addEventListener('resize', ajusterLargeur);
89+
90+ try {
91+ var memo = parseInt(localStorage.getItem(CLE_LARGEUR), 10);
92+ if (memo > 0) { largeurVoulue = memo; }
93+ } catch (e) {}
94+ appliquerZoom();
95+ ajusterLargeur();
96+ }
97+
98+ /* ---------- Ouverture / fermeture ---------- */
99+ function montrer() { document.body.classList.add('cv-ouvert'); ajusterLargeur(); }
100+ function fermer() {
101+ if (!document.body.classList.contains('cv-ouvert')) { return; }
102+ document.body.classList.remove('cv-ouvert');
103+ if (courant) { onglet.focus(); }
104+ }
105+ function ecrireNote(texte) { note.textContent = texte || ''; note.style.display = texte ? 'block' : 'none'; }
106+
107+ function appliquerZoom() {
108+ var z = ZOOMS[indexZoom];
109+ pages.style.setProperty('--cv-zoom', z);
110+ zoomEl.textContent = Math.round(z * 100) + ' %';
111+ btnMoins.disabled = indexZoom === 0;
112+ btnPlus.disabled = indexZoom === ZOOMS.length - 1;
113+ }
114+
115+ /* ---------- Largeur du panneau ---------- */
116+ function ajusterLargeur() {
117+ var fenetre = window.innerWidth;
118+ var plein = fenetre < CONFIG.seuilPleinEcran;
119+ document.body.classList.toggle('cv-plein', plein);
120+ if (plein) { return; }
121+ var voulue = largeurVoulue || Math.round(fenetre * 0.46);
122+ var max = Math.max(CONFIG.largeurMin, fenetre - CONFIG.conversationMin);
123+ var w = Math.max(CONFIG.largeurMin, Math.min(voulue, max));
124+ document.documentElement.style.setProperty('--cv-w', w + 'px');
125+ poignee.setAttribute('aria-valuenow', Math.round(w / fenetre * 100));
126+ }
127+ function memoriserLargeur() { try { localStorage.setItem(CLE_LARGEUR, String(largeurVoulue)); } catch (e) {} }
128+
129+ function brancherPoignee() {
130+ var actif = false;
131+ poignee.addEventListener('pointerdown', function (e) {
132+ actif = true; poignee.setPointerCapture(e.pointerId);
133+ document.body.classList.add('cv-redim'); e.preventDefault();
134+ });
135+ poignee.addEventListener('pointermove', function (e) {
136+ if (!actif) { return; }
137+ largeurVoulue = Math.round(window.innerWidth - e.clientX);
138+ ajusterLargeur();
139+ });
140+ function fin() {
141+ if (!actif) { return; }
142+ actif = false; document.body.classList.remove('cv-redim'); memoriserLargeur();
143+ }
144+ poignee.addEventListener('pointerup', fin);
145+ poignee.addEventListener('pointercancel', fin);
146+ poignee.addEventListener('keydown', function (e) {
147+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') { return; }
148+ e.preventDefault();
149+ largeurVoulue = Math.round(panneau.getBoundingClientRect().width + (e.key === 'ArrowLeft' ? 32 : -32));
150+ ajusterLargeur(); memoriserLargeur();
151+ });
152+ }
153+
154+ /* ---------- Lecture du fichier ---------- */
155+ function typeDe(fichier) {
156+ var n = (fichier.name || '').toLowerCase();
157+ if (fichier.type === 'application/pdf' || /\.pdf$/.test(n)) { return 'pdf'; }
158+ if (/\.docx$/.test(n)) { return 'docx'; }
159+ if (/\.txt$/.test(n)) { return 'texte'; }
160+ if (fichier.type === 'image/png' || fichier.type === 'image/jpeg' || /\.(png|jpe?g)$/.test(n)) { return 'image'; }
161+ return null;
162+ }
163+ function taille(octets) {
164+ if (octets < 1024 * 1024) { return Math.max(1, Math.round(octets / 1024)) + ' ko'; }
165+ return (octets / (1024 * 1024)).toFixed(1).replace('.', ',') + ' Mo';
166+ }
167+
168+ // Charge un script externe une seule fois et renvoie l'objet global qu'il définit.
169+ function chargerScript(adresse, nomGlobal) {
170+ if (window[nomGlobal]) { return Promise.resolve(window[nomGlobal]); }
171+ if (!scriptsCharges[adresse]) {
172+ scriptsCharges[adresse] = new Promise(function (ok, echec) {
173+ var s = document.createElement('script');
174+ s.src = adresse;
175+ s.onload = function () {
176+ if (window[nomGlobal]) { ok(window[nomGlobal]); }
177+ else { delete scriptsCharges[adresse]; echec(new Error('LECTEUR')); }
178+ };
179+ s.onerror = function () { delete scriptsCharges[adresse]; echec(new Error('LECTEUR')); };
180+ document.head.appendChild(s);
181+ });
182+ }
183+ return scriptsCharges[adresse];
184+ }
185+ function chargerPdfJs() {
186+ return chargerScript(CONFIG.pdfJs, 'pdfjsLib').then(function (pdfjs) {
187+ pdfjs.GlobalWorkerOptions.workerSrc = pdfjs.GlobalWorkerOptions.workerSrc || CONFIG.pdfWorker;
188+ return pdfjs;
189+ });
190+ }
191+
192+ function ajouterPage(doc, noeud) {
193+ if (courant !== doc) { return; } // un autre CV a été choisi entre-temps
194+ pages.appendChild(noeud);
195+ }
196+
197+ function afficherPdf(fichier, doc) {
198+ return chargerPdfJs().then(function (pdfjs) {
199+ return fichier.arrayBuffer().then(function (tampon) {
200+ return pdfjs.getDocument({ data: new Uint8Array(tampon), isEvalSupported: false }).promise;
201+ });
202+ }).then(function (pdf) {
203+ var total = pdf.numPages, affichees = Math.min(total, CONFIG.pagesMax);
204+ var dpr = Math.min(window.devicePixelRatio || 1, 2);
205+ var cible = Math.min(1800, Math.max(1000, (scene.clientWidth || 700) * dpr * 1.25));
206+ doc.pages = total; doc.tronque = total > affichees;
207+ var suite = Promise.resolve();
208+ for (var i = 1; i <= affichees; i++) {
209+ (function (numero) {
210+ suite = suite.then(function () {
211+ if (courant !== doc) { return null; }
212+ return pdf.getPage(numero).then(function (page) {
213+ var base = page.getViewport({ scale: 1 });
214+ var vue = page.getViewport({ scale: cible / base.width });
215+ var c = document.createElement('canvas');
216+ c.className = 'cv-page';
217+ c.width = Math.round(vue.width); c.height = Math.round(vue.height);
218+ c.setAttribute('role', 'img');
219+ c.setAttribute('aria-label', 'Page ' + numero + ' sur ' + total);
220+ return page.render({ canvasContext: c.getContext('2d'), viewport: vue }).promise.then(function () {
221+ ajouterPage(doc, c);
222+ });
223+ });
224+ });
225+ })(i);
226+ }
227+ return suite;
228+ });
229+ }
230+
231+ function afficherImage(fichier, doc) {
232+ return new Promise(function (ok, echec) {
233+ var img = new Image();
234+ img.className = 'cv-page';
235+ img.alt = 'CV : ' + fichier.name;
236+ img.onload = function () { doc.pages = 1; ajouterPage(doc, img); ok(); };
237+ img.onerror = function () { echec(new Error('IMAGE')); };
238+ img.src = URL.createObjectURL(fichier);
239+ });
240+ }
241+
242+ function feuille() {
243+ var f = document.createElement('div');
244+ f.className = 'cv-page cv-feuille';
245+ return f;
246+ }
247+
248+ function afficherTexte(fichier, doc) {
249+ return fichier.text().then(function (texte) {
250+ var f = feuille();
251+ f.classList.add('cv-texte');
252+ f.textContent = texte;
253+ ajouterPage(doc, f);
254+ });
255+ }
256+
257+ // Le HTML produit à partir du fichier Word n'est pas inséré tel quel : on ne garde
258+ // que des balises de mise en forme, sans attribut, sans lien et sans script.
259+ 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,
260+ SUP: 1, SUB: 1, BR: 1, TABLE: 1, THEAD: 1, TBODY: 1, TR: 1, TD: 1, TH: 1, IMG: 1, SPAN: 1, DIV: 1 };
261+ function nettoyer(source, cible) {
262+ for (var i = 0; i < source.childNodes.length; i++) {
263+ var n = source.childNodes[i];
264+ if (n.nodeType === 3) { cible.appendChild(document.createTextNode(n.nodeValue)); continue; }
265+ if (n.nodeType !== 1) { continue; }
266+ var nom = n.tagName.toUpperCase();
267+ if (nom === 'SCRIPT' || nom === 'STYLE' || nom === 'IFRAME' || nom === 'OBJECT' || nom === 'EMBED') { continue; }
268+ if (!BALISES_OK[nom]) { nettoyer(n, cible); continue; } // balise inconnue (lien, etc.) : on garde seulement son texte
269+ var copie = document.createElement(nom);
270+ if (nom === 'IMG') {
271+ var src = n.getAttribute('src') || '';
272+ if (!/^data:image\/(png|jpe?g|gif);base64,/i.test(src)) { continue; }
273+ copie.setAttribute('src', src);
274+ copie.setAttribute('alt', '');
275+ }
276+ if (nom === 'TD' || nom === 'TH') {
277+ var c = parseInt(n.getAttribute('colspan'), 10), r = parseInt(n.getAttribute('rowspan'), 10);
278+ if (c > 1) { copie.setAttribute('colspan', c); }
279+ if (r > 1) { copie.setAttribute('rowspan', r); }
280+ }
281+ nettoyer(n, copie);
282+ cible.appendChild(copie);
283+ }
284+ }
285+
286+ function afficherDocx(fichier, doc) {
287+ return chargerScript(CONFIG.mammoth, 'mammoth').then(function (mammoth) {
288+ return fichier.arrayBuffer().then(function (tampon) {
289+ return mammoth.convertToHtml({ arrayBuffer: tampon });
290+ });
291+ }).then(function (resultat) {
292+ var brut = new DOMParser().parseFromString('<div>' + (resultat.value || '') + '</div>', 'text/html');
293+ var f = feuille();
294+ nettoyer(brut.body, f);
295+ ajouterPage(doc, f);
296+ });
297+ }
298+
299+ function messageErreur(err) {
300+ if (err && err.message === 'LECTEUR') { return 'Le lecteur de documents ne s\'est pas chargé. Vérifiez votre connexion puis choisissez à nouveau votre CV.'; }
301+ if (err && err.name === 'PasswordException') { return 'Ce PDF est protégé par un mot de passe : l\'aperçu n\'est pas possible.'; }
302+ return 'L\'aperçu de ce fichier n\'est pas possible : il est illisible ou endommagé.';
303+ }
304+
305+ /* ---------- Point d'entrée ---------- */
306+ function ouvrir(fichier) {
307+ if (!fichier) { return false; }
308+ var type = typeDe(fichier);
309+ if (!type) { return false; } // format sans aperçu : on ne touche à rien
310+ if (fichier.size > CONFIG.tailleMax) { return false; } // refusé par le widget : pas de panneau
311+
312+ var doc = { nom: fichier.name, pages: 0, tronque: false };
313+ courant = doc;
314+ document.body.classList.add('cv-charge');
315+ pages.replaceChildren();
316+ nomEl.textContent = fichier.name;
317+ metaEl.textContent = taille(fichier.size);
318+ scene.scrollTop = 0; scene.scrollLeft = 0;
319+ montrer();
320+
321+ ecrireNote('Ouverture du CV…');
322+
323+ var rendu = type === 'pdf' ? afficherPdf(fichier, doc)
324+ : type === 'docx' ? afficherDocx(fichier, doc)
325+ : type === 'texte' ? afficherTexte(fichier, doc)
326+ : afficherImage(fichier, doc);
327+ rendu.then(function () {
328+ if (courant !== doc) { return; }
329+ ecrireNote('');
330+ var detail = type === 'pdf' ? doc.pages + (doc.pages > 1 ? ' pages' : ' page')
331+ : type === 'docx' ? 'Word'
332+ : type === 'texte' ? 'Texte' : 'Image';
333+ metaEl.textContent = detail + ' · ' + taille(fichier.size) +
334+ (doc.tronque ? ' · ' + CONFIG.pagesMax + ' premières pages affichées' : '') +
335+ (type === 'docx' ? ' · aperçu simplifié, la mise en page peut différer' : '');
336+ }).catch(function (err) {
337+ if (courant !== doc) { return; }
338+ pages.replaceChildren();
339+ ecrireNote(messageErreur(err));
340+ });
341+ return true;
342+ }
343+
344+ function demarrer() {
345+ construire();
346+ // Écoute tous les champs « fichier » de la page, y compris ceux ajoutés plus tard.
347+ document.addEventListener('change', function (e) {
348+ var champ = e.target;
349+ if (champ && champ.tagName === 'INPUT' && champ.type === 'file' && champ.files && champ.files[0]) {
350+ ouvrir(champ.files[0]);
351+ }
352+ }, true);
353+ }
354+
355+ window.ElioraCV = { ouvrir: ouvrir, fermer: fermer };
356+
357+ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', demarrer); }
358+ else { demarrer(); }
359+})();
360+