// Aufnahme-Webseite fuer Sprachnotizen. Die Aufnahme geht in eine Warteschlange fuer alle, // nicht an einen bestimmten Agenten: abgeholt wird sie vom Voice-Bot, und in welchem Repo // daraus ein Ticket entsteht, entscheidet er anhand des Transkripts. // Laeuft im Handy-Browser eines Menschen, getrennt von der Buero-Visualisierung // (office.html/office-render.js) - reines Vanilla-JS/DOM ohne Bundler, wie der Rest des // Frontends. navigator.mediaDevices.getUserMedia()/MediaRecorder verlangen HTTPS (Ausnahme: // localhost) - ohne das bricht init() kontrolliert mit einer Fehlermeldung ab, statt dass der // Start-Knopf stumm nichts tut. import { fetchVoiceAgent, uploadRecording } from "./office-api.js"; const targetEl = document.getElementById("record-target"); const statusEl = document.getElementById("status"); const startButton = document.getElementById("start-button"); const stopButton = document.getElementById("stop-button"); const uploadButton = document.getElementById("upload-button"); const discardButton = document.getElementById("discard-button"); const audioPreview = document.getElementById("audio-preview"); // MediaRecorder erzeugt je nach Browser unterschiedliche Container/Codecs - der erste vom // Browser unterstuetzte Typ gewinnt. webm/Opus zuerst (Chrome/Firefox/Desktop), mp4/AAC als // Fallback fuer Safari/iOS (kennt kein webm). Muss zur Whitelist in // VoiceRecordingRepository::ALLOWED_MIME_TYPES passen. const CANDIDATE_MIME_TYPES = ["audio/webm;codecs=opus", "audio/webm", "audio/mp4", "audio/ogg;codecs=opus"]; let mediaRecorder = null; let mediaStream = null; let recordedChunks = []; let recordedBlob = null; function setStatus(message, isError = false) { statusEl.textContent = message; statusEl.classList.toggle("status-error", isError); } function pickMimeType() { if (typeof MediaRecorder === "undefined" || typeof MediaRecorder.isTypeSupported !== "function") { return ""; } return CANDIDATE_MIME_TYPES.find((type) => MediaRecorder.isTypeSupported(type)) || ""; } async function init() { try { const voiceAgent = await fetchVoiceAgent(); if (voiceAgent) { const name = voiceAgent.display_name ?? voiceAgent.name ?? "Voice-Bot"; // Ein abwesender Bot ist kein Grund, die Aufnahme zu verhindern - sie wartet dann // eben, bis er wieder da ist. Gesagt wird es trotzdem, sonst wundert man sich, warum // kein Ticket entsteht. targetEl.textContent = voiceAgent.is_present === false ? `${name} (zurzeit nicht erreichbar - die Aufnahme wartet)` : name; } else { targetEl.textContent = "kein Voice-Bot registriert - die Aufnahme bleibt liegen"; } } catch { // API evtl. gerade nicht erreichbar - der Upload braucht sie erst am Ende, die Aufnahme // selbst funktioniert auch ohne diese Anzeige, deshalb hier nicht hart blockieren. targetEl.textContent = "Voice-Bot"; } if (!navigator.mediaDevices || typeof navigator.mediaDevices.getUserMedia !== "function") { setStatus("Aufnahme wird von diesem Browser nicht unterstuetzt (HTTPS oder localhost noetig).", true); startButton.disabled = true; } } async function startRecording() { recordedChunks = []; recordedBlob = null; audioPreview.hidden = true; uploadButton.hidden = true; discardButton.hidden = true; try { mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true }); } catch (error) { setStatus(`Mikrofon-Zugriff nicht möglich: ${error.message}`, true); return; } const mimeType = pickMimeType(); mediaRecorder = mimeType ? new MediaRecorder(mediaStream, { mimeType }) : new MediaRecorder(mediaStream); mediaRecorder.addEventListener("dataavailable", (event) => { if (event.data.size > 0) { recordedChunks.push(event.data); } }); mediaRecorder.addEventListener("stop", () => { mediaStream.getTracks().forEach((track) => track.stop()); recordedBlob = new Blob(recordedChunks, { type: mediaRecorder.mimeType || "audio/webm" }); audioPreview.src = URL.createObjectURL(recordedBlob); audioPreview.hidden = false; uploadButton.hidden = false; discardButton.hidden = false; setStatus("Aufnahme beendet - anhören und hochladen oder verwerfen."); }); mediaRecorder.start(); startButton.hidden = true; stopButton.hidden = false; setStatus("Aufnahme läuft …"); } function stopRecording() { if (mediaRecorder && mediaRecorder.state !== "inactive") { mediaRecorder.stop(); } stopButton.hidden = true; startButton.hidden = false; } function resetRecording() { recordedBlob = null; audioPreview.removeAttribute("src"); audioPreview.hidden = true; uploadButton.hidden = true; discardButton.hidden = true; } function discardRecording() { resetRecording(); setStatus("Aufnahme verworfen."); } async function upload() { if (!recordedBlob) { return; } uploadButton.disabled = true; setStatus("Wird hochgeladen …"); const extension = (recordedBlob.type.split("/")[1] || "webm").split(";")[0]; try { await uploadRecording(recordedBlob, `aufnahme.${extension}`); resetRecording(); setStatus("Aufnahme gespeichert, danke!"); } catch (error) { setStatus(error.message, true); } finally { uploadButton.disabled = false; } } startButton.addEventListener("click", startRecording); stopButton.addEventListener("click", stopRecording); uploadButton.addEventListener("click", upload); discardButton.addEventListener("click", discardRecording); init();