console: the Needs a Person queue, and what the AI said when it stopped

An AI employee here has no escalate tool — the platform withholds it from
autonomous employees on purpose — so "a person should take this" has to be
an activity in the workflow. It now is: Flag for Review, which lands the
lead in a new stage, Needs a Person.

The console side of that: the stage joins the nav under Needs you, its two
actions are Send back to the workflow / Send to underwriting, the flag
fields lead the lead-file groups, and a flagged lead opens on an amber
banner quoting the AI's own sentence about what stopped it, with the
blocker turned into something an operator can act on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-09-10 12:28:53 +05:30
parent 54333ef847
commit 3dd482f180
3 changed files with 57 additions and 0 deletions

View File

@ -52,6 +52,11 @@ export const STAGES = [
// policy nobody paid for. It gets its own band on the overview so the role
// that owns it does not have to find it in a list of things mostly handled
// by somebody else.
// WHERE THE AI PUTS DOWN WHAT IT CANNOT HONESTLY FINISH. An AI employee has
// no way to raise a ticket and no way to send a message; the only move it
// has for "a person should take this" is Flag for Review, and it lands here
// with the reason it wrote. Nothing is running on these leads.
{ uid: 'zk-state-review', name: 'Needs a Person', kind: 'needs', need: 'Flagged by the AI', by: 'operations' },
{ uid: 'zk-state-payment', name: 'Payment Pending', kind: 'needs', need: 'Premium confirmation', by: 'operations', approval: 'ops_admin', approvalLabel: 'Premium awaiting your confirmation', approvalNote: 'Cover cannot start until the premium is received — section 64VB. Only operations can confirm it.' },
{ uid: 'zk-state-issued', name: 'Policy Issued', kind: 'auto', doing: 'Closing the file', by: 'Engage AI' },
// Nurture. There is no scheduler yet, so Resume Outreach is a button and it
@ -160,6 +165,10 @@ const STATE_ACTIVITIES = {
{ uid: 'zk-act-uw-decline', label: 'Decline the risk', by: 'you', role: 'do' },
{ uid: 'zk-act-uw-prepare', label: 'Prepare Referral', by: 'Referral AI', role: 'force' },
],
'zk-state-review': [
{ uid: 'zk-act-review-resume', label: 'Send back to the workflow', by: 'you', role: 'do' },
{ uid: 'zk-act-review-refer', label: 'Send to underwriting', by: 'you', role: 'do' },
],
'zk-state-payment': [
{ uid: 'zk-act-realise', label: 'Confirm premium received', by: 'you', role: 'do' },
// Not offered: the chase is a SCHEDULED activity now (86), booked 24h

View File

@ -45,8 +45,22 @@ const MONEY = new Set(['quoted_premium','quoted_od_premium','quoted_tp_premium',
'sme_burglary_si','sme_ee_si','sme_bi_gross_profit','sme_claims_36m_amount','sme_stock_si'])
/** Field groups, in the order the lead was actually worked. */
/* What the AI said stopped it, turned into a sentence an operator can act on.
Keyed by the stored value so an unknown one degrades to the generic line. */
const BLOCKER = {
missing_evidence: 'It says the evidence it needed was not there.',
contradictory_data: 'It says the evidence it had contradicts itself.',
tool_failed: 'It says a lookup or a tool it depends on failed.',
outside_my_remit: 'It says the judgement is a person\u2019s to make, not its own.',
other: 'It could not complete its step.',
}
const GROUPS = [
['Source', ['lead_ref','product_line','source_channel','partner_code','partner_branch','rm_or_agent_id','consent_artefact']],
// First, because when a lead is flagged this is the only thing anyone opens
// it to read. Empty on every lead that was never flagged, and an empty group
// does not render.
['Flagged for review', ['review_blocker','review_reason','review_return','review_note']],
['Customer', ['customer_name','entity_name','mobile','email','pan','gstin','udyam_no']],
['Intake', ['lead_score','attribution_status','attribution_reason','dedupe_match_ref','eligibility_outcome','eligibility_reason']],
['Contact', ['contact_outcome','outreach_window','contact_notes','call_transcript']],
@ -430,6 +444,31 @@ export default function Lead() {
</div>
) : null}
</div>
) : stage?.uid === 'zk-state-review' ? (
/* THE AI PUT THIS DOWN. It could not finish its step honestly and said
so in the workflow, which is the only way it has of reaching a person.
Its own words lead, because deciding where the lead resumes is a
judgement made on them. */
<div className="attn" role="status">
<div>
<h3><span className="dot dot--amber" />The AI stopped here and asked for a person</h3>
{row.review_reason ? <p className="attn__said">{row.review_reason}</p> : null}
<p>
{BLOCKER[row.review_blocker] ?? 'It could not complete its step.'}{' '}
Nothing is running on this lead. Read the trail, fix what is missing, then send it
back to the workflow at the right step or hand it to an underwriter.
</p>
</div>
<div className="attn__acts">
{actions.filter((a) => a.role === 'do').map((a) => (
<button key={a.uid} type="button"
className={'btn ' + (a.uid === 'zk-act-review-resume' ? 'btn--navy' : 'btn--ghost')}
onClick={() => { setOpen(a.uid); goToAction() }}>
{a.label}
</button>
))}
</div>
</div>
) : primaryAction ? (
<div className="attn" role="status">
<div>

View File

@ -388,3 +388,12 @@
.kpis { grid-template-columns: repeat(2, 1fr); row-gap: 16px; }
.facts { grid-template-columns: repeat(2, 1fr); row-gap: 16px; }
}
/* The AI's own words, when it flagged a lead. Quoted, because deciding what
to do next is a judgement made on the sentence it wrote. */
.attn__said {
margin: 8px 0 0 !important; padding: 8px 14px;
border-left: 2px solid var(--zk-amber-bar); background: #fff;
border-radius: 0 var(--r-sm) var(--r-sm) 0;
font-size: var(--fs-md) !important; color: var(--zk-ink) !important; max-width: 78ch;
}