// Login screen + profile-completion onboarding (multi-step with API calls) // Constellation canvas — feed-forward neural net with firing pulses. // Input neurons fire periodically; signals travel along weighted edges as glowing // pulses, activating downstream neurons which forward to the next layer. const ConstellationCanvas = () => { const ref = React.useRef(null); React.useEffect(() => { const canvas = ref.current; if (!canvas) return; const ctx = canvas.getContext("2d"); let raf; let nodes = []; let edges = []; let outEdges = new Map(); let lastFire = 0; const LAYER_COUNT = 4; const build = () => { const W = canvas.width = canvas.offsetWidth; const H = canvas.height = canvas.offsetHeight; const xs = [W * 0.18, W * 0.40, W * 0.62, W * 0.84]; const counts = [8, 11, 11, 6]; nodes = []; counts.forEach((c, li) => { const padY = 80; const usable = H - padY * 2; for (let i = 0; i < c; i++) { const y = padY + (usable * (i + 0.5)) / c; nodes.push({ x: xs[li], y, baseX: xs[li], baseY: y, layer: li, activation: 0, phase: Math.random() * Math.PI * 2, }); } }); edges = []; outEdges = new Map(); for (let li = 0; li < LAYER_COUNT - 1; li++) { const a = nodes.filter(n => n.layer === li); const b = nodes.filter(n => n.layer === li + 1); for (const na of a) for (const nb of b) { if (Math.random() < 0.72) { const e = { a: na, b: nb, w: 0.2 + Math.random() * 0.8, fires: [] }; edges.push(e); if (!outEdges.has(na)) outEdges.set(na, []); outEdges.get(na).push(e); } } } }; build(); const onResize = () => build(); window.addEventListener("resize", onResize); const fireInput = (now) => { const inputs = nodes.filter(n => n.layer === 0); const n = inputs[Math.floor(Math.random() * inputs.length)]; n.activation = 1; const outs = outEdges.get(n) || []; const k = Math.max(2, Math.floor(outs.length * 0.55)); const picks = outs.slice().sort(() => Math.random() - 0.5).slice(0, k); for (const e of picks) e.fires.push({ start: now, dur: 600 + Math.random() * 300 }); }; const activateAndForward = (n, now) => { n.activation = Math.min(1, n.activation + 0.9); if (n.layer >= LAYER_COUNT - 1) return; const outs = outEdges.get(n) || []; const k = Math.max(1, Math.floor(outs.length * (0.45 - n.layer * 0.05))); const picks = outs.slice().sort(() => Math.random() - 0.5).slice(0, k); for (const e of picks) e.fires.push({ start: now, dur: 550 + Math.random() * 300 }); }; const tick = (t) => { const W = canvas.width, H = canvas.height; ctx.clearRect(0, 0, W, H); if (t - lastFire > 180) { if (Math.random() < 0.7) fireInput(t); lastFire = t; } for (const n of nodes) { n.x = n.baseX + Math.sin(t / 1800 + n.phase) * 3; n.y = n.baseY + Math.cos(t / 2200 + n.phase) * 4; } for (const e of edges) { ctx.lineWidth = 0.6; ctx.strokeStyle = `rgba(255,255,255,${0.10 + e.w * 0.10})`; ctx.beginPath(); ctx.moveTo(e.a.x, e.a.y); ctx.lineTo(e.b.x, e.b.y); ctx.stroke(); const still = []; for (const f of e.fires) { const p = (t - f.start) / f.dur; if (p > 1) { activateAndForward(e.b, t); continue; } still.push(f); const eased = p * p * (3 - 2 * p); const hx = e.a.x + (e.b.x - e.a.x) * eased; const hy = e.a.y + (e.b.y - e.a.y) * eased; const ts = Math.max(0, eased - 0.18); const tx = e.a.x + (e.b.x - e.a.x) * ts; const ty = e.a.y + (e.b.y - e.a.y) * ts; const lg = ctx.createLinearGradient(tx, ty, hx, hy); lg.addColorStop(0, "rgba(255,220,200,0)"); lg.addColorStop(1, "rgba(255,240,220,0.95)"); ctx.strokeStyle = lg; ctx.lineWidth = 1.6; ctx.beginPath(); ctx.moveTo(tx, ty); ctx.lineTo(hx, hy); ctx.stroke(); ctx.fillStyle = "rgba(255,255,255,0.95)"; ctx.beginPath(); ctx.arc(hx, hy, 2.4, 0, Math.PI * 2); ctx.fill(); } e.fires = still; } for (const n of nodes) { n.activation = Math.max(0, n.activation - 0.018); const baseR = (n.layer === 0 || n.layer === LAYER_COUNT - 1) ? 4 : 3.2; const a = n.activation; if (a > 0.05) { const haloR = baseR + 14 * a; const hg = ctx.createRadialGradient(n.x, n.y, 0, n.x, n.y, haloR); hg.addColorStop(0, `rgba(255,235,210,${0.55 * a})`); hg.addColorStop(1, "rgba(255,235,210,0)"); ctx.fillStyle = hg; ctx.beginPath(); ctx.arc(n.x, n.y, haloR, 0, Math.PI * 2); ctx.fill(); } ctx.fillStyle = a > 0.05 ? `rgba(255, ${Math.floor(220 + 35 * a)}, ${Math.floor(200 + 55 * a)}, ${0.85 + 0.15 * a})` : "rgba(255,255,255,0.55)"; ctx.beginPath(); ctx.arc(n.x, n.y, baseR + a * 1.5, 0, Math.PI * 2); ctx.fill(); if (a > 0.2) { ctx.fillStyle = `rgba(255,255,255,${a})`; ctx.beginPath(); ctx.arc(n.x - 0.6, n.y - 0.6, 1.2, 0, Math.PI * 2); ctx.fill(); } } raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", onResize); }; }, []); return ; }; const Login = ({ onLogin }) => { const [email, setEmail] = React.useState(""); const [pw, setPw] = React.useState(""); const [step, setStep] = React.useState(0); // 0: Identity, 1: Password (manual) const [err, setErr] = React.useState({}); const [loading, setLoading] = React.useState(false); const isSsoEmail = email.toLowerCase().endsWith("@primuspartners.in"); const onNext = (e) => { if (e) e.preventDefault(); const ne = {}; if (!email.match(/.+@.+\..+/)) ne.email = "Enter a valid work email."; setErr(ne); if (ne.email) return; if (isSsoEmail) { setLoading(true); window.location.href = `/auth/sso/login?email=${encodeURIComponent(email)}`; } else { setStep(1); } }; const onManualLogin = async (e) => { e.preventDefault(); if (pw.length < 6) { setErr({ pw: "Password must be at least 6 characters." }); return; } setLoading(true); try { const data = await PrimaAPI.login(email, pw); onLogin(data); } catch (e) { setErr({ pw: e.message || "Invalid credentials." }); } finally { setLoading(false); } }; return (
{/* ── Left: Branding panel ── */}
Prima·Curator v3.2 · Primus Partners
The collective intelligence

