MindSpark / components /VoiceRecorder.tsx
TheK3R1M's picture
Initial secure upload
fd4dc0d verified
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>
);
};