diff --git a/src/components/AvaLive.tsx b/src/components/AvaLive.tsx index 9f46055..a1005e8 100644 --- a/src/components/AvaLive.tsx +++ b/src/components/AvaLive.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { AlertTriangle, Brain, @@ -70,6 +70,7 @@ export function AvaLive({ steps, decisions, working, + fill = false, }: { steps: AvaTraceStep[]; decisions: AvaDecision[]; @@ -77,8 +78,27 @@ export function AvaLive({ * the "still working" footer, so an empty trace reads as "starting" rather * than "nothing happened". */ working: boolean; + /** While the review runs the trace is the page: let the list take the + * viewport instead of a fixed 32rem box. */ + fill?: boolean; }) { const [open, setOpen] = useState(null); + // Follow the run: when a new step lands while she is working, keep the + // newest step in view — unless the viewer has scrolled up to read. + const listRef = useRef(null); + const stuck = useRef(true); + useEffect(() => { + const el = listRef.current; + if (!el || !working || !stuck.current) return; + el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }); + }, [steps.length, working]); + function onScroll() { + const el = listRef.current; + if (!el) return; + stuck.current = el.scrollHeight - el.scrollTop - el.clientHeight < 48; + } + const last = steps[steps.length - 1]; + const sinceLast = last ? Math.max(0, (Date.now() - new Date(last.ended_at ?? last.started_at).getTime()) / 1000) : 0; const queries = steps.filter((s) => s.type === 'api'); const failed = queries.filter((s) => s.query_state === 'FAILED').length; @@ -112,6 +132,7 @@ export function AvaLive({ {spend > 0 && {money(spend.toFixed(2))}} + {working &&