// Stage 2: Upload voice note (mic + file) and Stage 3: Transcribe loading // Rev 2: captures real File/Blob and forwards it to the parent for /api/jobs. const UploadStage = ({ formKind, onBack, onTranscribe }) => { const [mode, setMode] = React.useState(null); // null | "record" | "file" const [recording, setRecording] = React.useState(false); const [paused, setPaused] = React.useState(false); const [elapsed, setElapsed] = React.useState(0); const [recordedBlob, setRecordedBlob] = React.useState(null); const [recordedMime, setRecordedMime] = React.useState(null); const [file, setFile] = React.useState(null); const [dragOver, setDragOver] = React.useState(false); const [recError, setRecError] = React.useState(null); const mediaRecorderRef = React.useRef(null); const chunksRef = React.useRef([]); const fileInputRef = React.useRef(null); // elapsed tick React.useEffect(() => { if (!recording || paused) return; const t = setInterval(() => setElapsed(e => e + 0.1), 100); return () => clearInterval(t); }, [recording, paused]); const startRecording = async () => { setRecError(null); setRecordedBlob(null); setElapsed(0); try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mime = MediaRecorder.isTypeSupported("audio/webm;codecs=opus") ? "audio/webm;codecs=opus" : (MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : ""); const rec = mime ? new MediaRecorder(stream, { mimeType: mime }) : new MediaRecorder(stream); chunksRef.current = []; rec.ondataavailable = (ev) => { if (ev.data && ev.data.size) chunksRef.current.push(ev.data); }; rec.onstop = () => { const blob = new Blob(chunksRef.current, { type: rec.mimeType || "audio/webm" }); setRecordedBlob(blob); setRecordedMime(rec.mimeType || "audio/webm"); stream.getTracks().forEach(t => t.stop()); }; mediaRecorderRef.current = rec; rec.start(); setRecording(true); setPaused(false); setMode("record"); } catch (e) { setRecError(e?.message || "microphone access denied"); } }; const pauseRecording = () => { const rec = mediaRecorderRef.current; if (rec && rec.state === "recording") { rec.pause(); setPaused(true); } }; const resumeRecording = () => { const rec = mediaRecorderRef.current; if (rec && rec.state === "paused") { rec.resume(); setPaused(false); } }; const stopRecording = () => { if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") { mediaRecorderRef.current.stop(); } setRecording(false); setPaused(false); }; const discardRecording = () => { stopRecording(); setRecordedBlob(null); setRecordedMime(null); setElapsed(0); setMode(null); }; const pickFile = (f) => { if (!f) return; const okExt = /\.(wav|mp3|webm|m4a)$/i.test(f.name); if (!okExt) { setRecError("Only .wav .mp3 .webm .m4a accepted"); return; } if (f.size > 50 * 1024 * 1024) { setRecError("File too large (max 50 MB)"); return; } setRecError(null); setFile(f); }; const onFilePicked = (e) => pickFile(e.target.files && e.target.files[0]); const onDrop = (e) => { e.preventDefault(); setDragOver(false); pickFile(e.dataTransfer.files && e.dataTransfer.files[0]); }; const handleTranscribe = () => { if (mode === "record" && recordedBlob) { const ext = (recordedMime && recordedMime.includes("webm")) ? ".webm" : ".webm"; const name = `recording-${Date.now()}${ext}`; onTranscribe({ blob: recordedBlob, filename: name, durationSec: elapsed, source: "recorded" }); return; } if (mode === "file" && file) { onTranscribe({ blob: file, filename: file.name, durationSec: null, source: "uploaded" }); return; } }; const fmtTime = (s) => { const m = Math.floor(s / 60), sec = Math.floor(s % 60); return `${String(m).padStart(2,"0")}:${String(sec).padStart(2,"0")}`; }; return (
Step 2 of 4 · Voice Note

Record or upload the client call

Dictate the applicant's details in English, Hindi, or Hinglish — we'll transcribe and extract.

{!mode && (
setMode("file")}/>
)} {recError && (
{recError}
)} {mode === "record" && (
{recording && !paused && (<>
)}
{fmtTime(elapsed)}
{recording && !paused ? "Recording — speak naturally" : (recording && paused ? "Paused — tap resume to continue" : (recordedBlob ? "Recorded — ready to transcribe" : "Ready"))}
{recording ? ( <> {paused ? : } ) : ( )}
)} {mode === "file" && (
{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={onDrop} onClick={() => fileInputRef.current && fileInputRef.current.click()} style={{ border: `1.5px dashed ${dragOver ? V.orange : V.borderMed}`, borderRadius: V.radius, padding: "44px 24px", textAlign: "center", cursor: "pointer", background: dragOver ? V.orangeSubtle : V.offWhite, transition: "all 0.2s", }}> {!file ? ( <>
Drop audio file here, or click to browse
MP3, WAV, WebM, M4A · up to 50 MB
) : (
{file.name}
{(file.size / (1024 * 1024)).toFixed(2)} MB
)}
)}
); }; const OptionTile = ({ icon, title, subtitle, onClick }) => { const [h, setH] = React.useState(false); return (
setH(true)} onMouseLeave={() => setH(false)} onClick={onClick} style={{ position: "relative", background: V.white, border: `1px solid ${h ? V.orange : V.borderMed}`, borderRadius: V.radius, padding: 24, cursor: "pointer", transition: "all 0.2s", transform: h ? "translateY(-2px)" : "none", boxShadow: h ? V.shadowMd : "none", }}>
{title}
{subtitle}
); }; const pulseRing = (delay) => ({ position: "absolute", inset: 0, borderRadius: "50%", border: `2px solid ${V.orange}`, opacity: 0, animation: `v2f-pulse 1.8s ease-out ${delay}s infinite`, }); const Waveform = ({ active, bars = 48 }) => { const [tick, setTick] = React.useState(0); React.useEffect(() => { if (!active) return; const t = setInterval(() => setTick(v => v + 1), 80); return () => clearInterval(t); }, [active]); const heights = React.useMemo(() => Array.from({ length: bars }, (_, i) => { if (!active) return 4; const seed = (i * 7 + tick * 3) % 17; const phase = Math.sin((i + tick) * 0.35) * 0.5 + 0.5; return 6 + Math.floor(phase * 30) + (seed % 6); }), [tick, active, bars]); return (
{heights.map((h, i) => (
))}
); }; const ProcessingStage = ({ phase, formKind }) => { const steps = [ { id: "transcribe", label: "Transcribing audio", sub: "AWS Transcribe · en-IN / hi-IN" }, { id: "extract", label: "Extracting fields", sub: "Claude Sonnet 4.5 on Bedrock" }, ]; return (

{phase === "transcribe" ? "Transcribing your voice note…" : "Extracting fields…"}

{phase === "transcribe" ? "Separating speech from silence and converting audio to text." : "Reading the transcript and mapping it to the " + (formKind === "sep" ? "SEP" : "Salaried") + " loan schema."}

{steps.map((s, i) => { const done = (phase === "extract" && s.id === "transcribe"); const active = phase === s.id; return (
{done ? : active ?
:
}
{s.label}
{s.sub}
{active &&
Running…
} {done &&
Done
}
); })}
); }; Object.assign(window, { UploadStage, ProcessingStage });