// 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%
);
};
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 (
);
};
const ITRBlock = ({ itr, onChange, revealed }) => (
);
// 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.
Loan amount
₹ {formatINR(amount)}
);
};
Object.assign(window, { TranscriptCard, FormStage, SuccessStage });