Every deck, dataset
and decision —
woven into one mind.

{/* ── Right: Form panel ── */}
Sign in
= 0 ? " active" : ""}`} /> = 1 ? " active" : ""}`} />
Step {step + 1} of 2

{step === 0 ? "Welcome back." : "Enter password."}

{step === 0 ? "Sign in to contribute to the firm's collective intelligence." : "Enter your credentials to continue."}

{step === 0 ? ( { setEmail(e.target.value); setErr({}); }} placeholder="name@primuspartners.in" error={err.email} autoFocus /> ) : ( <>
{email}
{ setPw(e.target.value); setErr({}); }} error={err.pw} autoFocus /> )}
Secure by default · Microsoft Entra ID enforced · @primuspartners.in SSO required
Trouble signing in? Contact your administrator
); }; // ─── Profile onboarding ─── const ONBOARDING_STEPS = ["Basic", "Expertise", "Industry", "Review"]; const Onboarding = ({ user, onComplete, onSkip }) => { const [step, setStep] = React.useState(0); const [form, setForm] = React.useState({ email: user?.primus_email || user?.email || "", fullName: user?.full_name || "", empCode: user?.emp_code || "", designation: user?.designation || "", yoe: user?.yoe || "", primary: user?.primary_domain || "", secondary: user?.secondary_domain || "", skills: user?.skills || [], tools: user?.tools || [], methodologies: user?.methodologies || [], industries: user?.industries || [], regions: user?.regions || [], languages: user?.languages || [], }); const [errors, setErrors] = React.useState({}); const [loading, setLoading] = React.useState(false); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); const validateStep = () => { const e = {}; if (step === 0) { if (!form.fullName.trim()) e.fullName = "Full name is required."; if (!form.empCode.trim()) e.empCode = "Employee code is required."; else if (!/^[A-Z0-9-]{4,}$/i.test(form.empCode.trim())) e.empCode = "Use alphanumeric characters only."; if (!form.designation) e.designation = "Pick a designation."; if (form.yoe === "" || isNaN(+form.yoe) || +form.yoe < 0) e.yoe = "Enter a number ≥ 0."; else if (+form.yoe > 50) e.yoe = "That seems unusually high."; } else if (step === 1) { if (!form.primary) e.primary = "Pick a primary domain."; if (form.skills.length === 0) e.skills = "Add at least one skill."; } else if (step === 2) { if (form.industries.length === 0) e.industries = "Select at least one industry."; } setErrors(e); return Object.keys(e).length === 0; }; const next = async () => { if (!validateStep()) return; setLoading(true); try { if (step === 0) { await PrimaAPI.profileStep1({ full_name: form.fullName, emp_code: form.empCode, designation: form.designation, yoe: parseInt(form.yoe, 10), }); } else if (step === 1) { await PrimaAPI.profileStep2({ primary_domain: form.primary, secondary_domain: form.secondary || "", skills: form.skills, tools: form.tools, methodologies: form.methodologies, }); } else if (step === 2) { await PrimaAPI.profileStep3({ industries: form.industries, regions: form.regions, languages: form.languages, }); } setStep(s => Math.min(s + 1, ONBOARDING_STEPS.length - 1)); } catch (e) { setErrors({ api: e.message }); } finally { setLoading(false); } }; const back = () => setStep(s => Math.max(s - 1, 0)); const handleSubmit = async () => { setLoading(true); try { await PrimaAPI.profileFinalize(); onComplete(form); } catch (e) { setErrors({ api: e.message }); } finally { setLoading(false); } }; return (
Profile completion · One-time

Tell us about your practice.

Every dropdown drives downstream tagging. Pick what's true today — you can refine later.

{/* Steps */}
{ONBOARDING_STEPS.map((s, i) => (
{i < step ? : i + 1} {s}
{i < ONBOARDING_STEPS.length - 1 && }
))}
{errors.api && (
{errors.api}
)} {step === 0 && } {step === 1 && } {step === 2 && } {step === 3 && }
{step > 0 && } {step < ONBOARDING_STEPS.length - 1 ? ( ) : ( )}
); }; const BasicStep = ({ form, set, errors }) => { const isSso = form.email.toLowerCase().endsWith("@primuspartners.in"); return (
set("fullName", e.target.value)} placeholder="As on records" error={errors.fullName} readOnly={isSso} />
set("empCode", e.target.value.toUpperCase())} placeholder="e.g. PP-04129" error={errors.empCode} /> set("primary", v)} options={TAXONOMY.domains} error={errors.primary} />