File size: 4,834 Bytes
fd4dc0d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | import React, { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
interface VoiceRecorderProps {
onTranscription: (text: string) => void;
isProcessing: boolean;
}
export const VoiceRecorder: React.FC<VoiceRecorderProps> = ({ onTranscription, isProcessing }) => {
const [isRecording, setIsRecording] = useState(false);
const [recordingTime, setRecordingTime] = useState(0);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const chunksRef = useRef<Blob[]>([]);
const timerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (isRecording) {
timerRef.current = setInterval(() => {
setRecordingTime(prev => prev + 1);
}, 1000);
} else {
if (timerRef.current) clearInterval(timerRef.current);
setRecordingTime(0);
}
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, [isRecording]);
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mediaRecorder = new MediaRecorder(stream);
mediaRecorderRef.current = mediaRecorder;
chunksRef.current = [];
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) {
chunksRef.current.push(e.data);
}
};
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(chunksRef.current, { type: 'audio/webm' });
const reader = new FileReader();
reader.onloadend = () => {
const base64Audio = (reader.result as string).split(',')[1];
handleTranscription(base64Audio);
};
reader.readAsDataURL(audioBlob);
// Stop all tracks
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start();
setIsRecording(true);
} catch (err) {
console.error("Microphone access denied:", err);
alert("Mikrofon erişimi reddedildi.");
}
};
const stopRecording = () => {
if (mediaRecorderRef.current && isRecording) {
mediaRecorderRef.current.stop();
setIsRecording(false);
}
};
const handleTranscription = async (base64Audio: string) => {
// We'll use Gemini to transcribe.
// Since we don't have a direct transcription API here,
// we'll send it as a prompt with audio data.
try {
// This will be handled in App.tsx or a service
onTranscription(base64Audio);
} catch (error) {
console.error("Transcription failed:", error);
}
};
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
return (
<div className="flex items-center gap-4">
<AnimatePresence>
{isRecording && (
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="flex items-center gap-3 bg-red-500/10 border border-red-500/20 px-4 py-2 rounded-2xl"
>
<div className="w-2 h-2 rounded-full bg-red-500 animate-pulse"></div>
<span className="text-xs font-black text-red-400 font-mono">{formatTime(recordingTime)}</span>
</motion.div>
)}
</AnimatePresence>
<button
onClick={isRecording ? stopRecording : startRecording}
disabled={isProcessing}
className={`w-12 h-12 rounded-2xl flex items-center justify-center transition-all duration-500 shadow-lg ${
isRecording
? 'bg-red-500 text-white animate-pulse shadow-red-500/30'
: 'bg-white/5 text-slate-400 hover:bg-indigo-600 hover:text-white border border-white/5'
} ${isProcessing ? 'opacity-50 cursor-not-allowed' : ''}`}
title={isRecording ? "Kaydı Durdur" : "Sesli Not Al"}
>
{isProcessing ? (
<div className="w-5 h-5 border-2 border-current border-t-transparent rounded-full animate-spin"></div>
) : isRecording ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z" clipRule="evenodd" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M7 4a3 3 0 016 0v4a3 3 0 11-6 0V4zm4 10.93A7.001 7.001 0 0017 8a1 1 0 10-2 0A5 5 0 015 8a1 1 0 00-2 0 7.001 7.001 0 006 6.93V17H6a1 1 0 100 2h8a1 1 0 100-2h-3v-2.07z" clipRule="evenodd" />
</svg>
)}
</button>
</div>
);
};
|