diff --git a/src/App.tsx b/src/App.tsx index 000bd5a..47b30f9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { } /> } /> } /> + } /> {/* 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. */} } /> diff --git a/src/api/config.ts b/src/api/config.ts index 3bc449c..6a8afe6 100644 --- a/src/api/config.ts +++ b/src/api/config.ts @@ -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 = { + 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'; diff --git a/src/layout/Shell.tsx b/src/layout/Shell.tsx index fd8ae70..24f9dea 100644 --- a/src/layout/Shell.tsx +++ b/src/layout/Shell.tsx @@ -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() {
- + {user && ( diff --git a/src/screens/CaseScreen.tsx b/src/screens/CaseScreen.tsx index 7832dfa..a007dc4 100644 --- a/src/screens/CaseScreen.tsx +++ b/src/screens/CaseScreen.tsx @@ -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() {

Case #{str(row.instance_id)}

- - {state} + + {quiet ? 'Nothing to report' : state} + {kind && FINDING_KIND[kind] && {FINDING_KIND[kind]}} {aiOwned ? ( Ava is working @@ -95,7 +102,12 @@ export function CaseScreen() { )}

- {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'}`)}

@@ -129,6 +141,28 @@ export function CaseScreen() { )} + {/* ---------------- what the sweep covered ---------------- */} + {(scanned || flagged != null) && ( +
+
+ +

+ {quiet ? 'Reviewed. Nothing to report.' : `Reviewed. Flagged ${flagged ?? 1}.`} +

+ + 6 payers · 8 departments · 9 desks · every week in the data + +
+ {scanned && ( +

{scanned}

+ )} +
+ )} + {/* ---------------- her conclusions ---------------- */} diff --git a/src/screens/QueueScreen.tsx b/src/screens/QueueScreen.tsx index 9e357c2..78f6cce 100644 --- a/src/screens/QueueScreen.tsx +++ b/src/screens/QueueScreen.tsx @@ -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 ( @@ -90,9 +96,11 @@ function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) { Case #{str(row.instance_id)} - - {state} + + {quiet ? 'Nothing to report' : state} + {isSweep && daily review} + {kind && FINDING_KIND[kind] && {FINDING_KIND[kind]}} {aiOwned ? ( Ava @@ -109,7 +117,10 @@ function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) {

- {summary || question || `Scope: ${scope || 'all payers'}`} + {summary || + (quiet ? scanned : '') || + question || + (isSweep ? 'Ava is reviewing every payer, department and desk…' : `Scope: ${scope || 'all payers'}`)}

{(atRisk || recoverable) && ( diff --git a/src/screens/RunReviewScreen.tsx b/src/screens/RunReviewScreen.tsx new file mode 100644 index 0000000..29156d4 --- /dev/null +++ b/src/screens/RunReviewScreen.tsx @@ -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(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 ( +
+
+

Daily review

+

+ Ava looks at every payer, department and front desk, and only opens a case if + something clears the bar. Most mornings she reports nothing. +

+
+ + {err && {err}} + + +
+ + + +
+

What she checks

+
    +
  • That the claims data reconciles with the finance report
  • +
  • Denial rate vs baseline — by payer, reason code, department, front desk, week
  • +
  • Money about to fall out of an appeal window unfiled
  • +
+

+ The bar: a denied count up more than 40% on meaningful volume, or more than $100K + expiring. Below it, she watches and says nothing. +

+

+ + Read-only against {LAKEHOUSE.platform} · {LAKEHOUSE.catalog}.{LAKEHOUSE.schema} +

+
+
+ +
+
+ +

+ Normally fires at 6am on its own. This is the same activity. +

+
+
+ +

+ Anchored on {QUIET_MORNING_AS_OF}, before anything went wrong — to see what + “nothing to report” looks like. +

+
+
+
+
+ ); +}