feat: Ava stops waiting to be asked — the daily review

The primary action is now "Run daily review": no form, no question. It
starts the new INIT door dni-act-review, which is the same activity a
scheduler performs at 6am; the button exists so a room does not have to
wait until 6am. Ava sweeps every payer, department and front desk against a
materiality bar and, if nothing clears it, records "nothing to report" and
closes the case herself — it never reaches a desk. "Ask a question" stays,
demoted to a ghost button, for the follow-up path.

A second button replays a quiet morning: the demo data is static, so every
live sweep would flag the same August spike. Anchoring one run on
2026-07-26 lets it genuinely come back empty, and the contrast — one quiet
morning beside one $1.09M morning — is the pitch.

Queue cards and the case page now carry the sweep outcome: what she
reviewed, how many patterns she flagged, and what kind (payer rule change,
front desk, expiring appeals, …). A closed quiet case reads "Nothing to
report" rather than "Closed", because those are different outcomes.

Backend counterpart: sm2/custom-apps/denials-investigator/08_daily_review.sql
and the sweep-first charter in 05_ai_employee.sql.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-09-16 13:12:40 +05:30
parent 1c1c120202
commit b2377b4645
6 changed files with 193 additions and 13 deletions

View File

@ -5,6 +5,7 @@ import { Login } from './pages/Login';
import { QueueScreen } from './screens/QueueScreen';
import { CaseScreen } from './screens/CaseScreen';
import { NewInvestigationScreen } from './screens/NewInvestigationScreen';
import { RunReviewScreen } from './screens/RunReviewScreen';
/**
* Routes.
@ -25,6 +26,7 @@ export function App() {
<Route path="/q/:q" element={<QueueScreen />} />
<Route path="/c/:id" element={<CaseScreen />} />
<Route path="/new" element={<NewInvestigationScreen />} />
<Route path="/review" element={<RunReviewScreen />} />
{/* Awaiting Review is the landing page, not "all cases": it is the
queue with work in it, and the one the console exists to serve. */}
<Route path="/" element={<Navigate to="/q/review" replace />} />

View File

