modern frontend

This commit is contained in:
YannAhlgrim
2025-10-08 16:37:49 +02:00
parent c4352b7461
commit fd695cdf26
11 changed files with 1305 additions and 80 deletions
+145 -73
View File
@@ -1,97 +1,169 @@
import { useState, useRef } from 'react'
import { startRecording, stopRecording, sendAudioToBackend, playAudioBlob } from '../services/audioService'
export default function Home() {
const [recording, setRecording] = useState(false)
const [playing, setPlaying] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const mediaRecorderRef = useRef(null)
const audioChunksRef = useRef([])
const audioRef = useRef(null)
async function startRecording() {
if (!navigator.mediaDevices) return alert('No microphone available')
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
const mr = new MediaRecorder(stream)
mr.ondataavailable = (e) => audioChunksRef.current.push(e.data)
mr.onstop = async () => {
const blob = new Blob(audioChunksRef.current, { type: 'audio/webm' })
audioChunksRef.current = []
await sendAudio(blob)
}
mediaRecorderRef.current = mr
audioChunksRef.current = []
mr.start()
setRecording(true)
async function handleStartRecording() {
await startRecording(mediaRecorderRef, audioChunksRef, setRecording, handleSendAudio)
}
function stopRecording() {
if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') {
mediaRecorderRef.current.stop()
setRecording(false)
}
function handleStopRecording() {
stopRecording(mediaRecorderRef, setRecording)
}
async function sendAudio(blob) {
async function handleSendAudio(blob) {
try {
const form = new FormData()
// Convert webm to wav on the client is complex; many servers accept webm/ogg.
form.append('file', blob, 'recording.webm')
console.log('Sending request to backend...')
const res = await fetch('http://localhost:8000/chat', { method: 'POST', body: form })
console.log('Response received:', res.status, res.statusText)
if (!res.ok) {
const text = await res.text()
alert('Error: ' + res.status + ' ' + text)
return
}
console.log('Converting response to blob...')
const audioBlob = await res.blob()
console.log('Audio blob created, size:', audioBlob.size)
const url = URL.createObjectURL(audioBlob)
if (audioRef.current) {
audioRef.current.src = url
audioRef.current.play()
setPlaying(true)
audioRef.current.onended = () => setPlaying(false)
}
setIsProcessing(true)
const audioBlob = await sendAudioToBackend(blob)
playAudioBlob(audioBlob, audioRef, setPlaying)
} catch (error) {
console.error('Error in sendAudio:', error)
alert('Failed to process audio: ' + error.message)
// Show a more user-friendly error notification
const errorDiv = document.createElement('div')
errorDiv.className = 'fixed top-4 right-4 bg-red-500 text-white px-6 py-4 rounded-lg shadow-lg z-50 transform transition-all duration-300'
errorDiv.textContent = `Error: ${error.message}`
document.body.appendChild(errorDiv)
setTimeout(() => {
errorDiv.style.transform = 'translateX(100%)'
setTimeout(() => document.body.removeChild(errorDiv), 300)
}, 3000)
} finally {
setIsProcessing(false)
}
}
return (
<div style={{ padding: 24, fontFamily: 'sans-serif' }}>
<h1>Local Voice Assistant Frontend</h1>
<p>Press and hold the big button to record, or click to toggle.</p>
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 text-white">
{/* Header */}
<div className="container mx-auto px-6 py-8">
<div className="text-center mb-12">
<h1 className="text-5xl font-bold bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent mb-4">
🎤 Voice Assistant
</h1>
<p className="text-xl text-gray-300 max-w-2xl mx-auto">
Experience the future of voice interaction. Press and hold to record your voice,
or simply click to toggle recording mode.
</p>
</div>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<button
onMouseDown={startRecording}
onMouseUp={stopRecording}
onTouchStart={startRecording}
onTouchEnd={stopRecording}
onClick={() => (recording ? stopRecording() : startRecording())}
style={{
width: 140,
height: 140,
borderRadius: 70,
background: recording ? 'red' : '#0b84ff',
color: 'white',
fontSize: 18,
border: 'none',
}}
>
{recording ? 'Recording...' : 'Hold / Click'}
</button>
{/* Main Interface */}
<div className="flex flex-col lg:flex-row items-center justify-center gap-12 max-w-6xl mx-auto">
<div>
<p>Playback:</p>
<audio ref={audioRef} controls />
{playing && <div>Playing...</div>}
{/* Recording Section */}
<div className="flex flex-col items-center space-y-6">
<div className="relative">
{/* Animated rings around button when recording */}
{recording && (
<>
<div className="absolute inset-0 rounded-full border-4 border-red-400 animate-ping opacity-20"></div>
<div className="absolute inset-0 rounded-full border-4 border-red-400 animate-ping opacity-40 animation-delay-75"></div>
</>
)}
<button
onMouseDown={handleStartRecording}
onMouseUp={handleStopRecording}
onTouchStart={handleStartRecording}
onTouchEnd={handleStopRecording}
onClick={() => (recording ? handleStopRecording() : handleStartRecording())}
disabled={isProcessing}
className={`record-button w-40 h-40 rounded-full border-none text-white text-lg font-semibold
${recording
? 'bg-gradient-to-r from-red-500 to-red-600 recording-pulse shadow-red-500/50'
: isProcessing
? 'bg-gradient-to-r from-yellow-500 to-orange-500 shadow-yellow-500/50'
: 'bg-gradient-to-r from-blue-500 to-purple-600 shadow-blue-500/50'
}
${isProcessing ? 'cursor-not-allowed opacity-75' : 'cursor-pointer hover:shadow-xl'}
disabled:cursor-not-allowed disabled:opacity-75
`}
>
<div className="flex flex-col items-center">
<div className="text-3xl mb-2">
{recording ? '🎙️' : isProcessing ? '⏳' : '🎤'}
</div>
<div className="text-sm">
{recording ? 'Recording...' : isProcessing ? 'Processing...' : 'Hold / Click'}
</div>
</div>
</button>
</div>
{/* Status indicators */}
<div className="flex space-x-4">
<div className={`flex items-center space-x-2 px-4 py-2 rounded-full transition-all duration-300 ${
recording ? 'bg-red-500/20 text-red-300' : 'bg-gray-700/50 text-gray-400'
}`}>
<div className={`w-2 h-2 rounded-full ${recording ? 'bg-red-400 animate-pulse' : 'bg-gray-500'}`}></div>
<span className="text-sm font-medium">Recording</span>
</div>
<div className={`flex items-center space-x-2 px-4 py-2 rounded-full transition-all duration-300 ${
isProcessing ? 'bg-yellow-500/20 text-yellow-300' : 'bg-gray-700/50 text-gray-400'
}`}>
<div className={`w-2 h-2 rounded-full ${isProcessing ? 'bg-yellow-400 animate-pulse' : 'bg-gray-500'}`}></div>
<span className="text-sm font-medium">Processing</span>
</div>
</div>
</div>
{/* Audio Playback Section */}
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 shadow-2xl border border-white/20 min-w-[400px]">
<div className="text-center mb-6">
<h3 className="text-2xl font-semibold text-white mb-2">🔊 Audio Response</h3>
<p className="text-gray-300">Your AI assistant's response will play here</p>
</div>
<div className="audio-visualizer bg-gradient-to-r from-blue-500/10 to-purple-500/10 rounded-xl p-6">
<audio
ref={audioRef}
controls
className="w-full mb-4"
style={{
filter: 'drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1))',
}}
/>
{playing && (
<div className="flex items-center justify-center space-x-3 text-green-400">
<div className="flex space-x-1">
<div className="w-1 h-6 bg-green-400 rounded animate-bounce"></div>
<div className="w-1 h-8 bg-green-400 rounded animate-bounce" style={{animationDelay: '0.1s'}}></div>
<div className="w-1 h-6 bg-green-400 rounded animate-bounce" style={{animationDelay: '0.2s'}}></div>
<div className="w-1 h-10 bg-green-400 rounded animate-bounce" style={{animationDelay: '0.3s'}}></div>
<div className="w-1 h-6 bg-green-400 rounded animate-bounce" style={{animationDelay: '0.4s'}}></div>
</div>
<span className="font-medium">Playing response...</span>
</div>
)}
</div>
</div>
</div>
{/* Instructions */}
<div className="mt-16 text-center">
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-6 max-w-3xl mx-auto border border-white/10">
<h4 className="text-lg font-semibold text-white mb-4">💡 How to Use</h4>
<div className="grid md:grid-cols-3 gap-4 text-sm text-gray-300">
<div className="flex flex-col items-center space-y-2">
<div className="w-12 h-12 bg-blue-500/20 rounded-full flex items-center justify-center text-2xl">1</div>
<p><strong>Press & Hold</strong><br/>Hold the button down while speaking</p>
</div>
<div className="flex flex-col items-center space-y-2">
<div className="w-12 h-12 bg-purple-500/20 rounded-full flex items-center justify-center text-2xl">2</div>
<p><strong>Or Click Toggle</strong><br/>Click once to start, click again to stop</p>
</div>
<div className="flex flex-col items-center space-y-2">
<div className="w-12 h-12 bg-pink-500/20 rounded-full flex items-center justify-center text-2xl">3</div>
<p><strong>Listen</strong><br/>Your AI assistant will respond with audio</p>
</div>
</div>
</div>
</div>
</div>
</div>