Nur der Klick auf Verwerfen zeigt noch "Aufnahme verworfen." Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
154 lines
5.4 KiB
JavaScript
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();
|