feat: reviewer chooses the dates — data as-of and the analysis window
Run review keeps the one-click default; a Custom run card takes 'data as of', 'window from' and 'window to' (all optional) and previews the window Ava will use. Case page shows the chosen window. Pairs with 12_review_window.sql (review_from / review_to on Run Daily Review) and the charter's THE WINDOW rule. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
e8741f6333
commit
5e0649fb7e
@ -240,9 +240,16 @@ export function CaseScreen() {
|
||||
<Facts
|
||||
cols={3}
|
||||
rows={[
|
||||
{ label: 'Trigger', value: str(row.review_as_of) ? 'Manual run (historical date)' : 'Daily review' },
|
||||
{ label: 'Data as of', value: str(row.review_as_of) ? str(row.review_as_of).slice(0, 10) : 'Latest available' },
|
||||
{ label: 'Look-back', value: row.period_weeks ? `${str(row.period_weeks)} weeks` : '6 weeks' },
|
||||
{ label: 'Trigger', value: str(row.review_as_of) || str(row.review_from) || str(row.review_to) ? 'Custom run (chosen dates)' : 'Daily review' },
|
||||
{ label: 'Data as of', value: str(row.review_to || row.review_as_of) ? str(row.review_to || row.review_as_of).slice(0, 10) : 'Latest available' },
|
||||
{
|
||||
label: 'Analysis window',
|
||||
value: str(row.review_from)
|
||||
? `${str(row.review_from).slice(0, 10)} – ${str(row.review_to || row.review_as_of).slice(0, 10) || 'latest'}`
|
||||
: row.period_weeks
|
||||
? `${str(row.period_weeks)} weeks`
|
||||
: '6 weeks',
|
||||
},
|
||||
...(str(row.review_note) ? [{ label: 'Reviewer note', value: str(row.review_note), wide: true }] : []),
|
||||
...(str(row.approval_note) ? [{ label: 'Approval note', value: str(row.approval_note), wide: true }] : []),
|
||||
]}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Brain, Database, Moon, Radar } from 'lucide-react';
|
||||
import { Brain, CalendarRange, Database, Radar } from 'lucide-react';
|
||||
import { useZino } from '../api/provider';
|
||||
import { ACT, LAKEHOUSE, QUIET_MORNING_AS_OF, WORKFLOW } from '../api/config';
|
||||
import { Button, Card, ErrorNote } from '../components/core';
|
||||
import { Button, Card, ErrorNote, Input, Label } from '../components/core';
|
||||
import { shortDate } from '../format';
|
||||
|
||||
/**
|
||||
* Run the daily review, now.
|
||||
@ -12,28 +13,43 @@ import { Button, Card, ErrorNote } from '../components/core';
|
||||
* the button exists so a room does not have to wait until 6am. Nothing is
|
||||
* asked of Ava — she decides what, if anything, is worth a person's time.
|
||||
*
|
||||
* The second button anchors the sweep on a date before the August spike. The
|
||||
* demo data is static, so without it every run flags the same thing; with it,
|
||||
* one run can genuinely come back "nothing to report" — which is what most
|
||||
* mornings look like, and the contrast is the point.
|
||||
* The custom run lets the reviewer choose the dates: what the data should be
|
||||
* treated as "as of" (a replayed morning), and the window Ava compares against
|
||||
* its baseline. The demo data is static, so without a date every run flags
|
||||
* the same thing; anchored before the August spike, a run can genuinely come
|
||||
* back "nothing to report" — which is what most mornings look like.
|
||||
*/
|
||||
export function RunReviewScreen() {
|
||||
const navigate = useNavigate();
|
||||
const { client } = useZino();
|
||||
const [busy, setBusy] = useState<'now' | 'quiet' | null>(null);
|
||||
const [busy, setBusy] = useState<'now' | 'custom' | null>(null);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const [asOf, setAsOf] = useState('');
|
||||
const [from, setFrom] = useState('');
|
||||
const [to, setTo] = useState('');
|
||||
|
||||
async function run(asOf?: string) {
|
||||
setBusy(asOf ? 'quiet' : 'now');
|
||||
// The window Ava will actually use, so the reviewer sees it before pressing Run.
|
||||
const effectiveTo = to || asOf;
|
||||
const dateErr =
|
||||
from && effectiveTo && from > effectiveTo
|
||||
? 'The window must start before it ends.'
|
||||
: to && asOf && to > asOf
|
||||
? '"Window to" cannot be after "data as of".'
|
||||
: null;
|
||||
|
||||
async function run(custom: boolean) {
|
||||
setBusy(custom ? 'custom' : 'now');
|
||||
setErr(null);
|
||||
try {
|
||||
// The activity's one field is optional; omit it entirely for a live run
|
||||
// so the workflow sees a genuinely empty submission.
|
||||
const res = await client.startInstance(
|
||||
WORKFLOW,
|
||||
ACT.REVIEW,
|
||||
asOf ? { review_as_of__review: asOf } : {},
|
||||
);
|
||||
// Every field on the activity is optional; send only the ones the
|
||||
// reviewer set, so a plain run is a genuinely empty submission.
|
||||
const data: Record<string, string> = {};
|
||||
if (custom) {
|
||||
if (asOf) data.review_as_of__review = asOf;
|
||||
if (from) data.review_from__review = from;
|
||||
if (to) data.review_to__review = to;
|
||||
}
|
||||
const res = await client.startInstance(WORKFLOW, ACT.REVIEW, data);
|
||||
if (res.instance_id) navigate(`/c/${res.instance_id}`);
|
||||
else navigate('/q/investigating');
|
||||
} catch (e) {
|
||||
@ -83,26 +99,76 @@ export function RunReviewScreen() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-wrap items-start gap-3 border-t border-line pt-4">
|
||||
<div>
|
||||
<Button kind="primary" onClick={() => run()} busy={busy === 'now'} disabled={busy !== null}>
|
||||
<div className="mt-5 border-t border-line pt-4">
|
||||
<Button kind="primary" onClick={() => run(false)} busy={busy === 'now'} disabled={busy !== null}>
|
||||
<Radar className="h-4 w-4" />
|
||||
Run review
|
||||
</Button>
|
||||
<p className="mt-1 max-w-[16rem] text-[11px] text-faint">
|
||||
Scheduled daily at 06:00. This runs the same activity immediately.
|
||||
<p className="mt-1 text-[11px] text-faint">
|
||||
Latest data, last 6 weeks against the 8 weeks before. Scheduled daily at 06:00; this runs
|
||||
the same activity immediately.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="px-4 py-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="rounded-full bg-panel2 p-2 text-muted ring-1 ring-line ring-inset">
|
||||
<CalendarRange className="h-4 w-4" />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-semibold text-ink">Custom run</p>
|
||||
<p className="mt-0.5 text-sm text-muted">
|
||||
Replay a past morning, or point the review at a specific period. Leave a date blank to
|
||||
use the default.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-3">
|
||||
<div>
|
||||
<Label>Data as of</Label>
|
||||
<Input type="date" value={asOf} onChange={setAsOf} />
|
||||
<p className="mt-1 text-[11px] text-faint">Ignore anything after this date. Try {shortDate(QUIET_MORNING_AS_OF)} for a quiet morning.</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button kind="secondary" onClick={() => run(QUIET_MORNING_AS_OF)} busy={busy === 'quiet'} disabled={busy !== null}>
|
||||
<Moon className="h-4 w-4" />
|
||||
Run as of {QUIET_MORNING_AS_OF}
|
||||
</Button>
|
||||
<p className="mt-1 max-w-[16rem] text-[11px] text-faint">
|
||||
Runs the review against data as of this date — a period with no material change,
|
||||
to show a no-findings outcome.
|
||||
</p>
|
||||
<Label>Window from</Label>
|
||||
<Input type="date" value={from} onChange={setFrom} />
|
||||
<p className="mt-1 text-[11px] text-faint">Start of the period Ava analyses.</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Window to</Label>
|
||||
<Input type="date" value={to} onChange={setTo} />
|
||||
<p className="mt-1 text-[11px] text-faint">End of the period. Defaults to "data as of".</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-xs text-muted">
|
||||
{dateErr ? (
|
||||
<span className="text-bad">{dateErr}</span>
|
||||
) : (
|
||||
<>
|
||||
Ava will compare{' '}
|
||||
<span className="font-semibold text-ink">
|
||||
{from ? shortDate(from) : effectiveTo ? '6 weeks before' : 'the last 6 weeks'}
|
||||
{from || effectiveTo ? ` – ${effectiveTo ? shortDate(effectiveTo) : 'latest data'}` : ''}
|
||||
</span>{' '}
|
||||
against the 8 weeks before it
|
||||
{asOf ? `, treating ${shortDate(asOf)} as the latest date` : ''}.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
kind="secondary"
|
||||
onClick={() => run(true)}
|
||||
busy={busy === 'custom'}
|
||||
disabled={busy !== null || !!dateErr || (!asOf && !from && !to)}
|
||||
>
|
||||
<CalendarRange className="h-4 w-4" />
|
||||
Run with these dates
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user