Files

95 lines
2.8 KiB
TypeScript
Raw Permalink Normal View History

'use client';
import { useState, useCallback, useRef, type MouseEvent } from 'react';
import { fileToBase64 } from '@/lib/copilot';
interface VoiceInputProps {
onVoice: (audioBase64: string, mimeType: string) => void;
disabled?: boolean;
}
declare global {
interface Window {
SpeechRecognition?: new () => SpeechRecognitionLike;
webkitSpeechRecognition?: new () => SpeechRecognitionLike;
}
}
interface SpeechRecognitionLike {
start: () => void;
stop: () => void;
onresult: ((event: SpeechRecognitionEventLike) => void) | null;
onerror: (() => void) | null;
onend: (() => void) | null;
}
interface SpeechRecognitionEventLike {
results: { length: number; [key: number]: { 0: { transcript: string } } };
}
export function VoiceInput({ onVoice, disabled }: VoiceInputProps) {
const [recording, setRecording] = useState(false);
const [supported, setSupported] = useState(true);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const chunksRef = useRef<Blob[]>([]);
const handleStartRecording = useCallback(async () => {
if (disabled) return;
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const recorder = new MediaRecorder(stream);
chunksRef.current = [];
recorder.ondataavailable = (e) => {
if (e.data.size > 0) chunksRef.current.push(e.data);
};
recorder.onstop = async () => {
const blob = new Blob(chunksRef.current, { type: 'audio/webm' });
const base64 = await fileToBase64(blob);
onVoice(base64, 'audio/webm');
stream.getTracks().forEach((track) => track.stop());
};
recorder.start();
mediaRecorderRef.current = recorder;
setRecording(true);
} catch {
setSupported(false);
}
}, [disabled, onVoice]);
const handleStopRecording = useCallback(() => {
if (mediaRecorderRef.current && recording) {
mediaRecorderRef.current.stop();
setRecording(false);
}
}, [recording]);
const handleToggle = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (recording) handleStopRecording();
else handleStartRecording();
},
[recording, handleStartRecording, handleStopRecording]
);
if (!supported) {
return (
<button disabled className="bg-gray-300 text-gray-500 px-3 py-2 rounded-lg cursor-not-allowed" data-testid="voice-button">
🎤
</button>
);
}
return (
<button
onClick={handleToggle}
disabled={disabled}
className={`${recording ? 'bg-red-600 animate-pulse' : 'bg-gray-200 hover:bg-gray-300'} text-gray-800 px-3 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed`}
title={recording ? 'Aufnahme stoppen' : 'Sprachnachricht aufnehmen'}
data-testid="voice-button"
>
🎤
</button>
);
}