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,
{ 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({
<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" />
<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">
Every step, from the platform's own trace — not a summary
Every step she takes, recorded by the platform as she takes it
</p>
</div>
<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">
{working && <Loader2 className="h-4 w-4 animate-spin text-ai" />}
{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.'}
</p>
) : (
@ -209,7 +209,7 @@ export function AvaLive({
</span>
<span className="ml-auto inline-flex items-center gap-1">
<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>
</footer>
</section>

View File

@ -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 (
<div className="space-y-4">
{/* --------------- the numbers, before the prose --------------- */}
<div className="grid gap-3 sm:grid-cols-3">
<Stat label="Claims affected" value={affected == null ? '—' : String(affected)} />
<Stat label="Billed at risk" value={money(atRisk)} tone="bad" />
<Stat
label="Recoverable"
<section className="lift rounded-xl border border-ai-edge bg-panel">
<div className="border-b border-ai-edge bg-ai-soft px-5 py-4">
<p className="text-[11px] font-semibold tracking-wide text-ai uppercase">What Ava found</p>
<p className="mt-1.5 text-[21px] leading-snug font-semibold text-ink">{line}</p>
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted">
<span className="inline-flex items-center gap-1">
<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)}
tone="good"
note={
atRisk && recoverable
? `${Math.round((recoverable / atRisk) * 100)}% of what is at risk`
deadline
? `if appeals are filed — first deadline ${shortDate(deadline)}`
: atRisk && recoverable
? `${Math.round((recoverable / atRisk) * 100)}% of what is at stake`
: undefined
}
/>
</div>
{/* --------------- 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.">
/** 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 (
<section className="lift rounded-xl border border-line bg-panel">
<header className="border-b border-line px-5 py-3.5">
<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="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">
<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-[13px] font-semibold text-ink">{title}</p>}
<p className="mt-0.5 text-sm leading-relaxed whitespace-pre-wrap text-muted">
{body}
</p>
</div>
{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>
</Card>
)}
</div>
</section>
);
}
/**
* 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 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 }) {
const [open, setOpen] = useState(false);
const steps = sqlSteps(row.sql_log);
if (steps.length === 0) return null;
return (
<section className="lift overflow-hidden rounded-lg border border-line bg-panel">
<button
onClick={() => 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"
<Disclosure
open={open}
onToggle={() => setOpen(!open)}
icon={<CheckCircle2 className="h-4 w-4 text-good" />}
title="Her evidence"
hint={`${steps.length} database queries, exactly as she ran them — anyone can re-run these`}
>
<CheckCircle2 className="h-4 w-4 text-good" />
<span className="text-sm font-semibold text-ink">Her evidence</span>
<span className="text-xs text-faint">
{steps.length} statement{steps.length === 1 ? '' : 's'}, exactly as she ran them
</span>
<ChevronDown
className={`ml-auto h-4 w-4 text-faint transition-transform ${open ? 'rotate-180' : ''}`}
/>
</button>
{open && (
<ol className="divide-y divide-line border-t border-line">
<ol className="divide-y divide-line">
{steps.map((s, i) => (
<li key={i} className="px-4 py-3">
<li key={i} className="px-5 py-3">
{s.label && (
<p className="text-[11px] font-semibold tracking-wide text-faint uppercase">
{s.label}
</p>
<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>
</Disclosure>
);
}
/** 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 (
<Card
title="The re-check"
subtitle="Ava came back on her own after the actions were approved"
actions={<Badge tone={better ? 'emerald' : 'amber'}>{better ? 'back to baseline' : 'still elevated'}</Badge>}
title={better ? 'The fix worked' : 'The fix has not landed yet'}
subtitle="Ava came back on her own to check"
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
cols={3}
rows={[
{ label: 'Rate now', value: pct(current), tone: better ? 'emerald' : 'rose' },
{ label: 'Baseline', value: pct(baseline) },
{ label: 'Recovered', value: recovered == null ? '—' : money(recovered), tone: 'emerald' },
{ label: 'Denial rate now', value: pct(current), tone: better ? 'emerald' : 'rose' },
{ label: 'Normal is', value: pct(baseline) },
{ label: 'Recovered so far', value: recovered == null ? '—' : money(recovered), tone: 'emerald' },
]}
/>
<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,
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 (
<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={`tnum mt-1 text-xl font-bold ${text}`}>{value}</p>
{note && <p className="mt-0.5 text-[11px] text-faint">{note}</p>}
<p className={`tnum mt-1 text-[28px] leading-none font-bold ${text}`}>{value}</p>
{note && <p className="mt-1.5 text-xs text-muted">{note}</p>}
</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);
}
/**
* 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>
</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 />
</main>
</div>

View File

@ -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<Action | null>(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 <ErrorNote>{cases.error}</ErrorNote>;
if (!row) return <ErrorNote>Case {id} not found.</ErrorNote>;
return (
<div className="space-y-4">
<Link
to="/q/review"
className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-ink"
>
<ArrowLeft className="h-4 w-4" /> Back to the queue
</Link>
const work = <AvaLive steps={trace.data ?? []} decisions={decisions.data ?? []} working={aiOwned} />;
{/* ---------------- hero: who holds it, and what is asked ------------- */}
<section className="lift rounded-lg border border-line bg-panel px-4 py-4">
<div className="flex flex-wrap items-start gap-3">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<h1 className="tnum text-lg font-semibold text-ink">Case #{str(row.instance_id)}</h1>
return (
<div className="mx-auto max-w-4xl space-y-4">
<div className="flex flex-wrap items-center gap-3">
<Link to="/q/review" className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-ink">
<ArrowLeft className="h-4 w-4" /> Back
</Link>
<span className="tnum text-sm text-faint">Case #{str(row.instance_id)}</span>
<Badge tone={quiet ? 'slate' : (STAGE_TONE[state] ?? 'slate')} dot={aiOwned}>
{quiet ? 'Nothing to report' : state}
</Badge>
{kind && FINDING_KIND[kind] && <Badge tone="violet">{FINDING_KIND[kind]}</Badge>}
{aiOwned ? (
<span className="inline-flex items-center gap-1 text-xs text-ai">
<Brain className="h-3.5 w-3.5" /> Ava is working
</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>
</div>
<span className="ml-auto inline-flex items-center gap-1 text-[11px] text-faint">
<Clock className="h-3 w-3" /> opened {dateTime(row.created_at)}
<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>
<Rail state={state} />
{/* The actions. Ava's own activities are deliberately absent — see the
note on STAGE_ACTIONS in config.ts. */}
{/* ---------- Ava still working: the trace is the page ---------- */}
{aiOwned && !hasFindings && (
<>
<section className="lift rounded-xl border border-ai-edge bg-ai-soft px-5 py-4">
<p className="inline-flex items-center gap-2 text-[11px] font-semibold tracking-wide text-ai uppercase">
<Brain className="h-4 w-4" /> Ava is reviewing
</p>
<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}
</>
)}
{/* ---------- A quiet morning ---------- */}
{quiet && (
<section className="lift rounded-xl border border-line bg-panel px-5 py-5">
<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
</p>
<p className="mt-1.5 text-[21px] leading-snug font-semibold text-ink">
Nothing needs your attention this morning.
</p>
{scanned && <p className="mt-3 text-sm leading-relaxed whitespace-pre-wrap text-muted">{scanned}</p>}
</section>
)}
{/* ---------- Needs a human ---------- */}
{state === NEEDS_HUMAN && (
<section className="lift rounded-xl border border-bad-edge bg-bad-soft px-5 py-4">
<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>
)}
{/* ---------- The findings: what happened, how much, what to approve ---------- */}
{hasFindings && (
<>
<Verdict row={row} />
<Recheck row={row} />
<Recommendations row={row} />
</>
)}
{/* ---------- The decision ---------- */}
{actions.length > 0 && (
<div className="mt-4 flex flex-wrap items-start gap-3 border-t border-line pt-4">
<section className="lift rounded-xl border border-warn-edge bg-warn-soft px-5 py-4">
<p className="text-[11px] font-semibold tracking-wide text-warn uppercase">Your decision</p>
<div className="mt-3 flex flex-wrap items-start gap-3">
{actions.map((a) => (
<div key={a.activity}>
<Button kind={a.kind} onClick={() => setForm(a)}>
<Button kind={a.kind} onClick={() => setForm(a)} className="px-5 py-2.5 text-[15px]">
{a.label}
</Button>
{a.hint && <p className="mt-1 max-w-[16rem] text-[11px] text-faint">{a.hint}</p>}
{a.hint && <p className="mt-1 max-w-[17rem] text-[11px] text-muted">{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>
)}
</section>
{/* ---------------- what the sweep covered ---------------- */}
{(scanned || flagged != null) && (
<section
className={`lift rounded-lg border px-4 py-3.5 ${
quiet ? 'border-line bg-panel' : 'border-ai-edge bg-ai-soft'
}`}
>
<div className="flex flex-wrap items-center gap-2">
<Radar className={`h-4 w-4 ${quiet ? 'text-muted' : 'text-ai'}`} />
<h2 className={`text-sm font-semibold ${quiet ? 'text-ink' : 'text-ai'}`}>
{quiet ? 'Reviewed. Nothing to report.' : `Reviewed. Flagged ${flagged ?? 1}.`}
</h2>
<span className="ml-auto text-[11px] text-faint">
6 payers · 8 departments · 9 desks · every week in the data
</span>
</div>
{scanned && (
<p className="mt-2 text-sm leading-relaxed whitespace-pre-wrap text-muted">{scanned}</p>
)}
</section>
)}
{/* ---------------- her conclusions ---------------- */}
<Findings row={row} />
<Recheck row={row} />
{/* ---------- 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>
</>
)}
{/* ---------------- her work, live ---------------- */}
<AvaLive steps={trace.data ?? []} decisions={decisions.data ?? []} working={aiOwned} />
{/* ---------------- the ask, in full ---------------- */}
<Card title="This review" subtitle="How the case came to exist">
<div className="px-4 py-4">
{/* ---------- 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 }]
: []),
...(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 }] : []),
]}
/>
</div>
</Card>
</section>
{form && (
<ActivityForm
@ -196,14 +223,7 @@ export function CaseScreen() {
subtitle={form.hint}
submitLabel={form.label}
danger={form.kind === 'danger'}
// The approval note opens with a draft grounded in her findings — the
// 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
}
prefill={form.activity === ACT.APPROVE ? { approval_note: defaultApprovalNote(row) } : undefined}
onDone={refresh}
/>
)}
@ -217,7 +237,7 @@ function Rail({ state }: { state: string }) {
if (state === NEEDS_HUMAN) return null;
const at = stageStep(state);
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) => {
const done = 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">
<span
className={`rounded-full px-2.5 py-1 text-[11px] font-semibold ${
here
? 'bg-brand text-brand-fg'
: done
? 'bg-good-soft text-good ring-1 ring-good-edge ring-inset'
: 'bg-panel2 text-faint'
here ? 'bg-brand text-brand-fg' : done ? 'bg-good-soft text-good ring-1 ring-good-edge ring-inset' : 'bg-panel2 text-faint'
}`}
>
{s}
@ -245,8 +261,6 @@ function Rail({ state }: { state: string }) {
function defaultApprovalNote(row: Record<string, unknown>): 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.';
}

View File

@ -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 (
<div className="space-y-4">
<header className="flex flex-wrap items-end justify-between gap-3">
<div className="mx-auto max-w-4xl space-y-4">
<Briefing rows={all.data ?? []} onRun={() => navigate('/review')} />
<header className="flex flex-wrap items-end justify-between gap-3 pt-2">
<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>
</div>
{!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.loading && !rows.data && <Spinner label="Loading cases…" />}
{rows.data && list.length === 0 && (
<Empty>
Nothing in this queue.{' '}
{queue.stage === 'Awaiting Review' && 'Ava puts her findings here when she is done.'}
</Empty>
<Empty>Nothing here.{queue.stage === 'Awaiting Review' && ' When Ava finds something worth your time, it lands here.'}</Empty>
)}
<div className="grid gap-3">
<div className="space-y-3">
{list.map((r) => (
<CaseCard key={str(r.instance_id)} row={r} onOpen={() => 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 (
<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 }) {
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 (
<Card className="cursor-pointer transition-colors hover:border-brand/50" >
<button onClick={onOpen} className="w-full px-4 py-3.5 text-left">
<button
onClick={onOpen}
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 ${
waiting ? 'border-warn-edge' : 'border-line'
}`}
>
<div className="flex flex-wrap items-center gap-2">
<span className="tnum text-[13px] font-semibold text-ink">
Case #{str(row.instance_id)}
</span>
<Badge tone={quiet ? 'slate' : tone} dot={aiOwned}>
{quiet ? 'Nothing to report' : state}
<Badge tone={quiet ? 'slate' : (STAGE_TONE[state] ?? 'slate')} dot={aiOwned}>
{quiet ? 'Nothing to report' : waiting ? 'Needs your decision' : state}
</Badge>
{kind && FINDING_KIND[kind] && <Badge tone="violet">{FINDING_KIND[kind]}</Badge>}
{aiOwned ? (
<span className="inline-flex items-center gap-1 text-[11px] text-ai">
<Brain className="h-3 w-3" /> Ava
</span>
) : (
<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 className="ml-auto text-[11px] text-faint">
#{str(row.instance_id)} · {dateTime(row.updated_at)}
</span>
</div>
<p className="mt-2 line-clamp-2 text-sm text-ink">
{summary || (quiet ? scanned : '') || 'Ava is reviewing every payer, department and desk…'}
<div className="mt-2.5 flex items-start gap-4">
<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>
{(atRisk || recoverable) && (
<div className="mt-3 flex flex-wrap gap-x-6 gap-y-1">
{atRisk != null && (
<span className="text-xs text-faint">
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>
{recoverable != null && recoverable > 0 && (
<span className="shrink-0 text-right">
<span className="tnum block text-[22px] leading-none font-bold text-good">{money(recoverable)}</span>
<span className="block text-[11px] text-faint">recoverable</span>
</span>
)}
<ChevronRight className="mt-1 h-4 w-4 shrink-0 text-faint" />
</div>
)}
{quiet && scanned && <p className="mt-1.5 line-clamp-2 text-xs text-muted">{scanned}</p>}
</button>
</Card>
);
}

View File

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