import { useId, useLayoutEffect, useRef, useState } from "react";
import { Badge, Button, Tooltip } from "@cloudflare/kumo";
import { Scales } from "@phosphor-icons/react";
import type { Race, Review, ProjectState } from "./contracts.ts";
import type { ViewModel } from "./model.ts";
import { ReviewActions, type ViewActions } from "./view.tsx";
import { agentIdentities, Saddlecloth } from "./lane.tsx";
import { adviceForReview, judgePromotion, pushedAfterAdvice, rankingWord } from "./judge.ts";
import { relativeTime } from "./time.ts";
export function JudgePickBadge({ race, agent }: { race: Race; agent: string }) {
return race.judge && race.verdict?.ranking[0] === agent ?
Judge's pick : null;
}
export function ReviewAdvice({ state, review }: { state?: ProjectState; review: Review }) {
const advice = state && adviceForReview(state, review);
return advice ?
Judge's advice: {advice.index === 0 ? "pick" :
`${rankingWord(advice.index)} of ${advice.race.verdict!.ranking.length}`} · {relativeTime(advice.at)}
: null;
}
export function JudgeAdvice({ race, view, actions }: { race: Race; view: ViewModel; actions: ViewActions }) {
return race.judge ? : null;
}
function AdviceContents({ race, view, actions }: { race: Race; view: ViewModel; actions: ViewActions }) {
const [expanded, setExpanded] = useState(false);
const rationaleId = useId();
const state = { ...view.state!, recentActivity: view.activity };
const advice = race.verdict;
const identities = agentIdentities(state.agents);
const pick = judgePromotion(state, race);
const pickedEntrant = race.entrants.find((entrant) => entrant.agent === pick.pick);
// The ranking already explains a newer push; keep the action blocked without repeating it.
const pickPushedLater = !!pickedEntrant && pushedAfterAdvice(state, race, pickedEntrant);
const [long, setLong] = useState(!!advice && (advice.rationale.split("\n").length > 5 || advice.rationale.length > 400));
const rationale = useRef(null);
useLayoutEffect(() => {
const element = rationale.current;
const window = element?.ownerDocument.defaultView;
if (!element || !window) return;
const measure = () => {
const line = parseFloat(window.getComputedStyle(element).lineHeight);
// DOM-only fixtures have no layout; the initial estimate remains their fallback.
if (element.clientHeight > 0 && Number.isFinite(line)) setLong(element.scrollHeight > line * 5 + 1);
};
measure();
const observer = window.ResizeObserver && new window.ResizeObserver(measure);
observer?.observe(element.parentElement!);
return () => observer?.disconnect();
}, [advice?.rationale]);
return
Judge's advice
{!advice ? {race.judge} {race.status === "finished" ?
"didn't send advice before the finish." : "hasn't sent advice yet."}
: <>
{advice.ranking.map((agent, index) =>
{index > 0 && › } )}
From {race.judge} · } >{relativeTime(advice.at)}
{race.status === "finished" && " · before the finish"}
Judge's ranking: {advice.ranking.join(", then ")}.
{advice.ranking.map((agent, index) => {
const entrant = race.entrants.find((e) => e.agent === agent);
const later = entrant && pushedAfterAdvice(state, race, entrant);
return
{rankingWord(index)} {agent}
{later && {agent} pushed after this advice
}
{entrant?.closedAt && !entrant.promotedAt && entrant.closedBy !== "race-finished" && left the race
}
;
})}
{advice.rationale}
{long && setExpanded(!expanded)}>{expanded ? "Show less" : "Show all"} }
{race.status === "finished" ?
race.winner ? race.winner === pick.pick ? `Winner: ${race.winner} (the judge's pick).` :
`Winner: ${race.winner}. The judge picked ${pick.pick}.` : "This race finished without a recorded winner." :
actions.owner ? "This is advice. Only the owner can promote, and the first promote wins the race." : "This is advice. The owner promotes."}
{actions.owner && race.status === "open" &&
{pick.review &&
actions.openReceipt ? actions.openReceipt(pick.review!.id) : actions.openReview(pick.review!.id)}
advice={{ judge: race.judge!, pick: pick.pick!, at: advice.at }} />}
{pick.available ? {pick.pick} : !pickPushedLater && {pick.reason}
}
}
>}
;
}