const conversation = document.querySelector("#conversation"); const statusDot = document.querySelector("#statusDot"); const statusText = document.querySelector("#statusText"); const textInput = document.querySelector("#textInput"); const sendButton = document.querySelector("#sendButton"); const voiceButton = document.querySelector("#voiceButton"); const voiceLabel = document.querySelector("#voiceLabel"); const resetButton = document.querySelector("#resetButton"); const audioToggle = document.querySelector("#audioToggle"); const sessionId = getSessionId(); let recorder = null; let chunks = []; let isRecording = false; let audioUnlocked = false; if ("serviceWorker" in navigator) { navigator.serviceWorker.register("/sw.js").catch(() => {}); } sendButton.addEventListener("click", sendText); voiceButton.addEventListener("click", toggleVoice); resetButton.addEventListener("click", resetConversation); textInput.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); sendText(); } }); function getSessionId() { const key = "opensquared_assistant_session"; let value = localStorage.getItem(key); if (!value) { if (window.crypto && typeof window.crypto.randomUUID === "function") { value = window.crypto.randomUUID(); } else { value = `session-${Date.now()}-${Math.random().toString(16).slice(2)}`; } localStorage.setItem(key, value); } return value; } function setStatus(text, mode = "idle") { statusText.textContent = text; statusDot.classList.toggle("live", mode === "live"); statusDot.classList.toggle("error", mode === "error"); } function addBubble(text, role = "assistant") { const bubble = document.createElement("article"); bubble.className = `bubble ${role}`; bubble.textContent = text; conversation.appendChild(bubble); conversation.scrollTop = conversation.scrollHeight; return bubble; } async function unlockAudio() { if (audioUnlocked) return; try { const AudioContext = window.AudioContext || window.webkitAudioContext; if (AudioContext) { const context = new AudioContext(); if (context.state === "suspended") { await context.resume(); } await context.close(); } } catch { // The click itself is enough to authorize later playback in most browsers. } audioUnlocked = true; } async function sendText() { const text = textInput.value.trim(); if (!text) return; textInput.value = ""; addBubble(text, "user"); setStatus("Analyse en cours", "live"); try { const response = await fetch("/api/chat", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ sessionId, text, withAudio: audioToggle.checked, }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error || "Erreur serveur"); await handleAssistantResponse(data); } catch (error) { addBubble(error.message, "error"); setStatus("Erreur", "error"); } } async function toggleVoice() { setStatus("Activation du micro", "live"); await unlockAudio(); if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { addBubble("Le micro n'est pas disponible dans ce navigateur. Ouvre l'app en HTTPS avec Chrome.", "error"); setStatus("Micro indisponible", "error"); return; } if (typeof MediaRecorder === "undefined") { addBubble("L'enregistrement audio n'est pas supporte par ce navigateur.", "error"); setStatus("Enregistrement indisponible", "error"); return; } if (isRecording) { recorder.stop(); return; } try { const stream = await navigator.mediaDevices.getUserMedia({audio: true}); const mimeType = pickMimeType(); recorder = new MediaRecorder(stream, mimeType ? {mimeType} : undefined); chunks = []; recorder.ondataavailable = (event) => { if (event.data.size) chunks.push(event.data); }; recorder.onstop = async () => { stream.getTracks().forEach((track) => track.stop()); isRecording = false; voiceButton.classList.remove("recording"); voiceLabel.textContent = "Demarrer"; await sendVoiceBlob(new Blob(chunks, {type: recorder.mimeType || "audio/webm"})); }; recorder.start(); isRecording = true; voiceButton.classList.add("recording"); voiceLabel.textContent = "Stop"; setStatus("Je t'ecoute", "live"); } catch (error) { addBubble("Micro indisponible : " + error.message, "error"); setStatus("Micro indisponible", "error"); } } function pickMimeType() { const types = ["audio/webm;codecs=opus", "audio/webm", "audio/ogg;codecs=opus"]; return types.find((type) => MediaRecorder.isTypeSupported(type)) || ""; } async function sendVoiceBlob(blob) { addBubble("Message vocal envoye", "user"); setStatus("Transcription", "live"); const form = new FormData(); const extension = blob.type.includes("ogg") ? "ogg" : "webm"; form.append("sessionId", sessionId); form.append("audio", blob, `voice.${extension}`); try { const response = await fetch("/api/voice", {method: "POST", body: form}); const data = await response.json(); if (!response.ok) throw new Error(data.error || "Erreur serveur"); if (data.transcript) addBubble(data.transcript, "user"); await handleAssistantResponse(data); } catch (error) { addBubble(error.message, "error"); setStatus("Erreur", "error"); } } async function handleAssistantResponse(data) { addBubble(data.reply || "", "assistant"); setStatus(data.emailSent ? "Email envoye" : "Pret", data.emailSent ? "live" : "idle"); if (audioToggle.checked && data.audioUrl) { try { const audio = new Audio(data.audioUrl); await audio.play(); } catch { addBubble("Audio pret, mais le navigateur a bloque la lecture automatique.", "error"); } } } async function resetConversation() { await fetch("/api/reset", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({sessionId}), }).catch(() => {}); conversation.innerHTML = ""; addBubble("Nouvelle conversation. Je t'ecoute.", "assistant"); setStatus("Conversation reinitialisee", "live"); }