Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-dashboard/src/judge.tsx

Snapshot at 34e3175

Containing directory

5,975 bytes · textView raw
1import { useId, useLayoutEffect, useRef, useState } from "react";
2import { Badge, Button, Tooltip } from "@cloudflare/kumo";
3import { Scales } from "@phosphor-icons/react";
4import type { Race, Review, ProjectState } from "./contracts.ts";
5import type { ViewModel } from "./model.ts";
6import { ReviewActions, type ViewActions } from "./view.tsx";
7import { agentIdentities, Saddlecloth } from "./lane.tsx";
8import { adviceForReview, judgePromotion, pushedAfterAdvice, rankingWord } from "./judge.ts";
9import { relativeTime } from "./time.ts";
10
11export function JudgePickBadge({ race, agent }: { race: Race; agent: string }) {
12  return race.judge && race.verdict?.ranking[0] === agent ?
13    <Badge variant="secondary" className="judge-pick"><Scales size={12} aria-hidden="true" />Judge's pick</Badge> : null;
14}
15export function ReviewAdvice({ state, review }: { state?: ProjectState; review: Review }) {
16  const advice = state && adviceForReview(state, review);
17  return advice ? <p className="caption muted review-advice">Judge's advice: {advice.index === 0 ? "pick" :
18    `${rankingWord(advice.index)} of ${advice.race.verdict!.ranking.length}`} · {relativeTime(advice.at)}</p> : null;
19}
20export function JudgeAdvice({ race, view, actions }: { race: Race; view: ViewModel; actions: ViewActions }) {
21  return race.judge ? <AdviceContents key={JSON.stringify(race.verdict)} race={race} view={view} actions={actions} /> : null;
22}
23function AdviceContents({ race, view, actions }: { race: Race; view: ViewModel; actions: ViewActions }) {
24  const [expanded, setExpanded] = useState(false);
25  const rationaleId = useId();
26  const state = { ...view.state!, recentActivity: view.activity };
27  const advice = race.verdict;
28  const identities = agentIdentities(state.agents);
29  const pick = judgePromotion(state, race);
30  const pickedEntrant = race.entrants.find((entrant) => entrant.agent === pick.pick);
31  // The ranking already explains a newer push; keep the action blocked without repeating it.
32  const pickPushedLater = !!pickedEntrant && pushedAfterAdvice(state, race, pickedEntrant);
33  const [long, setLong] = useState(!!advice && (advice.rationale.split("\n").length > 5 || advice.rationale.length > 400));
34  const rationale = useRef<HTMLParagraphElement>(null);
35  useLayoutEffect(() => {
36    const element = rationale.current;
37    const window = element?.ownerDocument.defaultView;
38    if (!element || !window) return;
39    const measure = () => {
40      const line = parseFloat(window.getComputedStyle(element).lineHeight);
41      // DOM-only fixtures have no layout; the initial estimate remains their fallback.
42      if (element.clientHeight > 0 && Number.isFinite(line)) setLong(element.scrollHeight > line * 5 + 1);
43    };
44    measure();
45    const observer = window.ResizeObserver && new window.ResizeObserver(measure);
46    observer?.observe(element.parentElement!);
47    return () => observer?.disconnect();
48  }, [advice?.rationale]);
49  return <section className="judge-advice" aria-label="Judge's advice">
50    <h4 className="label"><Scales size={16} aria-hidden="true" />Judge's advice</h4>
51    {!advice ? <p className="caption muted">{race.judge} {race.status === "finished" ?
52      "didn't send advice before the finish." : "hasn't sent advice yet."}</p> : <>
53      <div className="judge-forecast" aria-hidden="true">{advice.ranking.map((agent, index) =>
54        <span key={agent}>{index > 0 && <span className="muted">›</span>}<Saddlecloth identity={identities.get(agent)} /></span>)}</div>
55      <p className="caption muted judge-from">From {race.judge} · <Tooltip content={advice.at}
56        render={<time dateTime={advice.at} tabIndex={0} />} >{relativeTime(advice.at)}</Tooltip>
57        {race.status === "finished" && " · before the finish"}</p>
58      <span className="sr-only">Judge's ranking: {advice.ranking.join(", then ")}.</span>
59      <ol className="judge-ranking" aria-label="Judge's ranking">{advice.ranking.map((agent, index) => {
60        const entrant = race.entrants.find((e) => e.agent === agent);
61        const later = entrant && pushedAfterAdvice(state, race, entrant);
62        return <li key={agent}>
63          <div><span className="caption muted">{rankingWord(index)}</span><Saddlecloth identity={identities.get(agent)} /><strong>{agent}</strong></div>
64          {later && <p className="caption muted">{agent} pushed after this advice</p>}
65          {entrant?.closedAt && !entrant.promotedAt && entrant.closedBy !== "race-finished" && <p className="caption muted">left the race</p>}
66        </li>;
67      })}</ol>
68      <blockquote className="judge-rationale"><p ref={rationale} id={rationaleId} className={long && !expanded ? "judge-clamped" : undefined}>{advice.rationale}</p>
69        <footer className="caption muted">{race.judge}</footer></blockquote>
70      {long && <Button variant="ghost" size="sm" aria-expanded={expanded} aria-controls={rationaleId}
71        onClick={() => setExpanded(!expanded)}>{expanded ? "Show less" : "Show all"}</Button>}
72      <p className="caption muted">{race.status === "finished" ?
73        race.winner ? race.winner === pick.pick ? `Winner: ${race.winner} (the judge's pick).` :
74          `Winner: ${race.winner}. The judge picked ${pick.pick}.` : "This race finished without a recorded winner." :
75        actions.owner ? "This is advice. Only the owner can promote, and the first promote wins the race." : "This is advice. The owner promotes."}</p>
76      {actions.owner && race.status === "open" && <div className="judge-action">
77        {pick.review && <ReviewActions review={pick.review} view={view} actions={actions}
78          openReceipt={() => actions.openReceipt ? actions.openReceipt(pick.review!.id) : actions.openReview(pick.review!.id)}
79          advice={{ judge: race.judge!, pick: pick.pick!, at: advice.at }} />}
80        {pick.available ? <span className="caption muted">{pick.pick}</span> : !pickPushedLater && <p className="caption muted">{pick.reason}</p>}
81      </div>}
82    </>}
83  </section>;
84}
85