204 lines
6.1 KiB
JavaScript
204 lines
6.1 KiB
JavaScript
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");
|
|
}
|