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; }