chirp / frontend /src /App.jsx
mg643's picture
backend changes
2cc62ca
import React, { useState, useCallback } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import Header from './components/Header'
import AudioUploader from './components/AudioUploader'
import Spectrogram from './components/Spectrogram'
import BirdResult from './components/BirdResult'
import RangeMap from './components/RangeMap'
import { enrichResult } from './data/mockData'
import { useSpectrogram } from './hooks/useSpectrogram'
import styles from './App.module.css'
const API_URL = 'https://mg643-chirp.hf.space'
export default function App() {
const [audioSelection, setAudioSelection] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [result, setResult] = useState(null)
const [error, setError] = useState(null)
const { spectrogramUrl, generateSpectrogram, isGenerating } = useSpectrogram()
const handleAudioSelected = useCallback(async (selection) => {
setAudioSelection(selection)
setResult(null)
setError(null)
if (selection.file) {
generateSpectrogram(selection.file)
} else if (selection.sampleId) {
try {
const res = await fetch(`/samples/${selection.sampleId}.ogg`)
const blob = await res.blob()
generateSpectrogram(new File([blob], `${selection.sampleId}.ogg`, { type: 'audio/ogg' }))
} catch (err) {
console.warn('Spectrogram generation failed for sample:', err)
}
}
}, [generateSpectrogram])
const handleIdentify = useCallback(async () => {
if (!audioSelection) return
setIsLoading(true)
setError(null)
try {
let audioFile = audioSelection.file
if (!audioFile && audioSelection.sampleId) {
const res = await fetch(`/samples/${audioSelection.sampleId}.ogg`)
const blob = await res.blob()
audioFile = new File([blob], `${audioSelection.sampleId}.ogg`, { type: 'audio/ogg' })
}
const formData = new FormData()
formData.append('file', audioFile)
const res = await fetch(`${API_URL}/predict?top_k=3`, {
method: 'POST',
body: formData,
})
if (!res.ok) throw new Error(`API ${res.status}: ${res.statusText}`)
const data = await res.json()
setResult(enrichResult(data))
} catch (err) {
console.error('Identify failed:', err)
setError('Could not reach the identification service. Please try again.')
} finally {
setIsLoading(false)
}
}, [audioSelection])
const hasAudio = audioSelection && (audioSelection.file || audioSelection.sampleId)
return (
<div className={styles.page}>
<div className={styles.blob1} />
<div className={styles.blob2} />
<div className={styles.container}>
<Header />
<main className={styles.main}>
{/* ── Left panel ── */}
<motion.div
className={styles.leftPanel}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.1, ease: [0.22, 1, 0.36, 1] }}
>
<div className={styles.panelCard}>
<p className={styles.panelTitle}>Listen & Identify</p>
<p className={styles.panelSub}>
Upload a bird call recording or try our sample.
Chirp identifies the species in seconds.
</p>
<div className={styles.divider} />
<AudioUploader
onAudioSelected={handleAudioSelected}
isLoading={isLoading}
/>
{hasAudio && !isLoading && !result && (
<motion.button
className={styles.identifyBtn}
onClick={handleIdentify}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
Identify Bird
</motion.button>
)}
{isLoading && (
<div className={styles.loadingState}>
<div className={styles.spinner} />
<p>Analysing audio…</p>
</div>
)}
{error && (
<p className={styles.errorMsg}>{error}</p>
)}
</div>
{/* Spectrogram */}
<AnimatePresence>
{(spectrogramUrl || isGenerating) && (
<motion.div
className={styles.panelCard}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
>
<Spectrogram url={spectrogramUrl} isGenerating={isGenerating} />
</motion.div>
)}
</AnimatePresence>
</motion.div>
{/* ── Right panel ── */}
<AnimatePresence mode="wait">
{!result ? (
<motion.div
key="placeholder"
className={`${styles.rightPanel} ${styles.placeholder}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<div className={styles.placeholderInner}>
<div className={styles.birdIllustration}>
<svg viewBox="0 0 120 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="52" cy="42" rx="22" ry="14" fill="var(--mauve-200)" />
<circle cx="74" cy="34" r="10" fill="var(--mauve-200)" />
<ellipse cx="30" cy="46" rx="18" ry="8" fill="var(--mauve-100)" transform="rotate(-15 30 46)" />
<ellipse cx="68" cy="56" rx="14" ry="5" fill="var(--mauve-100)" transform="rotate(10 68 56)" />
<circle cx="78" cy="31" r="2.5" fill="var(--ink)" />
<circle cx="79" cy="30" r="0.8" fill="white" />
<path d="M83 34 L92 36 L83 37 Z" fill="var(--gold)" />
<path d="M10 65 Q60 60 110 68" stroke="var(--beige-300)" strokeWidth="2.5" strokeLinecap="round" fill="none" />
</svg>
</div>
<p className={styles.placeholderText}>
Upload or select an audio clip<br />to identify the species
</p>
</div>
</motion.div>
) : (
<motion.div
key="results"
className={styles.rightPanel}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
>
<BirdResult result={result} />
<RangeMap
range={result.range}
commonName={result.commonName}
color={result.color}
/>
<button
className={styles.resetBtn}
onClick={() => { setResult(null); setAudioSelection(null) }}
>
← Identify another bird
</button>
</motion.div>
)}
</AnimatePresence>
</main>
<footer className={styles.footer}>
<p>chirp! · built with BirdCLEF 2023 · EfficientNet-B0</p>
</footer>
</div>
</div>
)
}