From e294df6eba6b71b0877b4c98a8b750be3cac0022 Mon Sep 17 00:00:00 2001 From: Yashas Date: Wed, 16 Sep 2026 13:38:09 +0530 Subject: [PATCH] design: three questions, three sizes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuilt the two screens for a person with ninety seconds, not an analyst. Every screen now answers, in order and at descending size: what happened (one sentence, 21px), how much (three numbers you can read across a table — claims denied, money at stake, still recoverable with its first deadline), and what she wants approved (the ranked list, with the decision buttons directly under it). The full analysis, her SQL and the live trace sit behind disclosures — one click for the reviewer who wants to check, zero clutter for the manager who does not. The queue opens on a briefing strip: "1 decision waiting for you — $1,091,000 recoverable". A manager who reads only that line has the day. Cards lead with the verdict sentence and the recoverable figure; a waiting case says "Needs your decision", not "Awaiting Review". Jargon translated where a business reader meets it: "data reconciled" became "Numbers checked against Finance"; "97% confident" became "Ava is 97% sure"; the trace steps read "Asked the data" and "Thought about it". Ava's SHOUTED recommendation headings are sentence-cased. While Ava is still working, the trace is the page — there is nothing else to show yet, and watching her query is the demo. Co-Authored-By: Claude Fable 5.1 --- src/components/AvaLive.tsx | 20 +- src/components/Findings.tsx | 369 ++++++++++++++++++++---------------- src/format.ts | 36 ++++ src/layout/Shell.tsx | 2 +- src/screens/CaseScreen.tsx | 282 ++++++++++++++------------- src/screens/QueueScreen.tsx | 176 +++++++++-------- src/styles.css | 1 + 7 files changed, 505 insertions(+), 381 deletions(-) diff --git a/src/components/AvaLive.tsx b/src/components/AvaLive.tsx index ca2b0a8..423a8c2 100644 --- a/src/components/AvaLive.tsx +++ b/src/components/AvaLive.tsx @@ -39,14 +39,14 @@ const TYPE_META: Record< string, { label: string; icon: typeof Database; tone: 'violet' | 'sky' | 'slate' | 'emerald' } > = { - context: { label: 'Read the case', icon: FileSearch, tone: 'slate' }, - tool: { label: 'Read the case', icon: FileSearch, tone: 'slate' }, + context: { label: 'Read the case file', icon: FileSearch, tone: 'slate' }, + tool: { label: 'Read the case file', icon: FileSearch, tone: 'slate' }, knowledge: { label: 'Checked her notes', icon: FileSearch, tone: 'slate' }, - llm: { label: 'Thought', icon: Brain, tone: 'violet' }, + llm: { label: 'Thought about it', icon: Brain, tone: 'violet' }, plan: { label: 'Planned', icon: Brain, tone: 'violet' }, - api: { label: `Queried ${LAKEHOUSE.platform}`, icon: Database, tone: 'sky' }, - decision: { label: 'Decided', icon: Gavel, tone: 'emerald' }, - escalation: { label: 'Escalated', icon: AlertTriangle, tone: 'slate' }, + api: { label: `Asked the data (${LAKEHOUSE.platform})`, icon: Database, tone: 'sky' }, + decision: { label: 'Wrote up her decision', icon: Gavel, tone: 'emerald' }, + escalation: { label: 'Asked for a person', icon: AlertTriangle, tone: 'slate' }, }; function meta(type: string) { @@ -95,9 +95,9 @@ export function AvaLive({
-

Ava's working

+

How Ava is working this out

- Every step, from the platform's own trace — not a summary + Every step she takes, recorded by the platform as she takes it

