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

) }