@ -30,8 +30,10 @@ export const DECISIONS_VIEW = 'dni-rv-ava-decisions';
// hardcoded: each activity's fields are read from view-service at open time.
// ---------------------------------------------------------------------------
export const ACT = {
START: 'dni-act-start', // INIT
REVIEW: 'dni-act-review', // INIT — the daily sweep. No question, no form.
START: 'dni-act-start', // INIT — a human asks a specific question
FINDINGS: 'dni-act-findings', // Ava
NOTHING: 'dni-act-nothing', // Ava — a quiet morning; closes the case itself
APPROVE: 'dni-act-approve',
CHANGES: 'dni-act-changes',
REJECT: 'dni-act-reject',
@ -187,3 +189,25 @@ export const LAKEHOUSE = {
schema: 'rcm',
tables: ['claims', 'payers', 'departments', 'payer_policy_updates', 'finance_monthly_summary'],
} as const;
/**
* A sweep's finding, as Ava classifies it. Shown as a chip so the queue reads
* "what kind of problem" without opening the case.
*/
export const FINDING_KIND: Record<string, string> = {
payer_policy: 'Payer rule change',
front_desk: 'Front-desk / registration',
coding: 'Coding',
documentation: 'Documentation',
medical_necessity: 'Medical necessity',
timely_filing: 'Timely filing',
expiring_appeals: 'Appeals about to expire',
other: 'Other',
};
/**
* The demo's quiet-morning replay. The data is static, so every sweep finds the
* same August spike; anchoring a run on this date lets Ava genuinely answer
* "nothing to report" — one quiet morning next to one $1.09M morning.
*/
export const QUIET_MORNING_AS_OF = '2026-07-26';

View File

@ -1,5 +1,5 @@
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
import { LogOut, Search } from 'lucide-react';
import { LogOut, Radar, Search } from 'lucide-react';
import { useZino } from '../api/provider';
import { QUEUES, prettyRole } from '../api/config';
import { NorthlakeLockup } from '../components/NorthlakeLogo';
@ -29,9 +29,14 @@ export function Shell() {
<NorthlakeLockup />
<div className="ml-auto flex items-center gap-3">
<Button kind="primary" onClick={() => navigate('/new')}>
{/* The sweep is the product; the question is the exception. */}
<Button kind="primary" onClick={() => navigate('/review')}>
<Radar className="h-4 w-4" />
Run daily review
</Button>
<Button kind="ghost" onClick={() => navigate('/new')} title="Ask Ava a specific question">
<Search className="h-4 w-4" />
Ask Ava
Ask a question
</Button>
{user && (

View File

@ -1,9 +1,10 @@
import { useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { ArrowLeft, Brain, Clock, User } from 'lucide-react';
import { ArrowLeft, Brain, Clock, Radar, User } from 'lucide-react';
import { useQuery, useZino } from '../api/provider';
import {
ACT,
FINDING_KIND,
NEEDS_HUMAN,
RECORD_VIEW,
STAGES,
@ -16,7 +17,7 @@ import { ActivityForm } from '../components/ActivityForm';
import { AvaLive } from '../components/AvaLive';
import { Findings, Recheck, SqlEvidence } from '../components/Findings';
import type { Action } from '../api/config';
import { dateTime, str } from '../format';
import { dateTime, num, str } from '../format';
/**
* One investigation, end to end.
@ -50,6 +51,11 @@ export function CaseScreen() {
const row = cases.data?.[0];
const state = str(row?.current_state_name);
const aiOwned = state === 'Investigating' || state === 'Monitoring';
const scanned = str(row?.scanned_summary);
const flagged = num(row?.flagged_count);
const kind = str(row?.finding_kind);
const isSweep = scanned !== '' || flagged != null || !str(row?.question);
const quiet = flagged === 0 && !str(row?.summary);
const trace = useQuery(() => client.avaTrace(String(id)), [id], true, aiOwned ? 4000 : 0);
const decisions = useQuery(() => client.avaDecisions(String(id)), [id], true, aiOwned ? 8000 : 0);
@ -81,9 +87,10 @@ export function CaseScreen() {
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<h1 className="tnum text-lg font-semibold text-ink">Case #{str(row.instance_id)}</h1>
<Badge tone={STAGE_TONE[state] ?? 'slate'} dot={aiOwned}>
{state}
<Badge tone={quiet ? 'slate' : (STAGE_TONE[state] ?? 'slate')} dot={aiOwned}>
{quiet ? 'Nothing to report' : state}
</Badge>
{kind && FINDING_KIND[kind] && <Badge tone="violet">{FINDING_KIND[kind]}</Badge>}
{aiOwned ? (
<span className="inline-flex items-center gap-1 text-xs text-ai">
<Brain className="h-3.5 w-3.5" /> Ava is working
@ -95,7 +102,12 @@ export function CaseScreen() {
)}
</div>
<p className="mt-1.5 max-w-2xl text-sm text-muted">
{str(row.question) || `Scope: ${str(row.scope) || 'all payers and departments'}`}
{str(row.question) ||
(isSweep
? `Daily review — every payer, department and front desk${
str(row.review_as_of) ? `, as of ${str(row.review_as_of).slice(0, 10)}` : ''
}. Nobody asked; Ava decides what is worth your time.`
: `Scope: ${str(row.scope) || 'all payers and departments'}`)}
</p>
</div>
@ -129,6 +141,28 @@ export function CaseScreen() {
)}
</section>
{/* ---------------- what the sweep covered ---------------- */}
{(scanned || flagged != null) && (
<section
className={`lift rounded-lg border px-4 py-3.5 ${
quiet ? 'border-line bg-panel' : 'border-ai-edge bg-ai-soft'
}`}
>
<div className="flex flex-wrap items-center gap-2">
<Radar className={`h-4 w-4 ${quiet ? 'text-muted' : 'text-ai'}`} />
<h2 className={`text-sm font-semibold ${quiet ? 'text-ink' : 'text-ai'}`}>
{quiet ? 'Reviewed. Nothing to report.' : `Reviewed. Flagged ${flagged ?? 1}.`}
</h2>
<span className="ml-auto text-[11px] text-faint">
6 payers · 8 departments · 9 desks · every week in the data
</span>
</div>
{scanned && (
<p className="mt-2 text-sm leading-relaxed whitespace-pre-wrap text-muted">{scanned}</p>
)}
</section>
)}
{/* ---------------- her conclusions ---------------- */}
<Findings row={row} />
<Recheck row={row} />

View File

@ -1,7 +1,7 @@
import { useNavigate, useParams } from 'react-router-dom';
import { Brain, Clock, User } from 'lucide-react';
import { useQuery, useZino } from '../api/provider';
import { QUEUES, RECORD_VIEW, STAGE_TONE } from '../api/config';
import { FINDING_KIND, QUEUES, RECORD_VIEW, STAGE_TONE } from '../api/config';
import { Badge, Card, Empty, ErrorNote, Spinner } from '../components/core';
import type { Row } from '../api/types';
import { dateTime, money, num, str } from '../format';
@ -82,6 +82,12 @@ function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) {
const scope = str(row.scope);
const question = str(row.question);
const aiOwned = state === 'Investigating' || state === 'Monitoring';
const scanned = str(row.scanned_summary);
const flagged = num(row.flagged_count);
const kind = str(row.finding_kind);
// A sweep that found nothing: closed by Ava herself, never on a desk.
const quiet = flagged === 0 && !summary;
const isSweep = scanned !== '' || flagged != null;
return (
<Card className="cursor-pointer transition-colors hover:border-brand/50" >
@ -90,9 +96,11 @@ function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) {
<span className="tnum text-[13px] font-semibold text-ink">
Case #{str(row.instance_id)}
</span>
<Badge tone={tone} dot={aiOwned}>
{state}
<Badge tone={quiet ? 'slate' : tone} dot={aiOwned}>
{quiet ? 'Nothing to report' : state}
</Badge>
{isSweep && <Badge tone="slate">daily review</Badge>}
{kind && FINDING_KIND[kind] && <Badge tone="violet">{FINDING_KIND[kind]}</Badge>}
{aiOwned ? (
<span className="inline-flex items-center gap-1 text-[11px] text-ai">
<Brain className="h-3 w-3" /> Ava
@ -109,7 +117,10 @@ function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) {
</div>
<p className="mt-2 line-clamp-2 text-sm text-ink">
{summary || question || `Scope: ${scope || 'all payers'}`}
{summary ||
(quiet ? scanned : '') ||
question ||
(isSweep ? 'Ava is reviewing every payer, department and desk…' : `Scope: ${scope || 'all payers'}`)}
</p>
{(atRisk || recoverable) && (

View File

@ -0,0 +1,104 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Brain, Database, Moon, Radar } from 'lucide-react';
import { useZino } from '../api/provider';
import { ACT, LAKEHOUSE, QUIET_MORNING_AS_OF, WORKFLOW } from '../api/config';
import { Button, Card, ErrorNote } from '../components/core';
/**
* Run the daily review, now.
*
* There is no form. This is the same activity a scheduler performs at 6am;
* the button exists so a room does not have to wait until 6am. Nothing is
* asked of Ava — she decides what, if anything, is worth a person's time.
*
* The second button anchors the sweep on a date before the August spike. The
* demo data is static, so without it every run flags the same thing; with it,
* one run can genuinely come back "nothing to report" — which is what most
* mornings look like, and the contrast is the point.
*/
export function RunReviewScreen() {
const navigate = useNavigate();
const { client } = useZino();
const [busy, setBusy] = useState<'now' | 'quiet' | null>(null);
const [err, setErr] = useState<string | null>(null);
async function run(asOf?: string) {
setBusy(asOf ? 'quiet' : 'now');
setErr(null);
try {
// The activity's one field is optional; omit it entirely for a live run
// so the workflow sees a genuinely empty submission.
const res = await client.startInstance(
WORKFLOW,
ACT.REVIEW,
asOf ? { review_as_of__review: asOf } : {},
);
if (res.instance_id) navigate(`/c/${res.instance_id}`);
else navigate('/q/investigating');
} catch (e) {
setErr((e as { message?: string })?.message ?? 'Could not start the review');
setBusy(null);
}
}
return (
<div className="mx-auto max-w-2xl space-y-4">
<header>
<h1 className="text-xl font-semibold text-ink">Daily review</h1>
<p className="mt-0.5 text-sm text-muted">
Ava looks at every payer, department and front desk, and only opens a case if
something clears the bar. Most mornings she reports nothing.
</p>
</header>
{err && <ErrorNote>{err}</ErrorNote>}
<Card className="px-4 py-4">
<div className="flex items-start gap-3">
<span className="rounded-full bg-ai-soft p-2 text-ai ring-1 ring-ai-edge ring-inset">
<Brain className="h-4 w-4" />
</span>
<div className="min-w-0 flex-1 text-sm text-muted">
<p className="font-semibold text-ink">What she checks</p>
<ul className="mt-1.5 list-disc space-y-0.5 pl-4">
<li>That the claims data reconciles with the finance report</li>
<li>Denial rate vs baseline — by payer, reason code, department, front desk, week</li>
<li>Money about to fall out of an appeal window unfiled</li>
</ul>
<p className="mt-2 text-xs text-faint">
The bar: a denied count up more than 40% on meaningful volume, or more than $100K
expiring. Below it, she watches and says nothing.
</p>
<p className="mt-2 inline-flex items-center gap-1.5 text-xs text-faint">
<Database className="h-3.5 w-3.5" />
Read-only against {LAKEHOUSE.platform} · {LAKEHOUSE.catalog}.{LAKEHOUSE.schema}
</p>
</div>
</div>
<div className="mt-5 flex flex-wrap items-start gap-3 border-t border-line pt-4">
<div>
<Button kind="primary" onClick={() => run()} busy={busy === 'now'} disabled={busy !== null}>
<Radar className="h-4 w-4" />
Run the review now
</Button>
<p className="mt-1 max-w-[16rem] text-[11px] text-faint">
Normally fires at 6am on its own. This is the same activity.
</p>
</div>
<div>
<Button kind="secondary" onClick={() => run(QUIET_MORNING_AS_OF)} busy={busy === 'quiet'} disabled={busy !== null}>
<Moon className="h-4 w-4" />
Replay a quiet morning
</Button>
<p className="mt-1 max-w-[16rem] text-[11px] text-faint">
Anchored on {QUIET_MORNING_AS_OF}, before anything went wrong — to see what
&ldquo;nothing to report&rdquo; looks like.
</p>
</div>
</div>
</Card>
</div>
);
}