monitor/html/js/record.js
fds2610-bot d0d0b87af9 Aufnahmeseite: nach dem Upload "gespeichert" statt "verworfen"
Nur der Klick auf Verwerfen zeigt noch "Aufnahme verworfen."

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 13:43:52 +02:00

154 lines
5.4 KiB
JavaScript

// 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();