| 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); |
| |
| |
| 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) => { |
| |
| |
| |
| try { |
| |
| 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> |
| ); |
| }; |
|
|