import { useState } from "react"; import type { AdaptationRule, TriggerProof, } from "../../../../hooks/lib/adaptation-rules"; import type { ActiveRule, PipelineCandidate, RelationshipView } from "../../relationship"; import { Badge } from "../components/badge"; import { Button } from "../components/button"; import { clock } from "../format"; import { Empty, Panel, Pending } from "../frame"; import { useLoaded } from "../lib/api"; import { decideRule } from "../lib/write"; const REACTIONS = ["approved", "follow-up", "new-topic", "corrected", "repeated"]; function proofLine(proof: TriggerProof | undefined): string { if (!proof) return "not proven"; return `fired on ${proof.firedCorrections}/${proof.corrections} corrections, ${proof.firedOrdinary}/${proof.ordinary} ordinary turns`; } function Facts({ rows }: { rows: [string, string | undefined][] }) { return (
{rows .filter(([, value]) => value) .map(([label, value]) => (
{label}
{value}
))}
); } function ruleFacts(rule: AdaptationRule): [string, string | undefined][] { return [ ["trigger", `${rule.trigger.side} · /${rule.trigger.pattern}/i`], ["steering", rule.steering], ["check", rule.check], ["proof", proofLine(rule.proof)], ]; } function Evidence({ items }: { items: string[] }) { if (items.length === 0) return null; return ( ); } function replacedTrigger(rule: AdaptationRule, active: ActiveRule[]): string | undefined { if (!rule.widens) return undefined; const target = active.find((r) => r.id === rule.widens); if (!target) return `the trigger of rule ${rule.widens}`; return `/${target.trigger.pattern}/i`; } function Draft({ rule, active, onDecided, }: { rule: AdaptationRule; active: ActiveRule[]; onDecided: () => void; }) { const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const decide = (decision: "approved" | "denied") => { setBusy(true); void decideRule(rule.id, decision).then((failure) => { setBusy(false); setError(failure); if (!failure) onDecided(); }); }; return (
{rule.when}
{error && (

{error}

)}
); } function effectLine(rule: ActiveRule): string { const { fired, sentBack, judged, correctedAfter } = rule.effect; if (fired === 0) return "has not fired yet"; const back = rule.trigger.side === "reply" ? `, sent back ${sentBack}` : ""; return `fired ${fired}${back} · corrected after ${correctedAfter} of ${judged} judged`; } function Active({ rule }: { rule: ActiveRule }) { return (
{rule.when}
); } function percent(part: number, whole: number): string { return whole === 0 ? "—" : `${Math.round((part / whole) * 100)}%`; } function LastDays({ view }: { view: RelationshipView }) { const { turns } = view; const corrected = turns.byReaction.corrected ?? 0; return (
{REACTIONS.map((reaction) => ( {reaction} {turns.byReaction[reaction] ?? 0} ))}
{turns.recentCorrections.length === 0 ? ( No confirmed correction in this window. ) : (
    {turns.recentCorrections.map((c) => (
  • {clock(c.ts)} {c.issue} “{c.message}”
  • ))}
)}
); } function pipelineLine(candidate: PipelineCandidate): string { if (candidate.verdict === "waiting") return `needs ${candidate.ordinaryNeeded} more ordinary turns`; return `failed · ${proofLine(candidate.proof)}`; } function Pipeline({ candidates }: { candidates: PipelineCandidate[] }) { return ( {candidates.length === 0 ? ( Nothing being proven. ) : ( candidates.map((candidate) => (
{candidate.when} {pipelineLine(candidate)}
)) )}
); } function Denied({ rules }: { rules: AdaptationRule[] }) { if (rules.length === 0) return null; return (
{rules.length} denied
    {rules.map((rule) => (
  • {rule.when} {rule.steering}
  • ))}
); } export function Relationship() { const [version, setVersion] = useState(0); const view = useLoaded(`/api/relationship?v=${version}`); if (view.state !== "ready") return ; const { drafts, active, denied, pipeline } = view.data; const reload = () => setVersion(version + 1); return (
{drafts.length === 0 ? ( No rule draft waits for a decision. ) : ( drafts.map((rule) => ( )) )} {active.length === 0 ? ( No rule approved yet. ) : ( active.map((rule) => ) )}
); }