const { useState, useEffect, useRef } = React; const LOGO = "https://assets.omniskills.ai/white-bg-96.svg"; const api = (path, opts) => fetch(path, { headers: { "Content-Type": "application/json" }, ...opts }).then((r) => r.json()); // Brand palette const C = { tii: "#2563eb", green: "#10b981", digital: "#8b5cf6", purple: "#7e22ce", slate: "#94a3b8", amber: "#f59e0b", red: "#ef4444" }; const LEVELS = { none: 0, basic: 2, intermediate: 4, advanced: 6 }; const lvl = (s) => LEVELS[String(s || "").trim().toLowerCase()] ?? (parseInt(s, 10) || 0); // ── Field validators (return "" when valid, else an error string) ── // Singapore UEN (Unique Entity Number), 3 ACRA formats: // • Businesses (ROB): 8 digits + 1 letter e.g. 53333444A // • Local companies (ROC): 4-digit year + 5 digits + 1 letter e.g. 201912345K // • Other entities: [TSR] + yy + 2 letters + 4 digits + 1 letter e.g. T08GB0001A function validateUEN(raw) { const s = String(raw || "").trim().toUpperCase(); if (!s) return "UEN Number is required."; const business = /^\d{8}[A-Z]$/; const localCo = /^(19|20)\d{7}[A-Z]$/; const other = /^[TSR]\d{2}[A-Z]{2}\d{4}[A-Z]$/; if (business.test(s) || localCo.test(s) || other.test(s)) return ""; return "Enter a valid Singapore UEN (e.g. 201912345K, 53333444A or T08GB0001A)."; } const FIELD_VALIDATORS = { uen: validateUEN }; const fieldError = (f, val) => { if (!f || !f.validate || !FIELD_VALIDATORS[f.validate]) return ""; const has = Array.isArray(val) ? val.length : (val != null && String(val).trim() !== ""); if (!has && !f.required) return ""; return FIELD_VALIDATORS[f.validate](val || ""); }; /* ── Audio helpers for realtime voice (PCM16 @ 24kHz) ── */ function downsampleTo24k(buffer, srcRate) { const dst = 24000; if (srcRate === dst) return buffer; const ratio = srcRate / dst; const outLen = Math.floor(buffer.length / ratio); const out = new Float32Array(outLen); for (let i = 0; i < outLen; i++) { const start = Math.floor(i * ratio), end = Math.min(Math.floor((i + 1) * ratio), buffer.length); let sum = 0, n = 0; for (let j = start; j < end; j++) { sum += buffer[j]; n++; } out[i] = n ? sum / n : buffer[Math.min(start, buffer.length - 1)] || 0; } return out; } function pcm16ToBase64(float32) { const buf = new ArrayBuffer(float32.length * 2); const view = new DataView(buf); for (let i = 0; i < float32.length; i++) { let s = Math.max(-1, Math.min(1, float32[i])); view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7fff, true); } const bytes = new Uint8Array(buf); let bin = ""; for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000)); return btoa(bin); } function base64ToF32(b64) { const bin = atob(b64), len = bin.length, bytes = new Uint8Array(len); for (let i = 0; i < len; i++) bytes[i] = bin.charCodeAt(i); const view = new DataView(bytes.buffer), n = Math.floor(len / 2), out = new Float32Array(n); for (let i = 0; i < n; i++) out[i] = view.getInt16(i * 2, true) / 0x8000; return out; } /* ────────────────────────────────────────────────────────────── Top bar */ function TopBar() { return (
OmniSkills Agentic AI · SBF SDP
); } /* ─────────────────────────────────────────────────────── Confirm modal */ function ConfirmModal({ open, title, message, confirmLabel = "Confirm", danger, onConfirm, onCancel }) { useEffect(() => { if (!open) return; const h = (e) => { if (e.key === "Escape") onCancel && onCancel(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [open]); if (!open) return null; return (
e.stopPropagation()} role="dialog" aria-modal="true">

{title}

{message &&

{message}

}
); } /* ────────────────────────────────────────────────────────── Journey pick */ function JourneyPicker({ onPick, onOpenSaved }) { const [recent, setRecent] = useState([]); const [email, setEmail] = useState(""); const [err, setErr] = useState(""); const [confirmId, setConfirmId] = useState(null); useEffect(() => { api("/api/sessions").then((d) => setRecent(d.sessions || [])).catch(() => {}); }, []); const findByEmail = () => { if (!email.trim()) return; setErr(""); fetch("/api/session-by-email?email=" + encodeURIComponent(email.trim())) .then((r) => (r.ok ? r.json() : null)) .then((s) => { if (s && s.id) onOpenSaved(s.id); else setErr("No saved report found for that email."); }) .catch(() => setErr("Could not look that up.")); }; const del = (id, e) => { e.stopPropagation(); setConfirmId(id); }; const doDelete = () => { const id = confirmId; setConfirmId(null); fetch("/api/session/" + encodeURIComponent(id), { method: "DELETE" }) .then(() => setRecent((r) => r.filter((x) => x.id !== id))).catch(() => {}); }; return (
SBF Skills Development Partner · TII & Green Skills

Agentic AI Digital Advisor

Your Workforce Transformation Guide. Pick a journey — the whole consultation, from diagnosis to a funded action plan, happens right here.

{["Jobs-Skills Insights", "Job Redesign", "Learning Pathways", "SDP Report", "Survey Follow-Up"].map((c) => ( {c} ))}
setEmail(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") findByEmail(); }} />
{err &&
{err}
} {recent.length > 0 && (
Recent reports
{recent.slice(0, 6).map((s) => (
))}
)}
setConfirmId(null)} />
); } /* ─────────────────────────────────────────────────────── Saved artifacts */ function SavedNote({ id }) { const [copied, setCopied] = useState(false); const link = `${location.origin}/?session=${id}`; const copy = () => { navigator.clipboard && navigator.clipboard.writeText(link).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1800); }); }; return (
✓ Saved. Reopen this report anytime: {link}
); } function SavedReportView({ session, onExit, onDelete }) { const [showChat, setShowChat] = useState(false); const [confirmDel, setConfirmDel] = useState(false); const [rep, setRep] = useState(session.report); const [regen, setRegen] = useState(false); const regenerate = async () => { setRegen(true); try { const r = await api("/api/report", { method: "POST", body: JSON.stringify({ journey: session.journey, collected: session.collected || {}, transcript: session.transcript || [], provider: "azure" }) }); setRep(r); try { await api("/api/session", { method: "POST", body: JSON.stringify({ journey: session.journey, title: r.title, email: (session.collected && session.collected.email) || session.email || "", collected: session.collected, transcript: session.transcript, report: r }) }); } catch (e) { /* non-fatal */ } } catch (e) { /* keep the existing report on failure */ } setRegen(false); }; return (
{ setConfirmDel(false); onDelete(session.id); }} onCancel={() => setConfirmDel(false)} /> {showChat && (
{(session.transcript || []).map((m, i) => )}
)}
); } /* ─────────────────────────────────────────────────────────── Field input */ function FileField({ field, value, onChange }) { const [status, setStatus] = useState(value ? { name: value } : null); const [busy, setBusy] = useState(false); const inputRef = useRef(null); const pick = async (e) => { const file = e.target.files[0]; if (!file) return; setBusy(true); setStatus({ name: file.name }); const fd = new FormData(); fd.append("file", file); try { const r = await fetch("/api/upload", { method: "POST", body: fd }).then((x) => x.json()); setStatus({ name: r.filename, chars: r.chars, ok: r.ok }); onChange({ filename: r.filename, text: r.text || "" }); } catch (_) { setStatus({ name: file.name, error: true }); } setBusy(false); }; return (
inputRef.current?.click()}> {!status && 📎 Click to upload — parsed on device, used only with your consent} {status && ( {status.name} {busy ? · parsing… : status.ok ? · parsed {status.chars} chars ✓ : status.error ? · not parsed : · attached} )}
); } // Company-name input with ACRA type-ahead. Selecting a suggestion fills the name and // (via onPick) the UEN too. function CompanyAutocomplete({ field, value, onChange, onPick }) { const [open, setOpen] = useState(false); const [items, setItems] = useState([]); const [hi, setHi] = useState(-1); const boxRef = useRef(null), tRef = useRef(null); useEffect(() => { const onDoc = (e) => { if (boxRef.current && !boxRef.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, []); const runQuery = (q) => { clearTimeout(tRef.current); if (!q || q.trim().length < 2) { setItems([]); setOpen(false); return; } tRef.current = setTimeout(() => { api(`/api/uen-suggest?q=${encodeURIComponent(q.trim())}`).then((res) => { const list = (res && res.suggestions) || []; setItems(list); setOpen(list.length > 0); setHi(-1); }).catch(() => { setItems([]); setOpen(false); }); }, 220); }; const pick = (it) => { onChange(it.name); if (onPick) onPick(it.name, it.uen); setOpen(false); setItems([]); }; return (
{ onChange(e.target.value); runQuery(e.target.value); }} onFocus={() => { if (items.length) setOpen(true); }} onKeyDown={(e) => { if (!open) return; if (e.key === "ArrowDown") { e.preventDefault(); setHi((h) => Math.min(h + 1, items.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setHi((h) => Math.max(h - 1, 0)); } else if (e.key === "Enter") { if (hi >= 0 && items[hi]) { e.preventDefault(); pick(items[hi]); } } else if (e.key === "Escape") { setOpen(false); } }} /> {open && (
{items.map((it, i) => ( ))}
)}
); } function Field({ field, value, onChange, onPick }) { const cls = "field" + (field.full ? " full" : ""); if (field.autocomplete === "company") return ; if (field.type === "file") return ; if (field.type === "consent") { return ( ); } if (field.type === "select") { return (
); } if (field.type === "textarea") { return (