Ava now writes the work statement as four labelled lines (REVIEWED / FOUND / WATCHED / CLEAR, items separated by ';'); the case page renders each as a row of short chips under 'Review coverage'. Older prose falls back to sentence bullets. Charter also asks for a <=25-word first sentence and plain English — no column names or SQL in prose. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
168 lines
7.0 KiB
TypeScript
168 lines
7.0 KiB
TypeScript
import { useNavigate, useParams } from 'react-router-dom';
|
|
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, Button, Empty, ErrorNote, Spinner } from '../components/core';
|
|
import type { Row } from '../api/types';
|
|
import { dateTime, headline, money, num, prose, sentences, str, workStatement } from '../format';
|
|
|
|
/**
|
|
* A queue, as a morning briefing.
|
|
*
|
|
* 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();
|
|
const navigate = useNavigate();
|
|
const { client } = useZino();
|
|
const queue = QUEUES.find((x) => x.path === q) ?? QUEUES[0];
|
|
|
|
const rows = useQuery(
|
|
() =>
|
|
client.rows(RECORD_VIEW, {
|
|
limit: 100,
|
|
sortBy: 'updated_at',
|
|
sortDir: 'desc',
|
|
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="mx-auto max-w-5xl 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-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>}
|
|
</header>
|
|
|
|
{rows.error && <ErrorNote>{rows.error}</ErrorNote>}
|
|
{rows.loading && !rows.data && <Spinner label="Loading cases…" />}
|
|
{rows.data && list.length === 0 && (
|
|
<Empty>No cases in this queue.</Empty>
|
|
)}
|
|
|
|
<div className="space-y-3">
|
|
{list.map((r) => (
|
|
<CaseCard key={str(r.instance_id)} row={r} onOpen={() => navigate(`/c/${str(r.instance_id)}`)} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** 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">
|
|
Daily review{latest ? ` · last run ${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
|
|
? 'No cases awaiting decision.'
|
|
: [
|
|
waiting.length > 0 &&
|
|
`${waiting.length} case${waiting.length === 1 ? '' : 's'} awaiting decision${
|
|
recoverable > 0 ? ` · ${money(recoverable)} recoverable` : ''
|
|
}`,
|
|
working.length > 0 && `${working.length} review${working.length === 1 ? '' : 's'} in progress`,
|
|
needsHuman.length > 0 && `${needsHuman.length} escalated`,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' · ')}
|
|
</p>
|
|
<p className="mt-0.5 text-xs text-muted">
|
|
Automated review of all payers, departments and front desks. A case is opened only when a change clears the materiality threshold.
|
|
</p>
|
|
</div>
|
|
<Button kind="primary" onClick={onRun}>
|
|
<Radar className="h-4 w-4" /> Run review
|
|
</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 summary = prose(row.summary);
|
|
const scanned = prose(row.scanned_summary);
|
|
const flagged = num(row.flagged_count);
|
|
const kind = str(row.finding_kind);
|
|
const recoverable = num(row.recoverable_amount);
|
|
const aiOwned = state === 'Investigating' || state === 'Monitoring';
|
|
const quiet = flagged === 0 && !summary;
|
|
const waiting = state === 'Awaiting Review';
|
|
|
|
return (
|
|
<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">
|
|
<Badge tone={quiet ? 'slate' : (STAGE_TONE[state] ?? 'slate')} dot={aiOwned}>
|
|
{quiet ? 'No findings' : waiting ? 'Awaiting decision' : state}
|
|
</Badge>
|
|
{kind && FINDING_KIND[kind] && <Badge tone="violet">{FINDING_KIND[kind]}</Badge>}
|
|
<span className="ml-auto text-[11px] text-faint">
|
|
#{str(row.instance_id)} · {dateTime(row.updated_at)}
|
|
</span>
|
|
</div>
|
|
|
|
<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
|
|
? 'No material change detected across payers, departments or front desks.'
|
|
: 'Review in progress…'}
|
|
</p>
|
|
{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">
|
|
{(workStatement(scanned)?.find((r) => r.key === 'REVIEWED')?.items ?? sentences(scanned).slice(0, 1)).join(' · ')}
|
|
</p>
|
|
)}
|
|
</button>
|
|
);
|
|
}
|