// Stage 4: Transcript review + Stage 5: Form filling // Highlight tokens in transcript that map to extracted fields const TranscriptCard = ({ transcript, durationSec, highlights = [], onContinue, onReRecord }) => { // highlights: [{match: string, kind: "id"|"money"|"name"|"contact"}] let body = transcript; // Build regex-escaped pattern const highlightEl = (text) => { if (!highlights.length) return text; const all = [...text]; const parts = []; let i = 0; const find = (s, from) => { let best = null; highlights.forEach(h => { const idx = s.toLowerCase().indexOf(h.match.toLowerCase(), from); if (idx !== -1 && (best === null || idx < best.idx)) best = { idx, len: h.match.length, kind: h.kind }; }); return best; }; while (i < text.length) { const f = find(text, i); if (!f) { parts.push(text.slice(i)); break; } if (f.idx > i) parts.push(text.slice(i, f.idx)); parts.push({ snippet: text.substr(f.idx, f.len), kind: f.kind }); i = f.idx + f.len; } const kindStyle = (k) => { const map = { id: { bg: V.purpleSubtle, fg: V.purple }, money: { bg: V.orangeSubtle, fg: V.orange }, name: { bg: "rgba(22,163,74,0.08)", fg: V.success }, contact: { bg: "rgba(36,20,13,0.06)", fg: V.brown }, }; return map[k] || map.contact; }; return parts.map((p, idx) => typeof p === "string" ? p : {p.snippet} ); }; const fmt = (s) => `${String(Math.floor(s/60)).padStart(2,"0")}:${String(Math.floor(s%60)).padStart(2,"0")}`; return (
Step 3 of 4 · Transcript

Verify the transcript

Highlighted tokens are candidates for extraction. Review for errors — once you confirm, we'll extract into the form.

client-call-2026-04-22.m4a
{fmt(durationSec)} · English + Hindi · confidence 94%
{highlightEl(body)}
); }; const LegendDot = ({ color, label }) => ( {label} ); // ── FORM STAGE ───────────────────────────────────────────────────────── const FormStage = ({ formKind, fields, extracted, itr, transcript, durationSec, onSubmit, onBack }) => { const sections = formKind === "sep" ? SECTIONS_SEP : SECTIONS_SALARIED; const [values, setValues] = React.useState(() => { const v = {}; fields.forEach(f => { v[f.key] = extracted[f.key]?.value ?? null; }); return v; }); const [itrState, setItrState] = React.useState(itr || null); const [revealIdx, setRevealIdx] = React.useState(0); // animation counter const [focusedKey, setFocusedKey] = React.useState(null); // Staggered reveal animation React.useEffect(() => { if (revealIdx >= fields.length) return; const t = setTimeout(() => setRevealIdx(r => r + 1), 35); return () => clearTimeout(t); }, [revealIdx, fields.length]); const update = (k, v) => setValues(prev => ({ ...prev, [k]: v })); const getFieldStatus = (f) => { const ex = extracted[f.key]; const v = values[f.key]; if (v === null || v === undefined || v === "") { return f.required ? "missing" : "empty"; } if (ex?.status === "spelled") return "spelled"; if (ex?.c !== null && ex?.c !== undefined && ex.c < 0.75) return "low"; return "filled"; }; // Missing required fields summary const isMarried = values.marital_status === "Married"; const missing = fields.filter(f => { if (!f.required && f.key !== "spouse_name") return false; if (f.key === "spouse_name" && !isMarried) return false; const v = values[f.key]; return v === null || v === undefined || v === ""; }); const lowConfCount = fields.filter(f => { const ex = extracted[f.key]; return ex?.value !== null && ex?.c !== null && ex?.c !== undefined && ex.c < 0.75; }).length; const totalRequired = fields.filter(f => f.required).length + (isMarried ? 1 : 0); const filledRequired = totalRequired - missing.length; const filledBySection = {}; sections.forEach(s => { const secFields = fields.filter(f => f.section === s.id); const secReq = secFields.filter(f => f.required || (f.key === "spouse_name" && isMarried)); const secFilled = secReq.filter(f => values[f.key] !== null && values[f.key] !== undefined && values[f.key] !== ""); filledBySection[s.id] = { filled: secFilled.length, total: secReq.length, totalAll: secFields.length }; }); const avgConfidence = (() => { const scores = fields.map(f => extracted[f.key]?.c).filter(c => c !== null && c !== undefined); return scores.reduce((a, b) => a + b, 0) / scores.length; })(); return (
{/* Extraction summary banner */}
Extraction complete
Pre-filled {fields.filter(f => values[f.key] !== null && values[f.key] !== "").length} of {fields.length} fields from {Math.floor(durationSec/60)}:{String(Math.floor(durationSec%60)).padStart(2,"0")} voice note
0 ? "warning" : "neutral"}/>
{/* Missing required warning */} {missing.length > 0 && (
{missing.length} required {missing.length === 1 ? "field is" : "fields are"} missing
)} {/* Two column: sidebar section nav + sections */}
{sections.map(sec => { const secFields = fields.filter(f => f.section === sec.id); const stat = filledBySection[sec.id]; return (
{secFields.map((f, fi) => { // Hide spouse if not married if (f.conditional === "married" && !isMarried) return null; const ex = extracted[f.key]; const status = getFieldStatus(f); const globalIdx = fields.indexOf(f); const shown = globalIdx < revealIdx; const col = f.fullWidth ? "1 / -1" : "auto"; const isFocused = focusedKey === f.key; const val = values[f.key]; const displayVal = (f.type === "number" && val !== null && val !== "" && f.monospace) ? val : val; return (
update(f.key, v)} placeholder={f.placeholder} options={f.options} icon={f.icon} monospace={f.monospace} type={f.type} readOnly={f.readOnly} required={f.required || (f.key === "spouse_name" && isMarried)} confidence={ex?.c} status={status} note={ex?.note || (status === "missing" ? "Required — not captured in voice note" : null)} />
); })} {/* ITR special block for SEP financials */} {formKind === "sep" && sec.id === "financials" && itrState && (
= fields.length}/>
)}
); })} {/* Footer actions */}
{missing.length === 0 ? ( All required fields filled ) : ( {missing.length} required field{missing.length === 1 ? "" : "s"} remaining )}
); }; const Stat = ({ label, value, kind = "neutral" }) => { const colors = { success: V.success, warning: V.warning, info: V.purple, neutral: V.brown, }; return (
{label}
{value}
); }; const ITRBlock = ({ itr, onChange, revealed }) => (
ITR Filing History
onChange({ ...itr, years_filed: Number(v) })} monospace/> onChange({ ...itr, last_year_income: Number(v) })} monospace icon="coins"/> onChange({ ...itr, prior_year_income: Number(v) })} monospace icon="coins"/>
); // Stage 6: Success const SuccessStage = ({ values, formKind, onNew }) => { const applicant = values.full_name || "Applicant"; const amount = values.loan_amount; const appId = "VEC-" + (formKind === "sep" ? "S" : "L") + "-2026-" + String(Math.floor(Math.random() * 900000) + 100000); return (

Application submitted

{applicant}'s {formKind === "sep" ? "business loan" : "personal loan"} request has been queued for CIBIL pull and underwriting.

Application ID
{appId}
Loan amount
₹ {formatINR(amount)}
Est. decision
24–48 hrs
); }; Object.assign(window, { TranscriptCard, FormStage, SuccessStage });