| |
| |
| |
| |
|
|
| (function () { |
| "use strict"; |
|
|
| |
| var LANG = { |
| en: { |
| subtitle: "Socratic Learning Companion", |
| topicLabel: "Exploration topic", |
| topicPlaceholder: "E.g.: Artificial intelligence in professional training", |
| docsLabel: "Reference documents (optional)", |
| uploadText: "Drag your files here or click to select", |
| uploadHint: "PDF, PPTX, TXT or ZIP \u2014 multiple files allowed", |
| modeLabel: "Mode", |
| modeTutor: "Tutor", |
| modeTutorDesc: "Supportive guidance, open-ended questions", |
| modeCritic: "Critic", |
| modeCriticDesc: "Devil's advocate, tests logical weaknesses", |
| btnStart: "Start session", |
| btnEnd: "End session", |
| btnRestart: "Restart", |
| chatPlaceholder: "Type your thoughts here...", |
| btnSend: "Send", |
| reportTitle: "Session Report", |
| summaryTitle: "Summary", |
| strengthsTitle: "Key strengths", |
| weaknessesTitle: "Areas for improvement", |
| rhythmTitle: "Pace", |
| rhythmText: "Responses under 8 seconds:", |
| btnExport: "Export JSON", |
| btnNewSession: "New session", |
| modeBadgeTutor: "Tutor", |
| modeBadgeCritic: "Critic", |
| errorEmpty: "No response from server. Check API configuration.", |
| errorConnection: "Connection error. Please try again.", |
| errorNoExchange: "No exchanges to analyze.", |
| errorAnalysis: "Analysis error. Please try again.", |
| startMessage: "Hello, I would like to explore the topic: ", |
| scoreLabels: ["Reasoning", "Clarity", "Skepticism", "Process", "Reflection", "Integrity"], |
| noSummary: "No summary available.", |
| langBtn: "FR" |
| }, |
| fr: { |
| subtitle: "Compagnon socratique d'apprentissage", |
| topicLabel: "Sujet d'exploration", |
| topicPlaceholder: "Ex : L'intelligence artificielle en formation professionnelle", |
| docsLabel: "Documents de reference (optionnel)", |
| uploadText: "Glisse tes fichiers ici ou clique pour selectionner", |
| uploadHint: "PDF, PPTX, TXT ou ZIP \u2014 plusieurs fichiers possibles", |
| modeLabel: "Mode", |
| modeTutor: "Tuteur", |
| modeTutorDesc: "Accompagnement bienveillant, questions ouvertes", |
| modeCritic: "Critique", |
| modeCriticDesc: "Avocat du diable, teste les failles logiques", |
| btnStart: "Commencer la session", |
| btnEnd: "Terminer la session", |
| btnRestart: "Recommencer", |
| chatPlaceholder: "Tape ta reflexion ici...", |
| btnSend: "Envoyer", |
| reportTitle: "Rapport de session", |
| summaryTitle: "Bilan", |
| strengthsTitle: "Points forts", |
| weaknessesTitle: "Axes d'amelioration", |
| rhythmTitle: "Rythme", |
| rhythmText: "Reponses en moins de 8 secondes :", |
| btnExport: "Exporter JSON", |
| btnNewSession: "Nouvelle session", |
| modeBadgeTutor: "Tuteur", |
| modeBadgeCritic: "Critique", |
| errorEmpty: "Reponse vide du serveur. Verifiez la configuration API.", |
| errorConnection: "Erreur de connexion. Veuillez reessayer.", |
| errorNoExchange: "Aucun echange a analyser.", |
| errorAnalysis: "Erreur lors de l'analyse. Veuillez reessayer.", |
| startMessage: "Bonjour, je souhaite explorer le sujet : ", |
| scoreLabels: ["Raisonnement", "Clarte", "Scepticisme", "Processus", "Reflexion", "Integrite"], |
| noSummary: "Aucun bilan disponible.", |
| langBtn: "EN" |
| } |
| }; |
|
|
| var currentLang = "en"; |
|
|
| function t(key) { |
| return LANG[currentLang][key] || key; |
| } |
|
|
| function applyLanguage() { |
| document.querySelectorAll("[data-i18n]").forEach(function (el) { |
| el.textContent = t(el.dataset.i18n); |
| }); |
| document.querySelectorAll("[data-i18n-placeholder]").forEach(function (el) { |
| el.placeholder = t(el.dataset.i18nPlaceholder); |
| }); |
| document.getElementById("btn-lang").textContent = t("langBtn"); |
| document.documentElement.lang = currentLang === "fr" ? "fr" : "en"; |
| document.title = currentLang === "en" ? "AIM - Learning Companion" : "AIM - Compagnon d'apprentissage"; |
| } |
|
|
| document.getElementById("btn-lang").addEventListener("click", function () { |
| currentLang = currentLang === "en" ? "fr" : "en"; |
| applyLanguage(); |
| }); |
|
|
| |
| var state = { |
| mode: "TUTOR", |
| topic: "", |
| phase: 0, |
| phaseTurns: 0, |
| history: [], |
| timestamps: [], |
| analysisResult: null, |
| uploadedDocs: [] |
| }; |
|
|
| var PHASE_NAMES = [ |
| "Ciblage", |
| "Clarification", |
| "Mecanisme", |
| "Verification", |
| "Stress-test" |
| ]; |
|
|
| |
| var setupScreen = document.getElementById("setup-screen"); |
| var chatScreen = document.getElementById("chat-screen"); |
| var analysisScreen = document.getElementById("analysis-screen"); |
|
|
| var topicInput = document.getElementById("topic-input"); |
| var btnStart = document.getElementById("btn-start"); |
| var modeBtns = document.querySelectorAll(".mode-btn"); |
|
|
| var modeBadge = document.getElementById("mode-badge"); |
| var topicBadge = document.getElementById("topic-badge"); |
| var docsBadge = document.getElementById("docs-badge"); |
| var phaseDots = document.getElementById("phase-dots"); |
| var phaseLabels = document.getElementById("phase-labels"); |
| var messagesEl = document.getElementById("messages"); |
| var typingEl = document.getElementById("typing"); |
| var chatInput = document.getElementById("chat-input"); |
| var btnSend = document.getElementById("btn-send"); |
| var btnEnd = document.getElementById("btn-end-session"); |
| var btnReset = document.getElementById("btn-reset"); |
|
|
| var scoresGrid = document.getElementById("scores-grid"); |
| var summaryEl = document.getElementById("analysis-summary"); |
| var strengthsEl = document.getElementById("analysis-strengths"); |
| var weaknessesEl = document.getElementById("analysis-weaknesses"); |
| var rhythmCount = document.getElementById("rhythm-count"); |
| var btnExport = document.getElementById("btn-export"); |
| var btnNewSession = document.getElementById("btn-new-session"); |
|
|
| |
| var uploadZone = document.getElementById("upload-zone"); |
| var fileInput = document.getElementById("file-input"); |
| var uploadList = document.getElementById("upload-list"); |
| var uploadStatus = document.getElementById("upload-status"); |
|
|
| |
| function showScreen(screen) { |
| setupScreen.classList.remove("active"); |
| chatScreen.classList.remove("active"); |
| analysisScreen.classList.remove("active"); |
| screen.classList.add("active"); |
| } |
|
|
| |
| function renderPhaseIndicator() { |
| phaseDots.innerHTML = ""; |
| phaseLabels.innerHTML = ""; |
|
|
| for (var i = 0; i < 5; i++) { |
| if (i > 0) { |
| var conn = document.createElement("div"); |
| conn.className = "phase-connector" + (i <= state.phase ? " done" : ""); |
| phaseDots.appendChild(conn); |
| } |
| var dot = document.createElement("div"); |
| dot.className = "phase-dot"; |
| if (i === state.phase) dot.className += " active"; |
| else if (i < state.phase) dot.className += " done"; |
| dot.textContent = i; |
| phaseDots.appendChild(dot); |
|
|
| var lbl = document.createElement("div"); |
| lbl.className = "phase-label-text" + (i === state.phase ? " active" : ""); |
| lbl.textContent = PHASE_NAMES[i]; |
| phaseLabels.appendChild(lbl); |
| } |
| } |
|
|
| |
| function stripPhaseMarker(text) { |
| |
| var idx = text.indexOf("\n---"); |
| if (idx === -1) idx = text.indexOf("---\nPhase"); |
| return idx >= 0 ? text.substring(0, idx).trim() : text; |
| } |
|
|
| function addMessage(role, content) { |
| var div = document.createElement("div"); |
| div.className = "message " + role; |
| div.textContent = role === "assistant" ? stripPhaseMarker(content) : content; |
| messagesEl.insertBefore(div, typingEl); |
| messagesEl.scrollTop = messagesEl.scrollHeight; |
| } |
|
|
| function setTyping(on) { |
| typingEl.style.display = on ? "block" : "none"; |
| if (on) messagesEl.scrollTop = messagesEl.scrollHeight; |
| } |
|
|
| |
|
|
| function formatFileSize(bytes) { |
| if (bytes < 1024) return bytes + " o"; |
| if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " Ko"; |
| return (bytes / (1024 * 1024)).toFixed(1) + " Mo"; |
| } |
|
|
| function renderUploadList() { |
| uploadList.innerHTML = ""; |
| state.uploadedDocs.forEach(function (doc) { |
| var item = document.createElement("div"); |
| item.className = "upload-item"; |
|
|
| var icon = doc.filename.toLowerCase().endsWith(".pdf") ? "PDF" : |
| doc.filename.toLowerCase().endsWith(".pptx") ? "PPT" : |
| doc.filename.toLowerCase().endsWith(".ppt") ? "PPT" : "TXT"; |
|
|
| item.innerHTML = |
| '<span class="upload-item-icon">' + icon + '</span>' + |
| '<span class="upload-item-name">' + doc.filename + '</span>' + |
| '<span class="upload-item-chunks">' + doc.chunks + ' chunks</span>' + |
| '<button class="upload-item-delete" data-filename="' + doc.filename + '">X</button>'; |
| uploadList.appendChild(item); |
| }); |
|
|
| |
| uploadList.querySelectorAll(".upload-item-delete").forEach(function (btn) { |
| btn.addEventListener("click", function () { |
| deleteDoc(btn.dataset.filename); |
| }); |
| }); |
| } |
|
|
| function uploadFiles(fileList) { |
| if (!fileList || fileList.length === 0) return; |
|
|
| var formData = new FormData(); |
| for (var i = 0; i < fileList.length; i++) { |
| formData.append("files", fileList[i]); |
| } |
|
|
| uploadStatus.textContent = "Upload en cours..."; |
| uploadStatus.className = "upload-status uploading"; |
| uploadZone.classList.add("uploading"); |
|
|
| fetch("/api/upload", { |
| method: "POST", |
| body: formData |
| }) |
| .then(function (res) { return res.json(); }) |
| .then(function (data) { |
| uploadZone.classList.remove("uploading"); |
| var ok = 0; |
| var errors = []; |
|
|
| (data.results || []).forEach(function (r) { |
| if (r.status === "ok") { |
| ok++; |
| state.uploadedDocs.push({ filename: r.filename, chunks: r.chunks }); |
| } else { |
| errors.push(r.filename + ": " + (r.message || "erreur")); |
| } |
| }); |
|
|
| (data.skipped || []).forEach(function (s) { |
| errors.push(s.filename + ": " + s.reason); |
| }); |
|
|
| if (ok > 0 && errors.length === 0) { |
| uploadStatus.textContent = ok + " fichier(s) ajoute(s) au corpus"; |
| uploadStatus.className = "upload-status success"; |
| } else if (ok > 0 && errors.length > 0) { |
| uploadStatus.textContent = ok + " OK, " + errors.length + " erreur(s): " + errors.join("; "); |
| uploadStatus.className = "upload-status warning"; |
| } else { |
| uploadStatus.textContent = "Erreur: " + errors.join("; "); |
| uploadStatus.className = "upload-status error"; |
| } |
|
|
| renderUploadList(); |
| }) |
| .catch(function () { |
| uploadZone.classList.remove("uploading"); |
| uploadStatus.textContent = "Erreur de connexion. Reessaye."; |
| uploadStatus.className = "upload-status error"; |
| }); |
| } |
|
|
| function deleteDoc(filename) { |
| fetch("/api/documents/" + encodeURIComponent(filename), { method: "DELETE" }) |
| .then(function (res) { return res.json(); }) |
| .then(function () { |
| state.uploadedDocs = state.uploadedDocs.filter(function (d) { |
| return d.filename !== filename; |
| }); |
| renderUploadList(); |
| uploadStatus.textContent = filename + " supprime"; |
| uploadStatus.className = "upload-status success"; |
| }); |
| } |
|
|
| |
| uploadZone.addEventListener("click", function () { |
| fileInput.click(); |
| }); |
|
|
| fileInput.addEventListener("change", function () { |
| uploadFiles(fileInput.files); |
| fileInput.value = ""; |
| }); |
|
|
| uploadZone.addEventListener("dragover", function (e) { |
| e.preventDefault(); |
| uploadZone.classList.add("dragover"); |
| }); |
|
|
| uploadZone.addEventListener("dragleave", function () { |
| uploadZone.classList.remove("dragover"); |
| }); |
|
|
| uploadZone.addEventListener("drop", function (e) { |
| e.preventDefault(); |
| uploadZone.classList.remove("dragover"); |
| uploadFiles(e.dataTransfer.files); |
| }); |
|
|
| |
| function sendMessage(text) { |
| state.history.push({ role: "user", content: text }); |
| state.timestamps.push(Date.now()); |
| addMessage("user", text); |
|
|
| chatInput.value = ""; |
| btnSend.disabled = true; |
| setTyping(true); |
|
|
| fetch("/api/chat", { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ |
| message: text, |
| mode: state.mode, |
| topic: state.topic, |
| phase: state.phase, |
| phase_turns: state.phaseTurns, |
| lang: currentLang, |
| history: state.history.slice(0, -1) |
| }) |
| }) |
| .then(function (res) { |
| if (!res.ok) { |
| return res.json().then(function (err) { |
| throw new Error(err.error || "Erreur serveur " + res.status); |
| }); |
| } |
| return res.json(); |
| }) |
| .then(function (data) { |
| setTyping(false); |
| if (!data.reply) { |
| addMessage("assistant", t("errorEmpty")); |
| btnSend.disabled = false; |
| return; |
| } |
| state.phase = data.phase; |
| state.phaseTurns = data.phase_turns || 0; |
| state.history.push({ role: "assistant", content: data.reply }); |
| state.timestamps.push(Date.now()); |
| addMessage("assistant", data.reply); |
| renderPhaseIndicator(); |
| btnSend.disabled = false; |
| chatInput.focus(); |
| }) |
| .catch(function (err) { |
| setTyping(false); |
| console.error("sendMessage error:", err); |
| addMessage("assistant", err.message || t("errorConnection")); |
| btnSend.disabled = false; |
| }); |
| } |
|
|
| function requestAnalysis() { |
| btnEnd.disabled = true; |
| setTyping(true); |
|
|
| fetch("/api/analyze", { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ |
| history: state.history, |
| timestamps: state.timestamps |
| }) |
| }) |
| .then(function (res) { return res.json(); }) |
| .then(function (data) { |
| setTyping(false); |
| state.analysisResult = data; |
| renderAnalysis(data); |
| showScreen(analysisScreen); |
| }) |
| .catch(function () { |
| setTyping(false); |
| btnEnd.disabled = false; |
| alert(t("errorAnalysis")); |
| }); |
| } |
|
|
| |
| function renderAnalysis(data) { |
| var scoreKeys = ["reasoningScore", "clarityScore", "skepticismScore", "processScore", "reflectionScore", "integrityScore"]; |
| var labels = t("scoreLabels"); |
| var scores = scoreKeys.map(function (key, i) { |
| return { key: key, label: labels[i] }; |
| }); |
|
|
| scoresGrid.innerHTML = ""; |
| scores.forEach(function (s) { |
| var val = data[s.key] || 0; |
| var card = document.createElement("div"); |
| card.className = "score-card"; |
| card.innerHTML = |
| '<div class="score-value">' + val + '</div>' + |
| '<div class="score-label">' + s.label + '</div>' + |
| '<div class="score-bar"><div class="score-bar-fill" style="width:' + val + '%"></div></div>'; |
| scoresGrid.appendChild(card); |
| }); |
|
|
| summaryEl.textContent = data.summary || t("noSummary"); |
|
|
| strengthsEl.innerHTML = ""; |
| (data.keyStrengths || []).forEach(function (s) { |
| var li = document.createElement("li"); |
| li.textContent = s; |
| strengthsEl.appendChild(li); |
| }); |
|
|
| weaknessesEl.innerHTML = ""; |
| (data.weaknesses || []).forEach(function (w) { |
| var li = document.createElement("li"); |
| li.textContent = w; |
| weaknessesEl.appendChild(li); |
| }); |
|
|
| rhythmCount.textContent = data.rhythmBreakCount || 0; |
| } |
|
|
| |
| function exportJSON() { |
| var payload = { |
| mode: state.mode, |
| topic: state.topic, |
| messages: state.history, |
| timestamps: state.timestamps, |
| scores: state.analysisResult |
| }; |
| var blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); |
| var url = URL.createObjectURL(blob); |
| var a = document.createElement("a"); |
| a.href = url; |
| a.download = "aim-session-" + new Date().toISOString().slice(0, 10) + ".json"; |
| a.click(); |
| URL.revokeObjectURL(url); |
| } |
|
|
| |
| function resetSession() { |
| state.mode = "TUTOR"; |
| state.topic = ""; |
| state.phase = 0; |
| state.phaseTurns = 0; |
| state.history = []; |
| state.timestamps = []; |
| state.analysisResult = null; |
| state.uploadedDocs = []; |
|
|
| topicInput.value = ""; |
| chatInput.value = ""; |
| messagesEl.querySelectorAll(".message").forEach(function (el) { el.remove(); }); |
| uploadList.innerHTML = ""; |
| uploadStatus.textContent = ""; |
|
|
| modeBtns.forEach(function (btn) { |
| btn.classList.toggle("selected", btn.dataset.mode === "TUTOR"); |
| }); |
|
|
| btnStart.disabled = true; |
| btnEnd.disabled = false; |
| btnSend.disabled = false; |
|
|
| |
| loadDocumentList(); |
|
|
| showScreen(setupScreen); |
| } |
|
|
| |
| function loadDocumentList() { |
| fetch("/api/documents") |
| .then(function (res) { return res.json(); }) |
| .then(function (data) { |
| state.uploadedDocs = (data.documents || []).map(function (d) { |
| return { filename: d.filename, chunks: "?" }; |
| }); |
| renderUploadList(); |
| }) |
| .catch(function () {}); |
| } |
|
|
| |
|
|
| |
| modeBtns.forEach(function (btn) { |
| btn.addEventListener("click", function () { |
| modeBtns.forEach(function (b) { b.classList.remove("selected"); }); |
| btn.classList.add("selected"); |
| state.mode = btn.dataset.mode; |
| }); |
| }); |
|
|
| |
| topicInput.addEventListener("input", function () { |
| btnStart.disabled = !topicInput.value.trim(); |
| }); |
|
|
| |
| btnStart.addEventListener("click", function () { |
| var topic = topicInput.value.trim(); |
| if (!topic) return; |
|
|
| state.topic = topic; |
| state.phase = 0; |
| state.phaseTurns = 0; |
| state.history = []; |
| state.timestamps = []; |
| modeBadge.textContent = state.mode === "TUTOR" ? t("modeBadgeTutor") : t("modeBadgeCritic"); |
| topicBadge.textContent = topic; |
|
|
| |
| if (state.uploadedDocs.length > 0) { |
| docsBadge.textContent = state.uploadedDocs.length + " doc(s)"; |
| docsBadge.style.display = "inline-block"; |
| } else { |
| docsBadge.style.display = "none"; |
| } |
|
|
| renderPhaseIndicator(); |
| showScreen(chatScreen); |
| chatInput.focus(); |
|
|
| |
| startSession(); |
| }); |
|
|
| function startSession() { |
| setTyping(true); |
| btnSend.disabled = true; |
|
|
| fetch("/api/chat", { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ |
| message: t("startMessage") + state.topic, |
| mode: state.mode, |
| topic: state.topic, |
| phase: state.phase, |
| phase_turns: state.phaseTurns, |
| lang: currentLang, |
| history: [] |
| }) |
| }) |
| .then(function (res) { |
| if (!res.ok) { |
| return res.json().then(function (err) { |
| throw new Error(err.error || "Erreur serveur " + res.status); |
| }); |
| } |
| return res.json(); |
| }) |
| .then(function (data) { |
| setTyping(false); |
| if (!data.reply) { |
| addMessage("assistant", t("errorEmpty")); |
| btnSend.disabled = false; |
| return; |
| } |
| state.phase = data.phase; |
| state.phaseTurns = data.phase_turns || 0; |
| state.history.push({ role: "assistant", content: data.reply }); |
| state.timestamps.push(Date.now()); |
| addMessage("assistant", data.reply); |
| renderPhaseIndicator(); |
| btnSend.disabled = false; |
| chatInput.focus(); |
| }) |
| .catch(function (err) { |
| setTyping(false); |
| console.error("startSession error:", err); |
| addMessage("assistant", err.message || t("errorConnection")); |
| btnSend.disabled = false; |
| }); |
| } |
|
|
| |
| btnSend.addEventListener("click", function () { |
| var text = chatInput.value.trim(); |
| if (text) sendMessage(text); |
| }); |
|
|
| chatInput.addEventListener("keydown", function (e) { |
| if (e.key === "Enter") { |
| var text = chatInput.value.trim(); |
| if (text) sendMessage(text); |
| } |
| }); |
|
|
| |
| btnEnd.addEventListener("click", function () { |
| if (state.history.length === 0) { |
| alert(t("errorNoExchange")); |
| return; |
| } |
| requestAnalysis(); |
| }); |
|
|
| |
| btnReset.addEventListener("click", resetSession); |
|
|
| |
| btnExport.addEventListener("click", exportJSON); |
|
|
| |
| btnNewSession.addEventListener("click", resetSession); |
|
|
| |
| loadDocumentList(); |
|
|
| |
| applyLanguage(); |
|
|
| })(); |
|
|