Display-only rewrite (format.prose) on every surface that shows her text — headline, work statement, full story, recommendations, re-check, queue cards, slides, notes. Stored text keeps ISO dates so deadline and spike parsers still read them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
336 lines
14 KiB
TypeScript
336 lines
14 KiB
TypeScript
import { useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { AlertTriangle, CheckCircle2, ChevronDown, ExternalLink, FileSearch } from 'lucide-react';
|
|
import type { Slice } from '../api/charts';
|
|
import { affectedClaimsHref, recoverableClaimsHref } from '../lib/claimsLink';
|
|
import { Badge, Card, Facts } from './core';
|
|
import type { AvaDecision, AvaTraceStep, Row } from '../api/types';
|
|
import { firstDeadline, headline, money, num, pct, prose, recommendations, shortDate, splitRecommendation, sqlSteps, str } from '../format';
|
|
|
|
/**
|
|
* Ava's findings, laid out for a person who has ninety seconds.
|
|
*
|
|
* 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".
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
/** The headline card: one sentence, and the three numbers. */
|
|
export function Verdict({
|
|
row,
|
|
slice = null,
|
|
kind = '',
|
|
anchor = null,
|
|
}: {
|
|
row: Row;
|
|
slice?: Slice | null;
|
|
kind?: string;
|
|
anchor?: string | null;
|
|
}) {
|
|
const summary = str(row.summary);
|
|
if (!summary) return null;
|
|
const line = prose(headline(summary));
|
|
const scanned = prose(row.scanned_summary);
|
|
const affectedHref = affectedClaimsHref(kind, slice, anchor);
|
|
const recoverableHref = recoverableClaimsHref(kind, slice, anchor);
|
|
const atRisk = num(row.amount_at_risk);
|
|
const recoverable = num(row.recoverable_amount);
|
|
const affected = num(row.affected_claims);
|
|
const deadline = firstDeadline(row.recommendations);
|
|
const confidence = num(row.confidence);
|
|
const reconciled = row.data_reconciled === true || str(row.data_reconciled) === 'true';
|
|
|
|
return (
|
|
<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">Summary</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 ? 'Reconciled with finance summary' : 'Not reconciled with finance summary'}
|
|
</span>
|
|
{confidence != null && (
|
|
<span>
|
|
Confidence <span className="font-semibold text-ink">{pct(confidence * 100, 0)}</span>
|
|
</span>
|
|
)}
|
|
<span className="ml-auto">AI analysis · Ava</span>
|
|
</div>
|
|
{scanned && (
|
|
<p className="mt-3 border-t border-ai-edge/60 pt-3 text-[13px] leading-relaxed text-muted">
|
|
<span className="font-semibold text-ink">What Ava reviewed. </span>
|
|
{scanned}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid gap-px bg-line sm:grid-cols-3">
|
|
<Big
|
|
label={kind === 'slow_payer' ? 'Claims unanswered' : kind === 'underpayment' ? 'Claims short-paid' : 'Claims denied'}
|
|
value={affected == null ? '—' : affected.toLocaleString('en-US')}
|
|
href={affectedHref}
|
|
/>
|
|
<Big label="Money at stake" value={money(atRisk)} tone="bad" />
|
|
<Big
|
|
label="Still recoverable"
|
|
value={money(recoverable)}
|
|
tone="good"
|
|
href={recoverableHref}
|
|
note={
|
|
deadline
|
|
? `if appeals are filed — first deadline ${shortDate(deadline)}`
|
|
: atRisk && recoverable
|
|
? `${Math.round((recoverable / atRisk) * 100)}% of what is at stake`
|
|
: undefined
|
|
}
|
|
/>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
/** The decision list. Rendered directly above the Approve / Send back / Reject buttons. */
|
|
export function Recommendations({ row }: { row: Row }) {
|
|
const recs = recommendations(row.recommendations).map(prose);
|
|
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">Recommended actions</h2>
|
|
<p className="mt-0.5 text-xs text-muted">Ranked by financial impact. Each names 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 = prose(row.summary);
|
|
const cause = prose(row.root_cause);
|
|
const secondary = prose(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="Analysis"
|
|
hint={hasSecondary ? 'Full analysis, root cause, and a secondary finding' : 'Full analysis and 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">Root cause</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" /> Secondary finding
|
|
</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.
|
|
*
|
|
* SOURCE OF TRUTH IS THE TRACE, not her sql_log field. The platform records
|
|
* every statement the agentic tool sent, verbatim, with Databricks' own
|
|
* SUCCEEDED/FAILED and the row count — so that is what is shown. Her sql_log
|
|
* is now a one-line-per-step index ("3c: department cut -> Cardiology +13
|
|
* pts") and is used only as the note beside each statement. Asking her to
|
|
* paste 16 full queries into a form field was what blew the tool call past
|
|
* the output cap and lost a whole investigation.
|
|
*/
|
|
export function SqlEvidence({ row, steps, decisions }: { row: Row; steps: AvaTraceStep[]; decisions: AvaDecision[] }) {
|
|
const [open, setOpen] = useState(false);
|
|
// Only the attempt that produced the findings. A case can carry an earlier
|
|
// attempt that failed to submit (its queries are still in the trace); showing
|
|
// both would double the count and make her look twice as busy as she was.
|
|
const submitted = decisions.filter((d) => d.status === 'submitted').slice(-1)[0];
|
|
const scoped = submitted ? steps.filter((s) => s.decision_id == null || String(s.decision_id) === String(submitted.id)) : steps;
|
|
const queries = scoped.filter((s) => s.type === 'api' && s.detail);
|
|
const notes = sqlSteps(row.sql_log);
|
|
if (queries.length === 0 && notes.length === 0) return null;
|
|
|
|
return (
|
|
<Disclosure
|
|
open={open}
|
|
onToggle={() => setOpen(!open)}
|
|
icon={<CheckCircle2 className="h-4 w-4 text-good" />}
|
|
title="Query log"
|
|
hint={
|
|
queries.length
|
|
? `${queries.length} queries executed against Databricks — reproducible`
|
|
: `${notes.length} analysis steps`
|
|
}
|
|
>
|
|
{queries.length > 0 ? (
|
|
<ol className="divide-y divide-line">
|
|
{queries.map((q, i) => (
|
|
<li key={`${q.seq}-${i}`} className="px-5 py-3">
|
|
<p className="flex items-center gap-2 text-[11px] font-semibold tracking-wide text-faint uppercase">
|
|
Query {i + 1}
|
|
{q.query_state === 'FAILED' ? (
|
|
<Badge tone="amber">error — corrected in next query</Badge>
|
|
) : q.rows_returned != null ? (
|
|
<span className="tnum font-normal normal-case">{q.rows_returned} row{q.rows_returned === 1 ? '' : 's'}</span>
|
|
) : null}
|
|
</p>
|
|
<pre className="sql mt-1 text-muted">{q.detail}</pre>
|
|
{notes[i]?.note && <p className="mt-1.5 text-xs text-ink">{notes[i].note}</p>}
|
|
</li>
|
|
))}
|
|
</ol>
|
|
) : (
|
|
<ol className="divide-y divide-line">
|
|
{notes.map((s, i) => (
|
|
<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="mt-1 text-sm text-ink">{s.sql}</p>
|
|
{s.note && <p className="mt-1 text-xs text-muted">{s.note}</p>}
|
|
</li>
|
|
))}
|
|
</ol>
|
|
)}
|
|
</Disclosure>
|
|
);
|
|
}
|
|
|
|
/** The re-check. Same shape either way, so "fixed" and "not fixed" read alike. */
|
|
export function Recheck({ row }: { row: Row }) {
|
|
const summary = prose(row.recheck_summary);
|
|
if (!summary) return null;
|
|
const current = num(row.current_rate);
|
|
const baseline = num(row.baseline_rate);
|
|
const recovered = num(row.recovered_amount);
|
|
const better = current != null && baseline != null && current - baseline <= 2;
|
|
|
|
return (
|
|
<Card
|
|
title="Re-check result"
|
|
subtitle="Scheduled re-check after approval"
|
|
actions={<Badge tone={better ? 'emerald' : 'amber'}>{better ? 'Within baseline' : 'Still elevated'}</Badge>}
|
|
>
|
|
<div className="space-y-3 px-5 py-4">
|
|
<Facts
|
|
cols={3}
|
|
rows={[
|
|
{ label: 'Current denial rate', value: pct(current), tone: better ? 'emerald' : 'rose' },
|
|
{ label: 'Baseline', value: pct(baseline) },
|
|
{ label: 'Recovered to date', value: recovered == null ? '—' : money(recovered), tone: 'emerald' },
|
|
]}
|
|
/>
|
|
<p className="text-sm leading-relaxed whitespace-pre-wrap text-muted">{summary}</p>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function Big({ label, value, tone, note, href }: { label: string; value: string; tone?: 'good' | 'bad'; note?: string; href?: string | null }) {
|
|
const text = tone === 'good' ? 'text-good' : tone === 'bad' ? 'text-bad' : 'text-ink';
|
|
const body = (
|
|
<>
|
|
<p className="text-[11px] font-semibold tracking-wide text-faint uppercase">{label}</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>}
|
|
{href && (
|
|
<p className="mt-1.5 inline-flex items-center gap-1 text-xs text-brand">
|
|
See the claims <ExternalLink className="h-3 w-3" />
|
|
</p>
|
|
)}
|
|
</>
|
|
);
|
|
// A number you can click opens the rows behind it in the claims table.
|
|
return href ? (
|
|
<Link to={href} className="block bg-panel px-5 py-4 transition-colors hover:bg-panel2/60" title="Open these claims">
|
|
{body}
|
|
</Link>
|
|
) : (
|
|
<div className="bg-panel px-5 py-4">{body}</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()),
|
|
);
|
|
}
|