import React, { useState, useRef } from 'react'; import { Mic, Square, Send, Loader2 } from 'lucide-react'; import { apiService } from '../services/api'; interface HomeInputProps { onRecordComplete?: () => void; } /** * Home page input component for recording inspirations * This component allows users to record voice or type text * The input is processed and split into inspirations, todos, and moods */ export function HomeInput({ onRecordComplete }: HomeInputProps) { const [isRecording, setIsRecording] = useState(false); const [textInput, setTextInput] = useState(''); const [processing, setProcessing] = useState(false); const [error, setError] = useState(null); const [showSuccess, setShowSuccess] = useState(false); const mediaRecorderRef = useRef(null); const audioChunksRef = useRef([]); const startRecording = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // Try to use audio/wav if supported, otherwise use default let options: MediaRecorderOptions = { mimeType: 'audio/webm' }; if (MediaRecorder.isTypeSupported('audio/wav')) { options = { mimeType: 'audio/wav' }; } else if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) { options = { mimeType: 'audio/webm;codecs=opus' }; } else if (MediaRecorder.isTypeSupported('audio/webm')) { options = { mimeType: 'audio/webm' }; } const mediaRecorder = new MediaRecorder(stream, options); mediaRecorderRef.current = mediaRecorder; audioChunksRef.current = []; mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { audioChunksRef.current.push(event.data); } }; mediaRecorder.onstop = async () => { const mimeType = mediaRecorder.mimeType; const audioBlob = new Blob(audioChunksRef.current, { type: mimeType }); await processAudio(audioBlob, mimeType); // Stop all tracks stream.getTracks().forEach(track => track.stop()); }; mediaRecorder.start(); setIsRecording(true); setError(null); setShowSuccess(false); } catch (err) { console.error('Failed to start recording:', err); setError('无法访问麦克风,请检查权限设置'); } }; const stopRecording = () => { if (mediaRecorderRef.current && isRecording) { mediaRecorderRef.current.stop(); setIsRecording(false); } }; const processAudio = async (audioBlob: Blob, mimeType: string) => { setProcessing(true); setError(null); try { // Determine file extension based on mime type let extension = 'webm'; let fileName = 'recording.webm'; if (mimeType.includes('wav')) { extension = 'wav'; fileName = 'recording.wav'; } else if (mimeType.includes('mp4') || mimeType.includes('m4a')) { extension = 'm4a'; fileName = 'recording.m4a'; } else if (mimeType.includes('mpeg') || mimeType.includes('mp3')) { extension = 'mp3'; fileName = 'recording.mp3'; } // If it's webm, we need to convert it or send with proper naming // For now, we'll try to send as wav by renaming (browser may support it) if (extension === 'webm') { // Try to convert webm to wav using Web Audio API try { const wavBlob = await convertWebmToWav(audioBlob); const file = new File([wavBlob], 'recording.wav', { type: 'audio/wav' }); await apiService.processInput(file); } catch (conversionError) { console.error('Conversion failed, trying direct upload:', conversionError); // Fallback: try sending as mp3 (some backends might accept webm as mp3) const file = new File([audioBlob], 'recording.mp3', { type: 'audio/mpeg' }); await apiService.processInput(file); } } else { const file = new File([audioBlob], fileName, { type: mimeType }); await apiService.processInput(file); } // Show success message setShowSuccess(true); setTimeout(() => setShowSuccess(false), 3000); if (onRecordComplete) { onRecordComplete(); } } catch (err: any) { console.error('Failed to process audio:', err); setError(err.message || '处理失败,请重试'); } finally { setProcessing(false); } }; // Convert webm to wav using Web Audio API const convertWebmToWav = async (webmBlob: Blob): Promise => { const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); const arrayBuffer = await webmBlob.arrayBuffer(); const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); // Convert to WAV format const wavBuffer = audioBufferToWav(audioBuffer); return new Blob([wavBuffer], { type: 'audio/wav' }); }; // Convert AudioBuffer to WAV format const audioBufferToWav = (buffer: AudioBuffer): ArrayBuffer => { const length = buffer.length * buffer.numberOfChannels * 2 + 44; const arrayBuffer = new ArrayBuffer(length); const view = new DataView(arrayBuffer); const channels: Float32Array[] = []; let offset = 0; let pos = 0; // Write WAV header const setUint16 = (data: number) => { view.setUint16(pos, data, true); pos += 2; }; const setUint32 = (data: number) => { view.setUint32(pos, data, true); pos += 4; }; // RIFF identifier setUint32(0x46464952); // File length setUint32(length - 8); // RIFF type setUint32(0x45564157); // Format chunk identifier setUint32(0x20746d66); // Format chunk length setUint32(16); // Sample format (raw) setUint16(1); // Channel count setUint16(buffer.numberOfChannels); // Sample rate setUint32(buffer.sampleRate); // Byte rate setUint32(buffer.sampleRate * buffer.numberOfChannels * 2); // Block align setUint16(buffer.numberOfChannels * 2); // Bits per sample setUint16(16); // Data chunk identifier setUint32(0x61746164); // Data chunk length setUint32(length - pos - 4); // Write interleaved data for (let i = 0; i < buffer.numberOfChannels; i++) { channels.push(buffer.getChannelData(i)); } while (pos < length) { for (let i = 0; i < buffer.numberOfChannels; i++) { let sample = Math.max(-1, Math.min(1, channels[i][offset])); sample = sample < 0 ? sample * 0x8000 : sample * 0x7fff; view.setInt16(pos, sample, true); pos += 2; } offset++; } return arrayBuffer; }; const processText = async () => { if (!textInput.trim()) return; setProcessing(true); setError(null); try { await apiService.processInput(undefined, textInput); setTextInput(''); // Show success message setShowSuccess(true); setTimeout(() => setShowSuccess(false), 3000); if (onRecordComplete) { onRecordComplete(); } } catch (err: any) { console.error('Failed to process text:', err); setError(err.message || '处理失败,请重试'); } finally { setProcessing(false); } }; return (
{/* Voice Recording Button */}
{/* Recording status text */} {isRecording && (
正在录音... 点击停止
)} {/* Text Input */}
setTextInput(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && !e.shiftKey && processText()} placeholder="或者在这里输入文字..." disabled={processing || isRecording} className=" flex-1 px-5 py-3 rounded-full bg-white/90 backdrop-blur-sm border-2 border-slate-200 focus:outline-none focus:ring-2 focus:ring-purple-300 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-slate-400 transition-all duration-200 " />
{/* Processing Indicator */} {processing && (
正在分析...
)} {/* Success Message */} {showSuccess && (
记录成功!
)} {/* Error Message */} {error && (
{error}
)} {/* Hint text */}

说出或写下你的想法、灵感、待办事项
我会帮你整理和记录

); }