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
|
<Facts
|
||||||
cols={3}
|
cols={3}
|
||||||
rows={[
|
rows={[
|
||||||
{ label: 'Trigger', value: str(row.review_as_of) ? 'Manual run (historical date)' : 'Daily review' },
|
{ 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_as_of) ? str(row.review_as_of).slice(0, 10) : 'Latest available' },
|
{ 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: 'Look-back', value: row.period_weeks ? `${str(row.period_weeks)} weeks` : '6 weeks' },
|
{
|
||||||
|
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.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 }] : []),
|
...(str(row.approval_note) ? [{ label: 'Approval note', value: str(row.approval_note), wide: true }] : []),
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 { useZino } from '../api/provider';
|
||||||
import { ACT, LAKEHOUSE, QUIET_MORNING_AS_OF, WORKFLOW } from '../api/config';
|
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.
|
* 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
|
* 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.
|
* 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
|
* The custom run lets the reviewer choose the dates: what the data should be
|
||||||
* demo data is static, so without it every run flags the same thing; with it,
|
* treated as "as of" (a replayed morning), and the window Ava compares against
|
||||||
* one run can genuinely come back "nothing to report" — which is what most
|
* its baseline. The demo data is static, so without a date every run flags
|
||||||
* mornings look like, and the contrast is the point.
|
* 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() {
|
export function RunReviewScreen() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { client } = useZino();
|
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 [err, setErr] = useState<string | null>(null);
|
||||||
|
const [asOf, setAsOf] = useState('');
|
||||||
|
const [from, setFrom] = useState('');
|
||||||
|
const [to, setTo] = useState('');
|
||||||
|
|
||||||
async function run(asOf?: string) {
|
// The window Ava will actually use, so the reviewer sees it before pressing Run.
|
||||||
setBusy(asOf ? 'quiet' : 'now');
|
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);
|
setErr(null);
|
||||||
try {
|
try {
|
||||||
// The activity's one field is optional; omit it entirely for a live run
|
// Every field on the activity is optional; send only the ones the
|
||||||
// so the workflow sees a genuinely empty submission.
|
// reviewer set, so a plain run is a genuinely empty submission.
|
||||||
const res = await client.startInstance(
|
const data: Record<string, string> = {};
|
||||||
WORKFLOW,
|
if (custom) {
|
||||||
ACT.REVIEW,
|
if (asOf) data.review_as_of__review = asOf;
|
||||||
asOf ? { 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}`);
|
if (res.instance_id) navigate(`/c/${res.instance_id}`);
|
||||||
else navigate('/q/investigating');
|
else navigate('/q/investigating');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -83,26 +99,76 @@ export function RunReviewScreen() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 flex flex-wrap items-start gap-3 border-t border-line pt-4">
|
<div className="mt-5 border-t border-line pt-4">
|
||||||
<div>
|
<Button kind="primary" onClick={() => run(false)} busy={busy === 'now'} disabled={busy !== null}>
|
||||||
<Button kind="primary" onClick={() => run()} busy={busy === 'now'} disabled={busy !== null}>
|
|
||||||
<Radar className="h-4 w-4" />
|
<Radar className="h-4 w-4" />
|
||||||
Run review
|
Run review
|
||||||
</Button>
|
</Button>
|
||||||
<p className="mt-1 max-w-[16rem] text-[11px] text-faint">
|
<p className="mt-1 text-[11px] text-faint">
|
||||||
Scheduled daily at 06:00. This runs the same activity immediately.
|
Latest data, last 6 weeks against the 8 weeks before. Scheduled daily at 06:00; this runs
|
||||||
|
the same activity immediately.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<Button kind="secondary" onClick={() => run(QUIET_MORNING_AS_OF)} busy={busy === 'quiet'} disabled={busy !== null}>
|
<Label>Window from</Label>
|
||||||
<Moon className="h-4 w-4" />
|
<Input type="date" value={from} onChange={setFrom} />
|
||||||
Run as of {QUIET_MORNING_AS_OF}
|
<p className="mt-1 text-[11px] text-faint">Start of the period Ava analyses.</p>
|
||||||
</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>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user