@@ -117,7 +117,7 @@ export function AvaLive({

{working && } {working - ? 'Ava has picked up the case. Her first query appears here within a few seconds.' + ? 'Ava has picked up the case. Her first question to the data appears here within a few seconds.' : 'No steps recorded for this case.'}

) : ( @@ -209,7 +209,7 @@ export function AvaLive({ - Every statement wrapped in a SELECT — she cannot write + She can only read — nothing she does can change the data diff --git a/src/components/Findings.tsx b/src/components/Findings.tsx index fb26ecf..7d79493 100644 --- a/src/components/Findings.tsx +++ b/src/components/Findings.tsx @@ -1,175 +1,183 @@ -import { AlertTriangle, CheckCircle2, ChevronDown, Target } from 'lucide-react'; import { useState } from 'react'; +import { AlertTriangle, CheckCircle2, ChevronDown, FileSearch } from 'lucide-react'; import { Badge, Card, Facts } from './core'; import type { Row } from '../api/types'; -import { money, num, pct, recommendations, splitRecommendation, sqlSteps, str } from '../format'; +import { + firstDeadline, + headline, + money, + num, + pct, + recommendations, + shortDate, + splitRecommendation, + sqlSteps, + str, +} from '../format'; /** - * What Ava concluded, and what she wants done about it. + * Ava's findings, laid out for a person who has ninety seconds. * - * EVERY VALUE HERE IS HERS. Nothing on this panel is computed by the frontend - * or inferred from anything — each field is read off the instance exactly as - * she submitted it. The violet tint is the console's one semantic rule: if it - * is violet, an agent produced it. + * THREE QUESTIONS, THREE SIZES. What happened — one sentence, large. How much + * — three numbers you can read from across a table. What to do — the ranked + * list, with the decision buttons rendered right under it by the case screen. + * Everything a reviewer needs to act is above the fold; everything an analyst + * would want to check is behind "the full story" and "her evidence". * - * The recommendations are deliberately cards rather than a paragraph. A - * manager approves a LIST of actions with owners and amounts; rendering it as - * prose invites him to skim it as commentary. + * EVERY VALUE IS HERS. Nothing here is computed by the frontend — each field is + * read off the instance as she submitted it. Violet marks what an agent + * produced; nothing else is violet. */ -export function Findings({ row }: { row: Row }) { + +/** The headline card: one sentence, and the three numbers. */ +export function Verdict({ row }: { row: Row }) { const summary = str(row.summary); - const cause = str(row.root_cause); - const secondary = str(row.secondary_findings); - const recs = recommendations(row.recommendations); - const confidence = num(row.confidence); - const reconciled = row.data_reconciled === true || str(row.data_reconciled) === 'true'; + if (!summary) return null; + const line = headline(summary); const atRisk = num(row.amount_at_risk); const recoverable = num(row.recoverable_amount); const affected = num(row.affected_claims); - - if (!summary) return null; + const deadline = firstDeadline(row.recommendations); + const confidence = num(row.confidence); + const reconciled = row.data_reconciled === true || str(row.data_reconciled) === 'true'; return ( -
- {/* --------------- the numbers, before the prose --------------- */} -
- - - +
+

What Ava found

+

{line}

+
+ + + {reconciled ? 'Numbers checked against Finance' : 'Numbers NOT yet checked against Finance'} + + {confidence != null && ( + + Ava is {pct(confidence * 100, 0)} sure + + )} +
+
+ +
+ + +
- - {/* --------------- what she found --------------- */} -
-
- -

What Ava found

-
- {/* Her own first check, surfaced as a badge: she reconciles the - claims data against the finance report before she trusts it, - and a reviewer should be able to see that she did. */} - - {reconciled ? 'data reconciled' : 'data NOT reconciled'} - - {confidence != null && ( - = 0.9 ? 'emerald' : confidence >= 0.7 ? 'amber' : 'rose'}> - {pct(confidence * 100, 0)} confident - - )} -
-
- -
-

{summary}

- - {cause && ( -
-

- Root cause -

-

{cause}

-
- )} - - {secondary && !/^none found/i.test(secondary.trim()) && ( -
-

- A second, separate pattern -

-

{secondary}

-
- )} -
-
- - {/* --------------- what she wants done --------------- */} - {recs.length > 0 && ( - -
    - {recs.map((block, i) => { - const { title, body } = splitRecommendation(block); - return ( -
  1. -
    - - {i + 1} - -
    - {title &&

    {title}

    } -

    - {body} -

    -
    -
    -
  2. - ); - })} -
-
- )} -
+ ); } -/** - * Her evidence: every statement she ran, in order, with her own note on what - * came back. - * - * Collapsed by default. It is the proof, not the story — a reviewer opens it to - * check a number, and a sceptic in the room opens it to see that the SQL is - * real. Both are better served by a closed section than by a wall of query text - * above the recommendations. - */ +/** The decision list. Rendered directly above the Approve / Send back / Reject buttons. */ +export function Recommendations({ row }: { row: Row }) { + const recs = recommendations(row.recommendations); + if (recs.length === 0) return null; + return ( +
+
+

What she wants you to approve

+

Ranked by money. Each has an owner.

+
+
    + {recs.map((block, i) => { + const { title, body } = splitRecommendation(block); + return ( +
  1. + + {i + 1} + +
    + {title &&

    {sentenceCase(title)}

    } +

    {body}

    +
    +
  2. + ); + })} +
+
+ ); +} + +/** The paragraph version — for the reader who wants the whole account. Collapsed. */ +export function FullStory({ row }: { row: Row }) { + const [open, setOpen] = useState(false); + const summary = str(row.summary); + const cause = str(row.root_cause); + const secondary = str(row.secondary_findings); + if (!summary) return null; + const hasSecondary = secondary && !/^none found/i.test(secondary.trim()); + + return ( + setOpen(!open)} + icon={} + title="The full story" + hint={hasSecondary ? 'Analysis, root cause, and a second pattern she noticed' : 'Her full analysis and the root cause'} + tone="ai" + > +
+

{summary}

+ {cause && ( +
+

Why it happened

+

{cause}

+
+ )} + {hasSecondary && ( +
+

+ A second, separate problem +

+

{secondary}

+
+ )} +
+
+ ); +} + +/** Every SQL statement she ran. For the sceptic in the room. Collapsed. */ export function SqlEvidence({ row }: { row: Row }) { const [open, setOpen] = useState(false); const steps = sqlSteps(row.sql_log); if (steps.length === 0) return null; return ( -
- - - {open && ( -
    - {steps.map((s, i) => ( -
  1. - {s.label && ( -

    - {s.label} -

    - )} -
    {s.sql}
    - {s.note &&

    {s.note}

    } -
  2. - ))} -
- )} -
+ setOpen(!open)} + icon={} + title="Her evidence" + hint={`${steps.length} database queries, exactly as she ran them — anyone can re-run these`} + > +
    + {steps.map((s, i) => ( +
  1. + {s.label && ( +

    {s.label}

    + )} +
    {s.sql}
    + {s.note &&

    {s.note}

    } +
  2. + ))} +
+
); } -/** The re-check, when there is one. Same shape both ways: a rate against its - * baseline, so "recovered" and "still elevated" are read the same way. */ +/** The re-check. Same shape either way, so "fixed" and "not fixed" read alike. */ export function Recheck({ row }: { row: Row }) { const summary = str(row.recheck_summary); if (!summary) return null; @@ -180,17 +188,17 @@ export function Recheck({ row }: { row: Row }) { return ( {better ? 'back to baseline' : 'still elevated'}} + title={better ? 'The fix worked' : 'The fix has not landed yet'} + subtitle="Ava came back on her own to check" + actions={{better ? 'back to normal' : 'still elevated'}} > -
+

{summary}

@@ -199,23 +207,62 @@ export function Recheck({ row }: { row: Row }) { ); } -function Stat({ - label, - value, - tone, - note, -}: { - label: string; - value: string; - tone?: 'good' | 'bad'; - note?: string; -}) { +// --------------------------------------------------------------------------- + +function Big({ label, value, tone, note }: { label: string; value: string; tone?: 'good' | 'bad'; note?: string }) { const text = tone === 'good' ? 'text-good' : tone === 'bad' ? 'text-bad' : 'text-ink'; return ( -
+

{label}

-

{value}

- {note &&

{note}

} +

{value}

+ {note &&

{note}

}
); } + +export function Disclosure({ + open, + onToggle, + icon, + title, + hint, + tone, + children, +}: { + open: boolean; + onToggle: () => void; + icon: React.ReactNode; + title: string; + hint?: string; + tone?: 'ai'; + children: React.ReactNode; +}) { + return ( +
+ + {open &&
{children}
} +
+ ); +} + +/** "APPEALS — FILE RETRO-AUTH IMMEDIATELY ($1,091,000 …)" → "Appeals — file retro-auth immediately ($1,091,000 …)". + * Ava shouts her headings; a manager's screen shouldn't. Acronyms and codes survive. */ +function sentenceCase(s: string): string { + if (s !== s.toUpperCase()) return s; + return s + .toLowerCase() + .replace(/^\w/, (c) => c.toUpperCase()) + .replace(/\b(co-\d+|cpt|blueshield|bluecross|cardiology|northlake|d\. whitfield|dr\.)\b/gi, (m) => + m.replace(/\b\w/g, (c) => c.toUpperCase()), + ); +} diff --git a/src/format.ts b/src/format.ts index 2b569e8..dacbf78 100644 --- a/src/format.ts +++ b/src/format.ts @@ -121,3 +121,39 @@ export function sqlSteps(v: unknown): Array<{ label: string; sql: string; note: }) .filter((s) => s.sql); } + +/** + * The one sentence a busy person reads. + * + * Ava's summary is a paragraph written for someone who will read it all. The + * headline is its first sentence or two — enough to say what happened, short + * enough to be set at 22px. Cut on sentence boundaries, never mid-clause. + */ +export function headline(summary: unknown, maxChars = 220): string { + const s = str(summary).replace(/\s+/g, ' ').trim(); + if (!s) return ''; + const sentences = s.match(/[^.!?]+[.!?]+(\s|$)/g) ?? [s]; + let out = ''; + for (const sen of sentences) { + if (out && (out + sen).length > maxChars) break; + out += sen; + if (out.length > maxChars * 0.6) break; + } + return out.trim() || s.slice(0, maxChars); +} + +/** The earliest date Ava mentioned in her recommendations — usually the first + * appeal deadline. Null if she didn't name one. */ +export function firstDeadline(recs: unknown): string | null { + const m = str(recs).match(/\b(2026-\d{2}-\d{2})\b/g); + if (!m) return null; + const sorted = [...new Set(m)].sort(); + return sorted[0] ?? null; +} + +/** 2026-10-02 → "Oct 2" */ +export function shortDate(iso: string | null): string { + if (!iso) return ''; + const d = new Date(iso + 'T00:00:00'); + return Number.isNaN(d.getTime()) ? iso : d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); +} diff --git a/src/layout/Shell.tsx b/src/layout/Shell.tsx index 20fb3a9..b27b6cf 100644 --- a/src/layout/Shell.tsx +++ b/src/layout/Shell.tsx @@ -93,7 +93,7 @@ export function Shell() { -
+
diff --git a/src/screens/CaseScreen.tsx b/src/screens/CaseScreen.tsx index 2356aaa..ecf5e0b 100644 --- a/src/screens/CaseScreen.tsx +++ b/src/screens/CaseScreen.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { Link, useParams } from 'react-router-dom'; -import { ArrowLeft, Brain, Clock, Radar, User } from 'lucide-react'; +import { ArrowLeft, Brain, Radar } from 'lucide-react'; import { useQuery, useZino } from '../api/provider'; import { ACT, @@ -12,31 +12,34 @@ import { actionsFor, stageStep, } from '../api/config'; -import { Badge, Button, Card, ErrorNote, Facts, Spinner } from '../components/core'; +import { Badge, Button, ErrorNote, Facts, Spinner } from '../components/core'; import { ActivityForm } from '../components/ActivityForm'; import { AvaLive } from '../components/AvaLive'; -import { Findings, Recheck, SqlEvidence } from '../components/Findings'; +import { Disclosure, FullStory, Recheck, Recommendations, SqlEvidence, Verdict } from '../components/Findings'; import type { Action } from '../api/config'; import { dateTime, num, str } from '../format'; /** - * One investigation, end to end. + * One investigation, for a person with ninety seconds. * - * The page is ordered the way a reviewer reads it, not the way the data is - * shaped: what Ava concluded first, then what she wants done, then her - * evidence, then the raw trace of how she got there. The actions sit at the top - * because on a case in Awaiting Review, deciding is the only reason this screen - * is open. + * ORDER IS THE DESIGN. What happened (one sentence, large) → how much (three + * numbers) → what she wants approved → the buttons. That is the whole + * above-the-fold. The full analysis, her evidence and the live trace sit + * behind disclosures underneath, for the reviewer who wants to check and the + * sceptic who wants to see the SQL. * - * POLLING is on while Ava owns the case (Investigating / Monitoring). That is - * the whole live demo: the trace grows, query by query, with nobody touching - * the keyboard. When the case is with a person nothing is moving, so the poll - * stops rather than hammering the gateway for a screen that cannot change. + * The exception is while Ava is still working: then the live trace IS the + * page, because there is nothing else to show yet and watching her query is + * the demo. + * + * POLLING is on while she owns the case (Investigating / Monitoring); when it + * is with a person nothing moves, so the poll stops. */ export function CaseScreen() { const { id } = useParams(); const { client, user } = useZino(); const [form, setForm] = useState(null); + const [showWork, setShowWork] = useState(false); const cases = useQuery( () => @@ -55,6 +58,7 @@ export function CaseScreen() { const flagged = num(row?.flagged_count); const kind = str(row?.finding_kind); const quiet = flagged === 0 && !str(row?.summary); + const hasFindings = !!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); @@ -71,120 +75,143 @@ export function CaseScreen() { if (cases.error) return {cases.error}; if (!row) return Case {id} not found.; + const work = ; + return ( -
- - Back to the queue - +
+
+ + Back + + Case #{str(row.instance_id)} + + {quiet ? 'Nothing to report' : state} + + {kind && FINDING_KIND[kind] && {FINDING_KIND[kind]}} + + Daily review · {dateTime(row.created_at)} + {str(row.review_as_of) ? ` · as of ${str(row.review_as_of).slice(0, 10)}` : ''} + +
- {/* ---------------- hero: who holds it, and what is asked ------------- */} -
-
-
-
-

Case #{str(row.instance_id)}

- - {quiet ? 'Nothing to report' : state} - - {kind && FINDING_KIND[kind] && {FINDING_KIND[kind]}} - {aiOwned ? ( - - Ava is working - - ) : ( - - waiting on a person - - )} -
-

- 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. + + + {/* ---------- Ava still working: the trace is the page ---------- */} + {aiOwned && !hasFindings && ( + <> +

+

+ Ava is reviewing

-
+

+ Every payer, department and front desk — checking whether anything is materially off. +

+

+ Takes about four minutes. If nothing clears the bar, she closes this case herself. +

+
+ {work} + + )} - - opened {dateTime(row.created_at)} - -
- - - - {/* The actions. Ava's own activities are deliberately absent — see the - note on STAGE_ACTIONS in config.ts. */} - {actions.length > 0 && ( -
- {actions.map((a) => ( -
- - {a.hint &&

{a.hint}

} -
- ))} -
- )} - - {state === NEEDS_HUMAN && ( -

- Ava stopped and asked for you.{' '} - {str(row.escalation_reason) || 'No reason recorded.'} + {/* ---------- A quiet morning ---------- */} + {quiet && ( +

+

+ Reviewed — nothing to report

- )} -
- - {/* ---------------- 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}

- )} +

+ Nothing needs your attention this morning. +

+ {scanned &&

{scanned}

}
)} - {/* ---------------- her conclusions ---------------- */} - - - + {/* ---------- Needs a human ---------- */} + {state === NEEDS_HUMAN && ( +
+

Ava stopped and asked for you

+

+ {str(row.escalation_reason) || 'No reason recorded.'} +

+
+ )} - {/* ---------------- her work, live ---------------- */} - + {/* ---------- The findings: what happened, how much, what to approve ---------- */} + {hasFindings && ( + <> + + + + + )} - {/* ---------------- the ask, in full ---------------- */} - -
- -
-
+ {/* ---------- The decision ---------- */} + {actions.length > 0 && ( +
+

Your decision

+
+ {actions.map((a) => ( +
+ + {a.hint &&

{a.hint}

} +
+ ))} +
+
+ )} + + {/* ---------- For the reader who wants to check ---------- */} + {hasFindings && ( + <> + + + {!aiOwned && ( + setShowWork(!showWork)} + icon={} + title="Watch how she did it" + hint="Her every step, from the platform's own trace — read the case, think, query, think, decide" + tone="ai" + > +
{work}
+
+ )} + {aiOwned && work} + + )} + {quiet && ( + <> + + setShowWork(!showWork)} + icon={} + title="Watch how she did it" + hint="Her every step — the proof that a quiet morning was checked, not skipped" + tone="ai" + > +
{work}
+
+ + )} + + {/* ---------- Provenance ---------- */} +
+ +
{form && ( )} @@ -217,7 +237,7 @@ function Rail({ state }: { state: string }) { if (state === NEEDS_HUMAN) return null; const at = stageStep(state); return ( -
    +
      {STAGES.map((s, i) => { const done = at > i; const here = at === i; @@ -225,11 +245,7 @@ function Rail({ state }: { state: string }) {
    1. {s} @@ -245,8 +261,6 @@ function Rail({ state }: { state: string }) { function defaultApprovalNote(row: Record): string { const recoverable = row.recoverable_amount; return recoverable - ? `Approved. Proceed with the appeals and the process fix as recommended; expected recovery $${Number( - recoverable, - ).toLocaleString('en-US')}. Re-check when the timer fires.` + ? `Approved. Proceed with the appeals and the process fix as recommended; expected recovery $${Number(recoverable).toLocaleString('en-US')}. Re-check when the timer fires.` : 'Approved. Proceed as recommended and re-check when the timer fires.'; } diff --git a/src/screens/QueueScreen.tsx b/src/screens/QueueScreen.tsx index 3f4dc9b..5a59b5f 100644 --- a/src/screens/QueueScreen.tsx +++ b/src/screens/QueueScreen.tsx @@ -1,18 +1,21 @@ import { useNavigate, useParams } from 'react-router-dom'; -import { Brain, Clock, User } from 'lucide-react'; +import { Brain, ChevronRight, Radar } from 'lucide-react'; import { useQuery, useZino } from '../api/provider'; import { FINDING_KIND, QUEUES, RECORD_VIEW, STAGE_TONE } from '../api/config'; -import { Badge, Card, Empty, ErrorNote, Spinner } from '../components/core'; +import { Badge, Button, Empty, ErrorNote, Spinner } from '../components/core'; import type { Row } from '../api/types'; -import { dateTime, money, num, str } from '../format'; +import { dateTime, headline, money, num, str } from '../format'; /** - * One queue — the record view filtered on `current_state_name`. + * A queue, as a morning briefing. * - * POLLS, and that matters more here than in most consoles: a case moves - * Investigating → Awaiting Review on its own, driven by an AI employee with no - * user action to hang a refresh off. A stale list would look like a stuck - * system, and on stage it would look like nothing happened. + * The first thing on the page is not a list: it is one strip that says what + * Ava did and what is waiting — "reviewed everything, one thing needs you, + * worth $1.09M". A manager who reads only that line has the day's status. + * The cases follow, each led by its one-sentence verdict and the money. + * + * POLLS, because cases move on their own — Investigating → Awaiting Review with + * no user action to hang a refresh off. A stale list looks like a stuck system. */ export function QueueScreen() { const { q } = useParams(); @@ -26,22 +29,24 @@ export function QueueScreen() { limit: 100, sortBy: 'updated_at', sortDir: 'desc', - filters: queue.stage - ? [{ field_key: 'current_state_name', value: queue.stage, data_type: 'text' }] - : [], + filters: queue.stage ? [{ field_key: 'current_state_name', value: queue.stage, data_type: 'text' }] : [], }), [queue.path], true, 8000, ); + // The briefing needs the whole picture, whichever queue is open. + const all = useQuery(() => client.rows(RECORD_VIEW, { limit: 100, sortBy: 'updated_at', sortDir: 'desc' }), [], true, 15000); const list = rows.data ?? []; return ( -
      -
      +
      + navigate('/review')} /> + +
      -

      {queue.label}

      +

      {queue.label}

      {queue.hint}

      {!rows.loading && {list.length} case{list.length === 1 ? '' : 's'}} @@ -49,15 +54,11 @@ export function QueueScreen() { {rows.error && {rows.error}} {rows.loading && !rows.data && } - {rows.data && list.length === 0 && ( - - Nothing in this queue.{' '} - {queue.stage === 'Awaiting Review' && 'Ava puts her findings here when she is done.'} - + Nothing here.{queue.stage === 'Awaiting Review' && ' When Ava finds something worth your time, it lands here.'} )} -
      +
      {list.map((r) => ( navigate(`/c/${str(r.instance_id)}`)} /> ))} @@ -66,72 +67,97 @@ export function QueueScreen() { ); } -/** - * A case as a card rather than a table row. - * - * A denial investigation is not a record with twelve equal columns — it is one - * sentence (what Ava found) and two numbers (what it is worth). A table would - * truncate the sentence, which is the only part anyone reads. - */ +/** The one strip a manager reads. */ +function Briefing({ rows, onRun }: { rows: Row[]; onRun: () => void }) { + const byState = (s: string) => rows.filter((r) => str(r.current_state_name) === s); + const waiting = byState('Awaiting Review'); + const working = [...byState('Investigating'), ...byState('Monitoring')]; + const needsHuman = byState('Needs Human'); + const recoverable = waiting.reduce((t, r) => t + (num(r.recoverable_amount) ?? 0), 0); + const latest = rows[0]; + + return ( +
      +
      + + + +
      +

      + This morning{latest ? ` · last review ${dateTime(latest.updated_at)}` : ''} +

      +

      + {waiting.length === 0 && working.length === 0 && needsHuman.length === 0 + ? 'Nothing needs your attention.' + : [ + waiting.length > 0 && + `${waiting.length} decision${waiting.length === 1 ? '' : 's'} waiting for you${ + recoverable > 0 ? ` — ${money(recoverable)} recoverable` : '' + }`, + working.length > 0 && `${working.length} case${working.length === 1 ? '' : 's'} Ava is working on`, + needsHuman.length > 0 && `${needsHuman.length} she could not finish`, + ] + .filter(Boolean) + .join(' · ')} +

      +

      + Ava reviews every payer, department and front desk each morning and only opens a case when something clears the bar. +

      +
      + +
      +
      + ); +} + +/** A case, led by its verdict and its money. */ function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) { const state = str(row.current_state_name); - const tone = STAGE_TONE[state] ?? 'slate'; const summary = str(row.summary); - const atRisk = num(row.amount_at_risk); - const recoverable = num(row.recoverable_amount); - 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 recoverable = num(row.recoverable_amount); + const aiOwned = state === 'Investigating' || state === 'Monitoring'; const quiet = flagged === 0 && !summary; + const waiting = state === 'Awaiting Review'; return ( - - - + +
      + {quiet && scanned &&

      {scanned}

      } + ); } diff --git a/src/styles.css b/src/styles.css index de1fd71..dae38c6 100644 --- a/src/styles.css +++ b/src/styles.css @@ -180,6 +180,7 @@ body { background: var(--color-bg); color: var(--color-ink); font-family: var(--font-sans); + font-size: 15px; -webkit-font-smoothing: antialiased; }