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 (
{items.map((item) => (
- {item}
))}
);
}
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 (
);
}
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) => )
)}
);
}