import { useState, useEffect, useRef } from "react"
import api from "../lib/api"
import Reveal from "./Reveal"
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000"
const G = "linear-gradient(135deg, #1d4ed8 0%, #0891b2 100%)"
const PREPARE_STEPS = [
{ key: "transcribing", label: "Transcribing audio" },
{ key: "diarizing", label: "Identifying speakers" },
{ key: "detecting", label: "Detecting your voice" },
]
const FINALIZE_STEPS = [
{ key: "extracting", label: "Extracting behavioral signals" },
{ key: "scoring", label: "Scoring dimensions" },
{ key: "generating", label: "Generating AI insights" },
]
function StepProgress({ steps, currentStep, title, onCancel }) {
const currentIdx = steps.findIndex(s => s.key === currentStep)
return (
{title}
This usually takes 1–3 minutes
{steps.map((step, idx) => {
const isDone = currentIdx > idx
const isCurrent = currentIdx === idx
return (
{isDone &&
✓}
{isCurrent &&
}
{step.label}
{isCurrent && (
— in progress…
)}
)
})}
You can browse History while this runs
{onCancel && (
)}
)
}
export default function UploadView({ onResults, onActivate }) {
const [file, setFile] = useState(null)
const [step, setStep] = useState("idle")
const [error, setError] = useState("")
const [progressStep, setProgressStep] = useState(null)
const [serverWarm, setServerWarm] = useState(true)
const [usage, setUsage] = useState(null)
useEffect(() => {
api.get("/api/usage").then(r => setUsage(r.data)).catch(() => {})
}, [])
const esRef = useRef(null)
const pollRef = useRef(null)
const finalizeStartedRef = useRef(false)
const handleFinalize = async (sessionId, confirmedSpeaker) => {
setStep("finalizing")
setError("")
setProgressStep(null)
onActivate?.()
try {
const form = new FormData()
form.append("session_id", sessionId)
form.append("confirmed_speaker", confirmedSpeaker)
const { data: { job_id } } = await api.post(
"/api/finalize/start", form,
{ headers: { "Content-Type": "multipart/form-data" } }
)
const es = new EventSource(`${API_URL}/api/finalize/${job_id}/stream`)
esRef.current = es
es.onmessage = (e) => {
const msg = JSON.parse(e.data)
if (msg.event === "progress") {
setProgressStep(msg.step)
} else if (msg.event === "done") {
es.close(); esRef.current = null
sessionStorage.removeItem("pending_finalize_session")
setStep("idle"); setFile(null)
api.get("/api/usage").then(r => setUsage(r.data)).catch(() => {})
onResults(msg.data)
} else if (msg.event === "error") {
es.close(); esRef.current = null
sessionStorage.removeItem("pending_finalize_session")
setError(msg.message || "Analysis failed."); setStep("idle")
}
}
es.onerror = async () => {
es.close(); esRef.current = null
try {
await new Promise(r => setTimeout(r, 1200))
const res = await api.get("/api/sessions")
const saved = res.data.find(s => s.session_id === sessionId)
if (saved) {
sessionStorage.removeItem("pending_finalize_session")
setStep("idle"); setFile(null)
onResults({
signals: saved.signals, insights: saved.insights,
dimensions: saved.dimensions || {}, filename: saved.filename,
detected_speaker: saved.detected_speaker,
speaker_confirmed: saved.speaker_confirmed,
session_id: saved.session_id,
available_speakers: saved.available_speakers || [],
})
return
}
} catch {}
sessionStorage.removeItem("pending_finalize_session")
setError("Connection lost during analysis. Please try again.")
setStep("idle")
}
} catch (e) {
sessionStorage.removeItem("pending_finalize_session")
setError(e.response?.data?.detail || "Analysis failed.")
setStep("idle")
}
}
const cancelRecovery = () => {
clearInterval(pollRef.current); pollRef.current = null
esRef.current?.close(); esRef.current = null
sessionStorage.removeItem("pending_prepare_job")
sessionStorage.removeItem("pending_finalize_session")
setStep("idle")
}
useEffect(() => {
const savedFinalize = sessionStorage.getItem("pending_finalize_session")
if (savedFinalize) {
const { session_id, detected_speaker } = JSON.parse(savedFinalize)
setStep("finalizing"); onActivate?.()
handleFinalize(session_id, detected_speaker)
return
}
const saved = sessionStorage.getItem("pending_prepare_job")
if (!saved) return
const { job_id } = JSON.parse(saved)
setStep("preparing"); onActivate?.()
api.get(`/api/prepare/${job_id}/status`)
.then(res => {
sessionStorage.removeItem("pending_prepare_job")
handleFinalize(res.data.session_id, res.data.detected_speaker || "SPEAKER_00")
})
.catch(() => {
clearInterval(pollRef.current)
pollRef.current = setInterval(async () => {
try {
const res = await api.get(`/api/prepare/${job_id}/status`)
clearInterval(pollRef.current); pollRef.current = null
sessionStorage.removeItem("pending_prepare_job")
handleFinalize(res.data.session_id, res.data.detected_speaker || "SPEAKER_00")
} catch (e) {
if (e.response?.status && e.response.status !== 404) cancelRecovery()
}
}, 10000)
})
}, [])
useEffect(() => {
return () => { esRef.current?.close(); clearInterval(pollRef.current) }
}, [])
// Cold-start warmup check — HF Spaces sleeps after inactivity
useEffect(() => {
let cancelled = false
const timer = setTimeout(() => {
if (!cancelled) setServerWarm(false)
}, 3500)
api.get("/health").then(() => {
clearTimeout(timer)
if (!cancelled) setServerWarm(true)
}).catch(() => {
clearTimeout(timer)
if (!cancelled) setServerWarm(false)
})
return () => { cancelled = true; clearTimeout(timer) }
}, [])
const handlePrepare = async () => {
if (!file) return
setStep("preparing"); setError(""); setProgressStep(null)
finalizeStartedRef.current = false
try {
const form = new FormData()
form.append("audio", file)
form.append("filename", file.name)
const { data: { job_id } } = await api.post(
"/api/prepare/start", form,
{ headers: { "Content-Type": "multipart/form-data" } }
)
sessionStorage.setItem("pending_prepare_job", JSON.stringify({ job_id }))
const handlePrepareDone = (data) => {
if (finalizeStartedRef.current) return
finalizeStartedRef.current = true
clearInterval(pollRef.current); pollRef.current = null
sessionStorage.removeItem("pending_prepare_job")
sessionStorage.setItem("pending_finalize_session", JSON.stringify({
session_id: data.session_id,
detected_speaker: data.detected_speaker || "SPEAKER_00",
}))
handleFinalize(data.session_id, data.detected_speaker || "SPEAKER_00")
}
const startPolling = () => {
clearInterval(pollRef.current)
pollRef.current = setInterval(async () => {
try {
const res = await api.get(`/api/prepare/${job_id}/status`)
if (res.data.current_step) setProgressStep(res.data.current_step)
handlePrepareDone(res.data)
} catch (e) {
if (e.response?.status && e.response.status !== 404) {
clearInterval(pollRef.current); pollRef.current = null
setError("Something went wrong. Please try again."); setStep("idle")
}
}
}, 10000)
}
const es = new EventSource(`${API_URL}/api/prepare/${job_id}/stream`)
esRef.current = es
es.onmessage = (e) => {
const msg = JSON.parse(e.data)
if (msg.event === "progress") setProgressStep(msg.step)
else if (msg.event === "done") { es.close(); esRef.current = null; handlePrepareDone(msg.data) }
else if (msg.event === "error") {
es.close(); esRef.current = null
sessionStorage.removeItem("pending_prepare_job")
setError(msg.message || "Preparation failed."); setStep("idle")
}
}
es.onerror = async () => {
es.close(); esRef.current = null
await new Promise(r => setTimeout(r, 1500))
try {
const res = await api.get(`/api/prepare/${job_id}/status`)
if (res.data.current_step) setProgressStep(res.data.current_step)
handlePrepareDone(res.data); return
} catch {}
startPolling()
}
} catch (e) {
setError(e.response?.data?.detail || "Preparation failed."); setStep("idle")
}
}
if (step === "preparing") return (
)
if (step === "finalizing") return (
)
const hasFile = !!file
return (
{/* Drop zone */}
document.getElementById("file-input").click()}
style={{
border: `2px dashed ${hasFile ? "#1d4ed8" : "#1e2438"}`,
borderRadius: 16, padding: "52px 32px",
textAlign: "center", marginBottom: 20, cursor: "pointer",
background: hasFile ? "rgba(29,78,216,0.04)" : "#151922",
transition: "all 0.2s",
boxShadow: hasFile ? "inset 0 0 40px rgba(29,78,216,0.04)" : "none",
}}>
setFile(e.target.files[0])} />
{hasFile ? (
<>
🎵
{file.name}
{(file.size / 1024 / 1024).toFixed(1)} MB · Click to change
>
) : (
<>
🎙️
Drop audio or video here
.mp3 · .wav · .m4a · .mp4 · up to 100 MB
>
)}
{!serverWarm && (
⏳
Server is waking up — first analysis may take a little longer than usual.
)}
{usage && (
Sessions this month
{usage.used} / {usage.limit}
{usage.remaining === 0 && (
Limit reached — resets {usage.resets_on}
)}
)}
{error && (
⚠️ {error}
)}
🔒 Audio is deleted immediately after analysis. Only behavioural insights are stored.{" "}
Privacy Policy
)
}