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) { value = crypto.randomUUID(); 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; const audio = new Audio(); audio.muted = true; try { await audio.play(); } catch { // Mobile browsers only need the attempt to happen inside a tap handler. } 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() { await unlockAudio(); 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"); }