feat(T07): KI-Copilot with OpenRouter chat, voice input, action system, chat history
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user