/** * Human Review Queue — formalizes FDE Stage 23 (HITL). Two distinct * actions here: * * 1. Individual approve/reject (unchanged) — a recruiter's own real * decision on one candidate, calls POST /api/candidates/{id}/review * immediately. This already was human-in-the-loop, so it doesn't need * any extra gate. * * 2. Bulk propose (new) — select several candidates (any tier, via the * filter below, not just "To be Considered") and propose accepting or * rejecting all of them at once. This does NOT change any candidate — * it only records a pending proposal (POST /api/bulk-decisions) that * an admin must separately approve before anything actually happens. * The point: the AI/scoring pipeline's ranking_tier is only ever a * RECOMMENDATION, and a recruiter acting on a batch of them in bulk * doesn't finalize anything either — only an explicit admin approval * does (see Pipeline.jsx's "Pending Bulk Approvals" panel). */ var TIER_OPTIONS = ["To be Considered", "Recommended for Fast-Track", "Rejected"]; function ReviewQueue(props) { var api = props.api; var permissions = props.permissions || []; var reviewerName = (props.currentUser && props.currentUser.username) || ""; var state = React.useState({ loading: true, error: null, candidates: [], tier: "To be Considered" }); var data = state[0]; var setData = state[1]; var actionState = React.useState({}); // candidate_id -> "pending" | "done" | error message var actions = actionState[0]; var setActions = actionState[1]; var modalState = React.useState(null); // { candidateId, candidateName } | null var modalCandidate = modalState[0]; var setModalCandidate = modalState[1]; var configOpenState = React.useState(false); var configOpen = configOpenState[0]; var setConfigOpen = configOpenState[1]; var selectedState = React.useState({}); // candidate_id -> true var selected = selectedState[0]; var setSelected = selectedState[1]; var bulkState = React.useState({ submitting: false, error: null, result: null }); var bulk = bulkState[0]; var setBulk = bulkState[1]; function load() { setData(function (d) { return Object.assign({}, d, { loading: true, error: null }); }); api.listCandidates(data.tier) .then(function (result) { setData(function (d) { return Object.assign({}, d, { loading: false, candidates: result.candidates }); }); setSelected({}); }) .catch(function (err) { setData(function (d) { return Object.assign({}, d, { loading: false, error: err.message }); }); }); } React.useEffect(load, [data.tier]); function setTier(tier) { setData(function (d) { return Object.assign({}, d, { tier: tier }); }); } function handleDecision(candidateId, decision) { setActions(function (a) { var next = Object.assign({}, a); next[candidateId] = "pending"; return next; }); api.reviewCandidate(candidateId, decision, reviewerName, "") .then(function () { setActions(function (a) { var next = Object.assign({}, a); next[candidateId] = "done"; return next; }); setData(function (d) { return Object.assign({}, d, { candidates: d.candidates.filter(function (c) { return c.candidate_id !== candidateId; }) }); }); }) .catch(function (err) { setActions(function (a) { var next = Object.assign({}, a); next[candidateId] = "Error: " + err.message; return next; }); }); } function toggleSelected(candidateId) { setSelected(function (s) { var next = Object.assign({}, s); if (next[candidateId]) delete next[candidateId]; else next[candidateId] = true; return next; }); } var selectedIds = Object.keys(selected); function handleBulkPropose(decision) { if (selectedIds.length === 0) return; setBulk({ submitting: true, error: null, result: null }); api.proposeBulkDecision(decision, selectedIds, reviewerName, "") .then(function (result) { setBulk({ submitting: false, error: null, result: result }); setSelected({}); }) .catch(function (err) { setBulk({ submitting: false, error: err.message, result: null }); }); } return (

Human Review Queue

The AI only recommends a tier — approve/reject here (individually or, via the checkboxes, as a batch proposal) is what actually decides. Bulk proposals need separate admin approval before anything changes; individual approve/reject below takes effect immediately.

Reviewing as {reviewerName}
{selectedIds.length > 0 && (
{selectedIds.length} selected
)} {bulk.result && (
{bulk.result.message} ({bulk.result.candidate_count} candidate(s))
)} {bulk.error &&
{bulk.error}
} {data.error &&
Couldn't load queue: {data.error}
} {data.loading &&

Loading…

} {!data.loading && data.candidates.length === 0 && !data.error && (

No candidates in this tier.

)}
{data.candidates.map(function (c) { var action = actions[c.candidate_id]; return (
{c.Name} — {c["JD Match"]} match, {c["Candidate Type"]}

{c["Profile Summary"]}

{action === "pending" && Saving…} {action === "done" && Recorded} {action && action !== "pending" && action !== "done" && {action}} {(!action || (action !== "pending" && action !== "done")) && ( )}
); })}
{modalCandidate && ( )} {configOpen && ( )}
); }