design: three questions, three sizes

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 <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-09-16 13:38:09 +05:30
parent 76e2eddd05
commit e294df6eba
7 changed files with 505 additions and 381 deletions

View File

@ -39,14 +39,14 @@ const TYPE_META: Record<
string, string,
{ label: string; icon: typeof Database; tone: 'violet' | 'sky' | 'slate' | 'emerald' } { label: string; icon: typeof Database; tone: 'violet' | 'sky' | 'slate' | 'emerald' }
> = { > = {
context: { label: 'Read the case', icon: FileSearch, tone: 'slate' }, context: { label: 'Read the case file', icon: FileSearch, tone: 'slate' },
tool: { label: 'Read the case', icon: FileSearch, tone: 'slate' }, tool: { label: 'Read the case file', icon: FileSearch, tone: 'slate' },
knowledge: { label: 'Checked her notes', 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' }, plan: { label: 'Planned', icon: Brain, tone: 'violet' },
api: { label: `Queried ${LAKEHOUSE.platform}`, icon: Database, tone: 'sky' }, api: { label: `Asked the data (${LAKEHOUSE.platform})`, icon: Database, tone: 'sky' },
decision: { label: 'Decided', icon: Gavel, tone: 'emerald' }, decision: { label: 'Wrote up her decision', icon: Gavel, tone: 'emerald' },
escalation: { label: 'Escalated', icon: AlertTriangle, tone: 'slate' }, escalation: { label: 'Asked for a person', icon: AlertTriangle, tone: 'slate' },
}; };
function meta(type: string) { function meta(type: string) {
@ -95,9 +95,9 @@ export function AvaLive({
<header className="flex flex-wrap items-center gap-3 border-b border-ai-edge bg-ai-soft px-4 py-3"> <header className="flex flex-wrap items-center gap-3 border-b border-ai-edge bg-ai-soft px-4 py-3">
<Brain className="h-4 w-4 text-ai" /> <Brain className="h-4 w-4 text-ai" />
<div className="min-w-0"> <div className="min-w-0">
<h2 className="text-sm font-semibold text-ai">Ava's working</h2> <h2 className="text-sm font-semibold text-ai">How Ava is working this out</h2>
<p className="mt-0.5 text-xs text-ai/75"> <p className="mt-0.5 text-xs text-ai/75">
Every step, from the platform's own trace — not a summary Every step she takes, recorded by the platform as she takes it
</p> </p>
</div> </div>
<div className="ml-auto flex flex-wrap items-center gap-2"> <div className="ml-auto flex flex-wrap items-center gap-2">
@ -117,7 +117,7 @@ export function AvaLive({
<p className="flex items-center gap-2 px-4 py-8 text-sm text-faint"> <p className="flex items-center gap-2 px-4 py-8 text-sm text-faint">
{working && <Loader2 className="h-4 w-4 animate-spin text-ai" />} {working && <Loader2 className="h-4 w-4 animate-spin text-ai" />}
{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.'} : 'No steps recorded for this case.'}
</p> </p>
) : ( ) : (
@ -209,7 +209,7 @@ export function AvaLive({
</span> </span>
<span className="ml-auto inline-flex items-center gap-1"> <span className="ml-auto inline-flex items-center gap-1">
<CheckCircle2 className="h-3.5 w-3.5" /> <CheckCircle2 className="h-3.5 w-3.5" />
Every statement wrapped in a SELECT — she cannot write She can only read — nothing she does can change the data
</span> </span>
</footer> </footer>
</section> </section>

View File

@ -1,175 +1,183 @@
import { AlertTriangle, CheckCircle2, ChevronDown, Target } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { AlertTriangle, CheckCircle2, ChevronDown, FileSearch } from 'lucide-react';
import { Badge, Card, Facts } from './core'; import { Badge, Card, Facts } from './core';
import type { Row } from '../api/types'; 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 * THREE QUESTIONS, THREE SIZES. What happened — one sentence, large. How much
* or inferred from anything — each field is read off the instance exactly as * — three numbers you can read from across a table. What to do — the ranked
* she submitted it. The violet tint is the console's one semantic rule: if it * list, with the decision buttons rendered right under it by the case screen.
* is violet, an agent produced it. * 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 * EVERY VALUE IS HERS. Nothing here is computed by the frontend — each field is
* manager approves a LIST of actions with owners and amounts; rendering it as * read off the instance as she submitted it. Violet marks what an agent
* prose invites him to skim it as commentary. * 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 summary = str(row.summary);
const cause = str(row.root_cause); if (!summary) return null;
const secondary = str(row.secondary_findings); const line = headline(summary);
const recs = recommendations(row.recommendations);
const confidence = num(row.confidence);
const reconciled = row.data_reconciled === true || str(row.data_reconciled) === 'true';
const atRisk = num(row.amount_at_risk); const atRisk = num(row.amount_at_risk);
const recoverable = num(row.recoverable_amount); const recoverable = num(row.recoverable_amount);
const affected = num(row.affected_claims); const affected = num(row.affected_claims);
const deadline = firstDeadline(row.recommendations);
if (!summary) return null; const confidence = num(row.confidence);
const reconciled = row.data_reconciled === true || str(row.data_reconciled) === 'true';
return ( return (
<div className="space-y-4"> <section className="lift rounded-xl border border-ai-edge bg-panel">
{/* --------------- the numbers, before the prose --------------- */} <div className="border-b border-ai-edge bg-ai-soft px-5 py-4">
<div className="grid gap-3 sm:grid-cols-3"> <p className="text-[11px] font-semibold tracking-wide text-ai uppercase">What Ava found</p>
<Stat label="Claims affected" value={affected == null ? '—' : String(affected)} /> <p className="mt-1.5 text-[21px] leading-snug font-semibold text-ink">{line}</p>
<Stat label="Billed at risk" value={money(atRisk)} tone="bad" /> <div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted">
<Stat <span className="inline-flex items-center gap-1">
label="Recoverable" <CheckCircle2 className={`h-3.5 w-3.5 ${reconciled ? 'text-good' : 'text-warn'}`} />
{reconciled ? 'Numbers checked against Finance' : 'Numbers NOT yet checked against Finance'}
</span>
{confidence != null && (
<span>
Ava is <span className="font-semibold text-ink">{pct(confidence * 100, 0)}</span> sure
</span>
)}
</div>
</div>
<div className="grid gap-px bg-line sm:grid-cols-3">
<Big label="Claims denied" value={affected == null ? '—' : affected.toLocaleString('en-US')} />
<Big label="Money at stake" value={money(atRisk)} tone="bad" />
<Big
label="Still recoverable"
value={money(recoverable)} value={money(recoverable)}
tone="good" tone="good"
note={ note={
atRisk && recoverable deadline
? `${Math.round((recoverable / atRisk) * 100)}% of what is at risk` ? `if appeals are filed — first deadline ${shortDate(deadline)}`
: undefined : atRisk && recoverable
? `${Math.round((recoverable / atRisk) * 100)}% of what is at stake`
: undefined
} }
/> />
</div> </div>
</section>
{/* --------------- what she found --------------- */}
<section className="lift rounded-lg border border-ai-edge bg-panel">
<header className="flex flex-wrap items-center gap-2 border-b border-ai-edge bg-ai-soft px-4 py-3">
<Target className="h-4 w-4 text-ai" />
<h2 className="text-sm font-semibold text-ai">What Ava found</h2>
<div className="ml-auto flex items-center gap-2">
{/* 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. */}
<Badge tone={reconciled ? 'emerald' : 'amber'}>
{reconciled ? 'data reconciled' : 'data NOT reconciled'}
</Badge>
{confidence != null && (
<Badge tone={confidence >= 0.9 ? 'emerald' : confidence >= 0.7 ? 'amber' : 'rose'}>
{pct(confidence * 100, 0)} confident
</Badge>
)}
</div>
</header>
<div className="space-y-4 px-4 py-4">
<p className="text-sm leading-relaxed whitespace-pre-wrap text-ink">{summary}</p>
{cause && (
<div className="rounded-md border border-line bg-panel2 px-3.5 py-3">
<p className="text-[11px] font-semibold tracking-wide text-faint uppercase">
Root cause
</p>
<p className="mt-1 text-sm leading-relaxed whitespace-pre-wrap text-ink">{cause}</p>
</div>
)}
{secondary && !/^none found/i.test(secondary.trim()) && (
<div className="rounded-md border border-warn-edge bg-warn-soft px-3.5 py-3">
<p className="inline-flex items-center gap-1.5 text-[11px] font-semibold tracking-wide text-warn uppercase">
<AlertTriangle className="h-3.5 w-3.5" /> A second, separate pattern
</p>
<p className="mt-1 text-sm leading-relaxed whitespace-pre-wrap text-ink">{secondary}</p>
</div>
)}
</div>
</section>
{/* --------------- what she wants done --------------- */}
{recs.length > 0 && (
<Card title="What she recommends" subtitle="Ranked by money. You approve or send it back.">
<ol className="divide-y divide-line">
{recs.map((block, i) => {
const { title, body } = splitRecommendation(block);
return (
<li key={i} className="px-4 py-3.5">
<div className="flex items-start gap-3">
<span className="tnum mt-0.5 shrink-0 rounded-full bg-panel2 px-2 py-0.5 text-[11px] font-semibold text-muted ring-1 ring-line ring-inset">
{i + 1}
</span>
<div className="min-w-0">
{title && <p className="text-[13px] font-semibold text-ink">{title}</p>}
<p className="mt-0.5 text-sm leading-relaxed whitespace-pre-wrap text-muted">
{body}
</p>
</div>
</div>
</li>
);
})}
</ol>
</Card>
)}
</div>
); );
} }
/** /** The decision list. Rendered directly above the Approve / Send back / Reject buttons. */
* Her evidence: every statement she ran, in order, with her own note on what export function Recommendations({ row }: { row: Row }) {
* came back. const recs = recommendations(row.recommendations);
* if (recs.length === 0) return null;
* Collapsed by default. It is the proof, not the story — a reviewer opens it to return (
* check a number, and a sceptic in the room opens it to see that the SQL is <section className="lift rounded-xl border border-line bg-panel">
* real. Both are better served by a closed section than by a wall of query text <header className="border-b border-line px-5 py-3.5">
* above the recommendations. <h2 className="text-base font-semibold text-ink">What she wants you to approve</h2>
*/ <p className="mt-0.5 text-xs text-muted">Ranked by money. Each has an owner.</p>
</header>
<ol className="divide-y divide-line">
{recs.map((block, i) => {
const { title, body } = splitRecommendation(block);
return (
<li key={i} className="flex items-start gap-4 px-5 py-4">
<span className="tnum mt-0.5 shrink-0 rounded-full bg-brand px-2.5 py-0.5 text-xs font-bold text-brand-fg">
{i + 1}
</span>
<div className="min-w-0">
{title && <p className="text-[15px] font-semibold text-ink">{sentenceCase(title)}</p>}
<p className="mt-1 text-sm leading-relaxed whitespace-pre-wrap text-muted">{body}</p>
</div>
</li>
);
})}
</ol>
</section>
);
}
/** 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 (
<Disclosure
open={open}
onToggle={() => setOpen(!open)}
icon={<FileSearch className="h-4 w-4 text-ai" />}
title="The full story"
hint={hasSecondary ? 'Analysis, root cause, and a second pattern she noticed' : 'Her full analysis and the root cause'}
tone="ai"
>
<div className="space-y-4 px-5 py-4">
<p className="text-sm leading-relaxed whitespace-pre-wrap text-ink">{summary}</p>
{cause && (
<div className="rounded-md border border-line bg-panel2 px-4 py-3">
<p className="text-[11px] font-semibold tracking-wide text-faint uppercase">Why it happened</p>
<p className="mt-1 text-sm leading-relaxed whitespace-pre-wrap text-ink">{cause}</p>
</div>
)}
{hasSecondary && (
<div className="rounded-md border border-warn-edge bg-warn-soft px-4 py-3">
<p className="inline-flex items-center gap-1.5 text-[11px] font-semibold tracking-wide text-warn uppercase">
<AlertTriangle className="h-3.5 w-3.5" /> A second, separate problem
</p>
<p className="mt-1 text-sm leading-relaxed whitespace-pre-wrap text-ink">{secondary}</p>
</div>
)}
</div>
</Disclosure>
);
}
/** Every SQL statement she ran. For the sceptic in the room. Collapsed. */
export function SqlEvidence({ row }: { row: Row }) { export function SqlEvidence({ row }: { row: Row }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const steps = sqlSteps(row.sql_log); const steps = sqlSteps(row.sql_log);
if (steps.length === 0) return null; if (steps.length === 0) return null;
return ( return (
<section className="lift overflow-hidden rounded-lg border border-line bg-panel"> <Disclosure
<button open={open}
onClick={() => setOpen(!open)} onToggle={() => setOpen(!open)}
className="flex w-full items-center gap-2 px-4 py-3 text-left focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-brand" icon={<CheckCircle2 className="h-4 w-4 text-good" />}
> title="Her evidence"
<CheckCircle2 className="h-4 w-4 text-good" /> hint={`${steps.length} database queries, exactly as she ran them — anyone can re-run these`}
<span className="text-sm font-semibold text-ink">Her evidence</span> >
<span className="text-xs text-faint"> <ol className="divide-y divide-line">
{steps.length} statement{steps.length === 1 ? '' : 's'}, exactly as she ran them {steps.map((s, i) => (
</span> <li key={i} className="px-5 py-3">
<ChevronDown {s.label && (
className={`ml-auto h-4 w-4 text-faint transition-transform ${open ? 'rotate-180' : ''}`} <p className="text-[11px] font-semibold tracking-wide text-faint uppercase">{s.label}</p>
/> )}
</button> <pre className="sql mt-1 text-muted">{s.sql}</pre>
{s.note && <p className="mt-1.5 text-xs text-ink">{s.note}</p>}
{open && ( </li>
<ol className="divide-y divide-line border-t border-line"> ))}
{steps.map((s, i) => ( </ol>
<li key={i} className="px-4 py-3"> </Disclosure>
{s.label && (
<p className="text-[11px] font-semibold tracking-wide text-faint uppercase">
{s.label}
</p>
)}
<pre className="sql mt-1 text-muted">{s.sql}</pre>
{s.note && <p className="mt-1.5 text-xs text-ink">{s.note}</p>}
</li>
))}
</ol>
)}
</section>
); );
} }
/** The re-check, when there is one. Same shape both ways: a rate against its /** The re-check. Same shape either way, so "fixed" and "not fixed" read alike. */
* baseline, so "recovered" and "still elevated" are read the same way. */
export function Recheck({ row }: { row: Row }) { export function Recheck({ row }: { row: Row }) {
const summary = str(row.recheck_summary); const summary = str(row.recheck_summary);
if (!summary) return null; if (!summary) return null;
@ -180,17 +188,17 @@ export function Recheck({ row }: { row: Row }) {
return ( return (
<Card <Card
title="The re-check" title={better ? 'The fix worked' : 'The fix has not landed yet'}
subtitle="Ava came back on her own after the actions were approved" subtitle="Ava came back on her own to check"
actions={<Badge tone={better ? 'emerald' : 'amber'}>{better ? 'back to baseline' : 'still elevated'}</Badge>} actions={<Badge tone={better ? 'emerald' : 'amber'}>{better ? 'back to normal' : 'still elevated'}</Badge>}
> >
<div className="space-y-3 px-4 py-4"> <div className="space-y-3 px-5 py-4">
<Facts <Facts
cols={3} cols={3}
rows={[ rows={[
{ label: 'Rate now', value: pct(current), tone: better ? 'emerald' : 'rose' }, { label: 'Denial rate now', value: pct(current), tone: better ? 'emerald' : 'rose' },
{ label: 'Baseline', value: pct(baseline) }, { label: 'Normal is', value: pct(baseline) },
{ label: 'Recovered', value: recovered == null ? '—' : money(recovered), tone: 'emerald' }, { label: 'Recovered so far', value: recovered == null ? '—' : money(recovered), tone: 'emerald' },
]} ]}
/> />
<p className="text-sm leading-relaxed whitespace-pre-wrap text-muted">{summary}</p> <p className="text-sm leading-relaxed whitespace-pre-wrap text-muted">{summary}</p>
@ -199,23 +207,62 @@ export function Recheck({ row }: { row: Row }) {
); );
} }
function Stat({ // ---------------------------------------------------------------------------
label,
value, function Big({ label, value, tone, note }: { label: string; value: string; tone?: 'good' | 'bad'; note?: string }) {
tone,
note,
}: {
label: string;
value: string;
tone?: 'good' | 'bad';
note?: string;
}) {
const text = tone === 'good' ? 'text-good' : tone === 'bad' ? 'text-bad' : 'text-ink'; const text = tone === 'good' ? 'text-good' : tone === 'bad' ? 'text-bad' : 'text-ink';
return ( return (
<div className="lift rounded-lg border border-line bg-panel px-4 py-3"> <div className="bg-panel px-5 py-4">
<p className="text-[11px] font-semibold tracking-wide text-faint uppercase">{label}</p> <p className="text-[11px] font-semibold tracking-wide text-faint uppercase">{label}</p>
<p className={`tnum mt-1 text-xl font-bold ${text}`}>{value}</p> <p className={`tnum mt-1 text-[28px] leading-none font-bold ${text}`}>{value}</p>
{note && <p className="mt-0.5 text-[11px] text-faint">{note}</p>} {note && <p className="mt-1.5 text-xs text-muted">{note}</p>}
</div> </div>
); );
} }
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 (
<section className={`lift overflow-hidden rounded-xl border bg-panel ${tone === 'ai' ? 'border-ai-edge' : 'border-line'}`}>
<button
onClick={onToggle}
className="flex w-full items-center gap-3 px-5 py-3.5 text-left focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-brand"
>
{icon}
<span className="min-w-0">
<span className="block text-sm font-semibold text-ink">{title}</span>
{hint && <span className="block text-xs text-faint">{hint}</span>}
</span>
<ChevronDown className={`ml-auto h-4 w-4 shrink-0 text-faint transition-transform ${open ? 'rotate-180' : ''}`} />
</button>
{open && <div className="border-t border-line">{children}</div>}
</section>
);
}
/** "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()),
);
}

View File

@ -121,3 +121,39 @@ export function sqlSteps(v: unknown): Array<{ label: string; sql: string; note:
}) })
.filter((s) => s.sql); .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' });
}

View File

@ -93,7 +93,7 @@ export function Shell() {
</ul> </ul>
</nav> </nav>
<main className="min-w-0 flex-1 p-4 lg:p-6"> <main className="min-w-0 flex-1 p-4 lg:p-8">
<Outlet /> <Outlet />
</main> </main>
</div> </div>

View File

@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { Link, useParams } from 'react-router-dom'; 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 { useQuery, useZino } from '../api/provider';
import { import {
ACT, ACT,
@ -12,31 +12,34 @@ import {
actionsFor, actionsFor,
stageStep, stageStep,
} from '../api/config'; } 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 { ActivityForm } from '../components/ActivityForm';
import { AvaLive } from '../components/AvaLive'; 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 type { Action } from '../api/config';
import { dateTime, num, str } from '../format'; 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 * ORDER IS THE DESIGN. What happened (one sentence, large) → how much (three
* shaped: what Ava concluded first, then what she wants done, then her * numbers) → what she wants approved → the buttons. That is the whole
* evidence, then the raw trace of how she got there. The actions sit at the top * above-the-fold. The full analysis, her evidence and the live trace sit
* because on a case in Awaiting Review, deciding is the only reason this screen * behind disclosures underneath, for the reviewer who wants to check and the
* is open. * sceptic who wants to see the SQL.
* *
* POLLING is on while Ava owns the case (Investigating / Monitoring). That is * The exception is while Ava is still working: then the live trace IS the
* the whole live demo: the trace grows, query by query, with nobody touching * page, because there is nothing else to show yet and watching her query is
* the keyboard. When the case is with a person nothing is moving, so the poll * the demo.
* stops rather than hammering the gateway for a screen that cannot change. *
* 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() { export function CaseScreen() {
const { id } = useParams(); const { id } = useParams();
const { client, user } = useZino(); const { client, user } = useZino();
const [form, setForm] = useState<Action | null>(null); const [form, setForm] = useState<Action | null>(null);
const [showWork, setShowWork] = useState(false);
const cases = useQuery( const cases = useQuery(
() => () =>
@ -55,6 +58,7 @@ export function CaseScreen() {
const flagged = num(row?.flagged_count); const flagged = num(row?.flagged_count);
const kind = str(row?.finding_kind); const kind = str(row?.finding_kind);
const quiet = flagged === 0 && !str(row?.summary); 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 trace = useQuery(() => client.avaTrace(String(id)), [id], true, aiOwned ? 4000 : 0);
const decisions = useQuery(() => client.avaDecisions(String(id)), [id], true, aiOwned ? 8000 : 0); const decisions = useQuery(() => client.avaDecisions(String(id)), [id], true, aiOwned ? 8000 : 0);
@ -71,120 +75,143 @@ export function CaseScreen() {
if (cases.error) return <ErrorNote>{cases.error}</ErrorNote>; if (cases.error) return <ErrorNote>{cases.error}</ErrorNote>;
if (!row) return <ErrorNote>Case {id} not found.</ErrorNote>; if (!row) return <ErrorNote>Case {id} not found.</ErrorNote>;
const work = <AvaLive steps={trace.data ?? []} decisions={decisions.data ?? []} working={aiOwned} />;
return ( return (
<div className="space-y-4"> <div className="mx-auto max-w-4xl space-y-4">
<Link <div className="flex flex-wrap items-center gap-3">
to="/q/review" <Link to="/q/review" className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-ink">
className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-ink" <ArrowLeft className="h-4 w-4" /> Back
> </Link>
<ArrowLeft className="h-4 w-4" /> Back to the queue <span className="tnum text-sm text-faint">Case #{str(row.instance_id)}</span>
</Link> <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>}
<span className="ml-auto text-[11px] text-faint">
Daily review · {dateTime(row.created_at)}
{str(row.review_as_of) ? ` · as of ${str(row.review_as_of).slice(0, 10)}` : ''}
</span>
</div>
{/* ---------------- hero: who holds it, and what is asked ------------- */} <Rail state={state} />
<section className="lift rounded-lg border border-line bg-panel px-4 py-4">
<div className="flex flex-wrap items-start gap-3"> {/* ---------- Ava still working: the trace is the page ---------- */}
<div className="min-w-0"> {aiOwned && !hasFindings && (
<div className="flex flex-wrap items-center gap-2"> <>
<h1 className="tnum text-lg font-semibold text-ink">Case #{str(row.instance_id)}</h1> <section className="lift rounded-xl border border-ai-edge bg-ai-soft px-5 py-4">
<Badge tone={quiet ? 'slate' : (STAGE_TONE[state] ?? 'slate')} dot={aiOwned}> <p className="inline-flex items-center gap-2 text-[11px] font-semibold tracking-wide text-ai uppercase">
{quiet ? 'Nothing to report' : state} <Brain className="h-4 w-4" /> Ava is reviewing
</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
</span>
) : (
<span className="inline-flex items-center gap-1 text-xs text-faint">
<User className="h-3.5 w-3.5" /> waiting on a person
</span>
)}
</div>
<p className="mt-1.5 max-w-2xl text-sm text-muted">
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.
</p> </p>
</div> <p className="mt-1.5 text-[19px] leading-snug font-semibold text-ink">
Every payer, department and front desk — checking whether anything is materially off.
</p>
<p className="mt-1 text-sm text-muted">
Takes about four minutes. If nothing clears the bar, she closes this case herself.
</p>
</section>
{work}
</>
)}
<span className="ml-auto inline-flex items-center gap-1 text-[11px] text-faint"> {/* ---------- A quiet morning ---------- */}
<Clock className="h-3 w-3" /> opened {dateTime(row.created_at)} {quiet && (
</span> <section className="lift rounded-xl border border-line bg-panel px-5 py-5">
</div> <p className="inline-flex items-center gap-2 text-[11px] font-semibold tracking-wide text-faint uppercase">
<Radar className="h-4 w-4" /> Reviewed — nothing to report
<Rail state={state} />
{/* The actions. Ava's own activities are deliberately absent — see the
note on STAGE_ACTIONS in config.ts. */}
{actions.length > 0 && (
<div className="mt-4 flex flex-wrap items-start gap-3 border-t border-line pt-4">
{actions.map((a) => (
<div key={a.activity}>
<Button kind={a.kind} onClick={() => setForm(a)}>
{a.label}
</Button>
{a.hint && <p className="mt-1 max-w-[16rem] text-[11px] text-faint">{a.hint}</p>}
</div>
))}
</div>
)}
{state === NEEDS_HUMAN && (
<p className="mt-4 rounded-md border border-bad-edge bg-bad-soft px-3.5 py-2.5 text-sm text-bad">
<span className="font-semibold">Ava stopped and asked for you.</span>{' '}
{str(row.escalation_reason) || 'No reason recorded.'}
</p> </p>
)} <p className="mt-1.5 text-[21px] leading-snug font-semibold text-ink">
</section> Nothing needs your attention this morning.
</p>
{/* ---------------- what the sweep covered ---------------- */} {scanned && <p className="mt-3 text-sm leading-relaxed whitespace-pre-wrap text-muted">{scanned}</p>}
{(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> </section>
)} )}
{/* ---------------- her conclusions ---------------- */} {/* ---------- Needs a human ---------- */}
<Findings row={row} /> {state === NEEDS_HUMAN && (
<Recheck row={row} /> <section className="lift rounded-xl border border-bad-edge bg-bad-soft px-5 py-4">
<SqlEvidence row={row} /> <p className="text-[11px] font-semibold tracking-wide text-bad uppercase">Ava stopped and asked for you</p>
<p className="mt-1.5 text-[17px] leading-snug font-semibold text-ink">
{str(row.escalation_reason) || 'No reason recorded.'}
</p>
</section>
)}
{/* ---------------- her work, live ---------------- */} {/* ---------- The findings: what happened, how much, what to approve ---------- */}
<AvaLive steps={trace.data ?? []} decisions={decisions.data ?? []} working={aiOwned} /> {hasFindings && (
<>
<Verdict row={row} />
<Recheck row={row} />
<Recommendations row={row} />
</>
)}
{/* ---------------- the ask, in full ---------------- */} {/* ---------- The decision ---------- */}
<Card title="This review" subtitle="How the case came to exist"> {actions.length > 0 && (
<div className="px-4 py-4"> <section className="lift rounded-xl border border-warn-edge bg-warn-soft px-5 py-4">
<Facts <p className="text-[11px] font-semibold tracking-wide text-warn uppercase">Your decision</p>
rows={[ <div className="mt-3 flex flex-wrap items-start gap-3">
{ label: 'Started by', value: str(row.review_as_of) ? 'Replay of a past morning' : 'Daily review' }, {actions.map((a) => (
{ label: 'Anchored on', value: str(row.review_as_of) ? str(row.review_as_of).slice(0, 10) : 'Latest data' }, <div key={a.activity}>
{ label: 'Look-back', value: row.period_weeks ? `${str(row.period_weeks)} weeks` : '6 weeks' }, <Button kind={a.kind} onClick={() => setForm(a)} className="px-5 py-2.5 text-[15px]">
...(str(row.review_note) {a.label}
? [{ label: 'Sent back with', value: str(row.review_note), wide: true }] </Button>
: []), {a.hint && <p className="mt-1 max-w-[17rem] text-[11px] text-muted">{a.hint}</p>}
...(str(row.approval_note) </div>
? [{ label: 'Approved with', value: str(row.approval_note), wide: true }] ))}
: []), </div>
]} </section>
/> )}
</div>
</Card> {/* ---------- For the reader who wants to check ---------- */}
{hasFindings && (
<>
<FullStory row={row} />
<SqlEvidence row={row} />
{!aiOwned && (
<Disclosure
open={showWork}
onToggle={() => setShowWork(!showWork)}
icon={<Brain className="h-4 w-4 text-ai" />}
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"
>
<div className="p-3">{work}</div>
</Disclosure>
)}
{aiOwned && work}
</>
)}
{quiet && (
<>
<SqlEvidence row={row} />
<Disclosure
open={showWork}
onToggle={() => setShowWork(!showWork)}
icon={<Brain className="h-4 w-4 text-ai" />}
title="Watch how she did it"
hint="Her every step — the proof that a quiet morning was checked, not skipped"
tone="ai"
>
<div className="p-3">{work}</div>
</Disclosure>
</>
)}
{/* ---------- Provenance ---------- */}
<section className="rounded-xl border border-line bg-panel px-5 py-4">
<Facts
cols={3}
rows={[
{ label: 'Started by', value: str(row.review_as_of) ? 'Replay of a past morning' : 'Daily review' },
{ label: 'Anchored on', value: str(row.review_as_of) ? str(row.review_as_of).slice(0, 10) : 'Latest data' },
{ label: 'Look-back', value: row.period_weeks ? `${str(row.period_weeks)} weeks` : '6 weeks' },
...(str(row.review_note) ? [{ label: 'Sent back with', value: str(row.review_note), wide: true }] : []),
...(str(row.approval_note) ? [{ label: 'Approved with', value: str(row.approval_note), wide: true }] : []),
]}
/>
</section>
{form && ( {form && (
<ActivityForm <ActivityForm
@ -196,14 +223,7 @@ export function CaseScreen() {
subtitle={form.hint} subtitle={form.hint}
submitLabel={form.label} submitLabel={form.label}
danger={form.kind === 'danger'} danger={form.kind === 'danger'}
// The approval note opens with a draft grounded in her findings — the prefill={form.activity === ACT.APPROVE ? { approval_note: defaultApprovalNote(row) } : undefined}
// manager edits or replaces it. Typing out numbers already on screen
// turns a decision into data entry.
prefill={
form.activity === ACT.APPROVE
? { approval_note: defaultApprovalNote(row) }
: undefined
}
onDone={refresh} onDone={refresh}
/> />
)} )}
@ -217,7 +237,7 @@ function Rail({ state }: { state: string }) {
if (state === NEEDS_HUMAN) return null; if (state === NEEDS_HUMAN) return null;
const at = stageStep(state); const at = stageStep(state);
return ( return (
<ol className="mt-4 flex flex-wrap items-center gap-1.5"> <ol className="flex flex-wrap items-center gap-1.5">
{STAGES.map((s, i) => { {STAGES.map((s, i) => {
const done = at > i; const done = at > i;
const here = at === i; const here = at === i;
@ -225,11 +245,7 @@ function Rail({ state }: { state: string }) {
<li key={s} className="flex items-center gap-1.5"> <li key={s} className="flex items-center gap-1.5">
<span <span
className={`rounded-full px-2.5 py-1 text-[11px] font-semibold ${ className={`rounded-full px-2.5 py-1 text-[11px] font-semibold ${
here here ? 'bg-brand text-brand-fg' : done ? 'bg-good-soft text-good ring-1 ring-good-edge ring-inset' : 'bg-panel2 text-faint'
? 'bg-brand text-brand-fg'
: done
? 'bg-good-soft text-good ring-1 ring-good-edge ring-inset'
: 'bg-panel2 text-faint'
}`} }`}
> >
{s} {s}
@ -245,8 +261,6 @@ function Rail({ state }: { state: string }) {
function defaultApprovalNote(row: Record<string, unknown>): string { function defaultApprovalNote(row: Record<string, unknown>): string {
const recoverable = row.recoverable_amount; const recoverable = row.recoverable_amount;
return recoverable return recoverable
? `Approved. Proceed with the appeals and the process fix as recommended; expected recovery $${Number( ? `Approved. Proceed with the appeals and the process fix as recommended; expected recovery $${Number(recoverable).toLocaleString('en-US')}. Re-check when the timer fires.`
recoverable,
).toLocaleString('en-US')}. Re-check when the timer fires.`
: 'Approved. Proceed as recommended and re-check when the timer fires.'; : 'Approved. Proceed as recommended and re-check when the timer fires.';
} }

View File

@ -1,18 +1,21 @@
import { useNavigate, useParams } from 'react-router-dom'; 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 { useQuery, useZino } from '../api/provider';
import { FINDING_KIND, 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 { Badge, Button, Empty, ErrorNote, Spinner } from '../components/core';
import type { Row } from '../api/types'; 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 * The first thing on the page is not a list: it is one strip that says what
* Investigating → Awaiting Review on its own, driven by an AI employee with no * Ava did and what is waiting — "reviewed everything, one thing needs you,
* user action to hang a refresh off. A stale list would look like a stuck * worth $1.09M". A manager who reads only that line has the day's status.
* system, and on stage it would look like nothing happened. * 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() { export function QueueScreen() {
const { q } = useParams(); const { q } = useParams();
@ -26,22 +29,24 @@ export function QueueScreen() {
limit: 100, limit: 100,
sortBy: 'updated_at', sortBy: 'updated_at',
sortDir: 'desc', sortDir: 'desc',
filters: queue.stage filters: queue.stage ? [{ field_key: 'current_state_name', value: queue.stage, data_type: 'text' }] : [],
? [{ field_key: 'current_state_name', value: queue.stage, data_type: 'text' }]
: [],
}), }),
[queue.path], [queue.path],
true, true,
8000, 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 ?? []; const list = rows.data ?? [];
return ( return (
<div className="space-y-4"> <div className="mx-auto max-w-4xl space-y-4">
<header className="flex flex-wrap items-end justify-between gap-3"> <Briefing rows={all.data ?? []} onRun={() => navigate('/review')} />
<header className="flex flex-wrap items-end justify-between gap-3 pt-2">
<div> <div>
<h1 className="text-xl font-semibold text-ink">{queue.label}</h1> <h1 className="text-lg font-semibold text-ink">{queue.label}</h1>
<p className="mt-0.5 text-sm text-muted">{queue.hint}</p> <p className="mt-0.5 text-sm text-muted">{queue.hint}</p>
</div> </div>
{!rows.loading && <Badge tone="slate">{list.length} case{list.length === 1 ? '' : 's'}</Badge>} {!rows.loading && <Badge tone="slate">{list.length} case{list.length === 1 ? '' : 's'}</Badge>}
@ -49,15 +54,11 @@ export function QueueScreen() {
{rows.error && <ErrorNote>{rows.error}</ErrorNote>} {rows.error && <ErrorNote>{rows.error}</ErrorNote>}
{rows.loading && !rows.data && <Spinner label="Loading cases…" />} {rows.loading && !rows.data && <Spinner label="Loading cases…" />}
{rows.data && list.length === 0 && ( {rows.data && list.length === 0 && (
<Empty> <Empty>Nothing here.{queue.stage === 'Awaiting Review' && ' When Ava finds something worth your time, it lands here.'}</Empty>
Nothing in this queue.{' '}
{queue.stage === 'Awaiting Review' && 'Ava puts her findings here when she is done.'}
</Empty>
)} )}
<div className="grid gap-3"> <div className="space-y-3">
{list.map((r) => ( {list.map((r) => (
<CaseCard key={str(r.instance_id)} row={r} onOpen={() => navigate(`/c/${str(r.instance_id)}`)} /> <CaseCard key={str(r.instance_id)} row={r} onOpen={() => navigate(`/c/${str(r.instance_id)}`)} />
))} ))}
@ -66,72 +67,97 @@ export function QueueScreen() {
); );
} }
/** /** The one strip a manager reads. */
* A case as a card rather than a table row. function Briefing({ rows, onRun }: { rows: Row[]; onRun: () => void }) {
* const byState = (s: string) => rows.filter((r) => str(r.current_state_name) === s);
* A denial investigation is not a record with twelve equal columns — it is one const waiting = byState('Awaiting Review');
* sentence (what Ava found) and two numbers (what it is worth). A table would const working = [...byState('Investigating'), ...byState('Monitoring')];
* truncate the sentence, which is the only part anyone reads. const needsHuman = byState('Needs Human');
*/ const recoverable = waiting.reduce((t, r) => t + (num(r.recoverable_amount) ?? 0), 0);
const latest = rows[0];
return (
<section className="lift rounded-xl border border-line bg-panel">
<div className="flex flex-wrap items-center gap-4 px-5 py-4">
<span className="rounded-full bg-ai-soft p-2.5 text-ai ring-1 ring-ai-edge ring-inset">
<Brain className="h-5 w-5" />
</span>
<div className="min-w-0 flex-1">
<p className="text-[11px] font-semibold tracking-wide text-faint uppercase">
This morning{latest ? ` · last review ${dateTime(latest.updated_at)}` : ''}
</p>
<p className="mt-0.5 text-[19px] leading-snug font-semibold text-ink">
{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(' · ')}
</p>
<p className="mt-0.5 text-xs text-muted">
Ava reviews every payer, department and front desk each morning and only opens a case when something clears the bar.
</p>
</div>
<Button kind="primary" onClick={onRun}>
<Radar className="h-4 w-4" /> Run the review now
</Button>
</div>
</section>
);
}
/** A case, led by its verdict and its money. */
function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) { function CaseCard({ row, onOpen }: { row: Row; onOpen: () => void }) {
const state = str(row.current_state_name); const state = str(row.current_state_name);
const tone = STAGE_TONE[state] ?? 'slate';
const summary = str(row.summary); 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 scanned = str(row.scanned_summary);
const flagged = num(row.flagged_count); const flagged = num(row.flagged_count);
const kind = str(row.finding_kind); 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 quiet = flagged === 0 && !summary;
const waiting = state === 'Awaiting Review';
return ( return (
<Card className="cursor-pointer transition-colors hover:border-brand/50" > <button
<button onClick={onOpen} className="w-full px-4 py-3.5 text-left"> onClick={onOpen}
<div className="flex flex-wrap items-center gap-2"> className={`lift block w-full rounded-xl border bg-panel px-5 py-4 text-left transition-colors hover:border-brand/60 focus-visible:outline focus-visible:outline-2 focus-visible:outline-brand ${
<span className="tnum text-[13px] font-semibold text-ink"> waiting ? 'border-warn-edge' : 'border-line'
Case #{str(row.instance_id)} }`}
</span> >
<Badge tone={quiet ? 'slate' : tone} dot={aiOwned}> <div className="flex flex-wrap items-center gap-2">
{quiet ? 'Nothing to report' : state} <Badge tone={quiet ? 'slate' : (STAGE_TONE[state] ?? 'slate')} dot={aiOwned}>
</Badge> {quiet ? 'Nothing to report' : waiting ? 'Needs your decision' : state}
{kind && FINDING_KIND[kind] && <Badge tone="violet">{FINDING_KIND[kind]}</Badge>} </Badge>
{aiOwned ? ( {kind && FINDING_KIND[kind] && <Badge tone="violet">{FINDING_KIND[kind]}</Badge>}
<span className="inline-flex items-center gap-1 text-[11px] text-ai"> <span className="ml-auto text-[11px] text-faint">
<Brain className="h-3 w-3" /> Ava #{str(row.instance_id)} · {dateTime(row.updated_at)}
</span> </span>
) : ( </div>
<span className="inline-flex items-center gap-1 text-[11px] text-faint">
<User className="h-3 w-3" /> with a person
</span>
)}
<span className="ml-auto inline-flex items-center gap-1 text-[11px] text-faint">
<Clock className="h-3 w-3" />
{dateTime(row.updated_at)}
</span>
</div>
<p className="mt-2 line-clamp-2 text-sm text-ink"> <div className="mt-2.5 flex items-start gap-4">
{summary || (quiet ? scanned : '') || 'Ava is reviewing every payer, department and desk…'} <p className="min-w-0 flex-1 text-[16px] leading-snug font-semibold text-ink">
{summary
? headline(summary, 160)
: quiet
? 'Reviewed everything — nothing needs attention.'
: 'Ava is reviewing every payer, department and front desk…'}
</p> </p>
{recoverable != null && recoverable > 0 && (
{(atRisk || recoverable) && ( <span className="shrink-0 text-right">
<div className="mt-3 flex flex-wrap gap-x-6 gap-y-1"> <span className="tnum block text-[22px] leading-none font-bold text-good">{money(recoverable)}</span>
{atRisk != null && ( <span className="block text-[11px] text-faint">recoverable</span>
<span className="text-xs text-faint"> </span>
At risk <span className="tnum font-semibold text-ink">{money(atRisk)}</span>
</span>
)}
{recoverable != null && (
<span className="text-xs text-faint">
Recoverable{' '}
<span className="tnum font-semibold text-good">{money(recoverable)}</span>
</span>
)}
</div>
)} )}
</button> <ChevronRight className="mt-1 h-4 w-4 shrink-0 text-faint" />
</Card> </div>
{quiet && scanned && <p className="mt-1.5 line-clamp-2 text-xs text-muted">{scanned}</p>}
</button>
); );
} }

View File

@ -180,6 +180,7 @@ body {
background: var(--color-bg); background: var(--color-bg);
color: var(--color-ink); color: var(--color-ink);
font-family: var(--font-sans); font-family: var(--font-sans);
font-size: 15px;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }