@anthusai/papyrus 1.4.0-next.6 → 1.4.0-next.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,204 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { fetchAuthSession } from "aws-amplify/auth";
5
+ import { isLocalPreviewEnabled } from "../lib/local-preview";
6
+ import {
7
+ DEFAULT_DELEGATION_HOURS,
8
+ MAXIMUM_DELEGATION_HOURS,
9
+ MINIMUM_DELEGATION_HOURS,
10
+ buildDelegatedCommandLine,
11
+ delegationExpiryText,
12
+ describeDelegationError,
13
+ validateDelegationInput,
14
+ type CurationDelegationRecord,
15
+ } from "../lib/curation-delegation-helpers";
16
+ import { createCurationDelegation, listCurationDelegations, revokeCurationDelegation, type CreatedCurationDelegation } from "./curation-delegation-client";
17
+
18
+ type IssuedToken = { agentLabel: string; token: string };
19
+
20
+ async function readSignedInEmail(): Promise<string> {
21
+ if (isLocalPreviewEnabled()) return "ryan@example.com";
22
+ try {
23
+ const session = await fetchAuthSession();
24
+ const email = session.tokens?.idToken?.payload?.email;
25
+ return typeof email === "string" ? email : "";
26
+ } catch {
27
+ return "";
28
+ }
29
+ }
30
+
31
+ export function AdministrationDelegationsPanel({ disabled }: { disabled: boolean }) {
32
+ const [agentLabelText, setAgentLabelText] = useState("");
33
+ const [hoursText, setHoursText] = useState(String(DEFAULT_DELEGATION_HOURS));
34
+ const [busy, setBusy] = useState(false);
35
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
36
+ const [issued, setIssued] = useState<IssuedToken | null>(null);
37
+ const [copied, setCopied] = useState<"token" | "command" | null>(null);
38
+ const [email, setEmail] = useState("");
39
+ const [delegations, setDelegations] = useState<CurationDelegationRecord[]>([]);
40
+ const [confirmingRevokeId, setConfirmingRevokeId] = useState<string | null>(null);
41
+ const [nowMilliseconds, setNowMilliseconds] = useState(() => Date.now());
42
+ const mounted = useRef(true);
43
+
44
+ useEffect(() => {
45
+ mounted.current = true;
46
+ void readSignedInEmail().then((value) => {
47
+ if (mounted.current) setEmail(value);
48
+ });
49
+ void refreshDelegations();
50
+ const timer = window.setInterval(() => setNowMilliseconds(Date.now()), 30_000);
51
+ return () => {
52
+ mounted.current = false;
53
+ window.clearInterval(timer);
54
+ setIssued(null);
55
+ };
56
+ }, []);
57
+
58
+ async function refreshDelegations() {
59
+ try {
60
+ const listed = await listCurationDelegations();
61
+ if (mounted.current) setDelegations(listed);
62
+ } catch (error) {
63
+ if (mounted.current) setErrorMessage(describeDelegationError(error));
64
+ }
65
+ }
66
+
67
+ async function handleCreate() {
68
+ const validation = validateDelegationInput(agentLabelText, hoursText);
69
+ if (!validation.ok) {
70
+ setErrorMessage(validation.message);
71
+ return;
72
+ }
73
+ setBusy(true);
74
+ setErrorMessage(null);
75
+ try {
76
+ const created: CreatedCurationDelegation = await createCurationDelegation(validation.agentLabel, validation.hours);
77
+ setIssued({ agentLabel: created.agentLabel, token: created.token });
78
+ setCopied(null);
79
+ await refreshDelegations();
80
+ setAgentLabelText("");
81
+ setNowMilliseconds(Date.now());
82
+ } catch (error) {
83
+ setErrorMessage(describeDelegationError(error));
84
+ } finally {
85
+ setBusy(false);
86
+ }
87
+ }
88
+
89
+ async function handleRevoke(delegationId: string) {
90
+ setBusy(true);
91
+ setErrorMessage(null);
92
+ try {
93
+ await revokeCurationDelegation(delegationId);
94
+ await refreshDelegations();
95
+ setConfirmingRevokeId(null);
96
+ } catch (error) {
97
+ setErrorMessage(describeDelegationError(error));
98
+ } finally {
99
+ setBusy(false);
100
+ }
101
+ }
102
+
103
+ async function copyText(kind: "token" | "command", text: string) {
104
+ try {
105
+ await navigator.clipboard.writeText(text);
106
+ setCopied(kind);
107
+ } catch {
108
+ setErrorMessage("Copying was blocked by the browser. Select the text and copy it by hand.");
109
+ }
110
+ }
111
+
112
+ const commandLine = issued ? buildDelegatedCommandLine({ email, agentLabel: issued.agentLabel, token: issued.token }) : "";
113
+
114
+ return (
115
+ <div data-news-desk-admin-panel="delegations">
116
+ <article className="news-desk-doctrine-card">
117
+ <div className="news-desk-doctrine-card__header">
118
+ <div>
119
+ <h3>Agent delegations</h3>
120
+ <span>Let one agent approve or reject References for you, for a short time</span>
121
+ </div>
122
+ </div>
123
+ <p className="news-desk-delegation-note">
124
+ A delegation is a token you hand to one named agent. While it lasts, that agent&apos;s approvals and rejections are recorded under your name and marked as relayed. You can revoke it at any moment.
125
+ </p>
126
+ <p className="news-desk-delegation-note" data-delegation-words-note>
127
+ The agent must pass your actual words, in quotes, as the instruction on every approval or rejection. Papyrus keeps those words with the record. An agent that paraphrases or invents them is not following this delegation.
128
+ </p>
129
+ <div className="news-desk-delegation-form">
130
+ <label className="news-desk-doctrine-card__field">
131
+ <span>Agent name</span>
132
+ <input value={agentLabelText} onChange={(event) => setAgentLabelText(event.target.value)} placeholder="curator-agent" autoComplete="off" disabled={disabled || busy} />
133
+ </label>
134
+ <label className="news-desk-doctrine-card__field">
135
+ <span>{`Hours (${MINIMUM_DELEGATION_HOURS} to ${MAXIMUM_DELEGATION_HOURS})`}</span>
136
+ <input value={hoursText} onChange={(event) => setHoursText(event.target.value)} inputMode="numeric" disabled={disabled || busy} />
137
+ </label>
138
+ <button type="button" className="news-desk-primary-action-button" data-delegation-create onClick={() => void handleCreate()} disabled={disabled || busy}>
139
+ {busy && !issued ? "Creating" : "Create delegation"}
140
+ </button>
141
+ </div>
142
+ {errorMessage ? <p className="news-desk-delegation-error" role="alert" data-delegation-error>{errorMessage}</p> : null}
143
+ </article>
144
+
145
+ {issued ? (
146
+ <article className="news-desk-delegation-token" data-delegation-token-box aria-live="polite">
147
+ <h3>Copy this token now</h3>
148
+ <p>This token is shown once. Papyrus does not keep it, and it cannot be shown again. If you lose it, revoke the delegation and create a new one.</p>
149
+ <div className="news-desk-delegation-token__row">
150
+ <code data-delegation-token>{issued.token}</code>
151
+ <button type="button" onClick={() => void copyText("token", issued.token)}>{copied === "token" ? "Copied" : "Copy token"}</button>
152
+ </div>
153
+ <p className="story-label">Command line for {issued.agentLabel}</p>
154
+ <div className="news-desk-delegation-token__row">
155
+ <code data-delegation-command>{commandLine}</code>
156
+ <button type="button" onClick={() => void copyText("command", commandLine)}>{copied === "command" ? "Copied" : "Copy command"}</button>
157
+ </div>
158
+ <div className="news-desk-doctrine-card__actions">
159
+ <button type="button" className="news-desk-primary-action-button" data-delegation-done onClick={() => { setIssued(null); setCopied(null); }}>Done</button>
160
+ </div>
161
+ </article>
162
+ ) : null}
163
+
164
+ <article className="news-desk-doctrine-card">
165
+ <div className="news-desk-doctrine-card__header">
166
+ <div>
167
+ <h3>Delegations</h3>
168
+ <span>Granted by you, newest first</span>
169
+ </div>
170
+ </div>
171
+ {delegations.length === 0 ? (
172
+ <p className="news-desk-delegation-note" data-delegation-empty>You have not granted any delegations.</p>
173
+ ) : (
174
+ <ul className="news-desk-delegation-list">
175
+ {delegations.map((entry) => {
176
+ const stateText = delegationExpiryText(entry.expiresAt, entry.revokedAt, nowMilliseconds);
177
+ const live = !entry.revokedAt && stateText !== "Expired";
178
+ return (
179
+ <li key={entry.delegationId} data-delegation-row={entry.delegationId} data-delegation-live={live || undefined}>
180
+ <div>
181
+ <strong>{entry.agentLabel}</strong>
182
+ <span>{`Granted by ${entry.grantorLabel ?? "you"}`}</span>
183
+ </div>
184
+ <span className="news-desk-delegation-state">{stateText}</span>
185
+ {live ? (
186
+ confirmingRevokeId === entry.delegationId ? (
187
+ <span className="news-desk-delegation-confirm">
188
+ <span>{`Stop ${entry.agentLabel} from curating for you?`}</span>
189
+ <button type="button" data-delegation-revoke-confirm onClick={() => void handleRevoke(entry.delegationId)} disabled={busy}>Revoke now</button>
190
+ <button type="button" onClick={() => setConfirmingRevokeId(null)} disabled={busy}>Keep</button>
191
+ </span>
192
+ ) : (
193
+ <button type="button" data-delegation-revoke onClick={() => setConfirmingRevokeId(entry.delegationId)} disabled={disabled || busy}>Revoke</button>
194
+ )
195
+ ) : null}
196
+ </li>
197
+ );
198
+ })}
199
+ </ul>
200
+ )}
201
+ </article>
202
+ </div>
203
+ );
204
+ }
@@ -1,45 +1,61 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useState } from "react";
4
- import { createPortal } from "react-dom";
3
+ import { useEffect } from "react";
5
4
 
6
5
  type Excerpt = { issueId: string; text: string };
7
6
 
8
- function CardExcerpt({ text }: { text: string }) {
9
- const [expanded, setExpanded] = useState(false);
10
- return <div className="board-card-excerpt" data-board-card-excerpt data-expanded={expanded}>
11
- <p>{text}</p>
12
- {text.length > 130 && <button aria-expanded={expanded} onClick={event => { event.stopPropagation(); setExpanded(open => !open); }} onKeyDown={event => event.stopPropagation()} type="button">{expanded ? "Less" : "More"}</button>}
13
- </div>;
7
+ const EXCERPT_TOGGLE_THRESHOLD = 130;
8
+
9
+ function buildExcerptElement(text: string): HTMLElement {
10
+ const container = document.createElement("div");
11
+ container.className = "board-card-excerpt";
12
+ container.dataset.boardCardExcerpt = "";
13
+ container.dataset.expanded = "false";
14
+ const paragraph = document.createElement("p");
15
+ paragraph.textContent = text;
16
+ container.append(paragraph);
17
+ if (text.length > EXCERPT_TOGGLE_THRESHOLD) {
18
+ const toggle = document.createElement("button");
19
+ toggle.type = "button";
20
+ toggle.textContent = "More";
21
+ toggle.setAttribute("aria-expanded", "false");
22
+ const flip = (event: Event) => {
23
+ event.stopPropagation();
24
+ const expanded = container.dataset.expanded !== "true";
25
+ container.dataset.expanded = String(expanded);
26
+ toggle.textContent = expanded ? "Less" : "More";
27
+ toggle.setAttribute("aria-expanded", String(expanded));
28
+ };
29
+ toggle.addEventListener("click", flip);
30
+ toggle.addEventListener("keydown", event => event.stopPropagation());
31
+ container.append(toggle);
32
+ }
33
+ return container;
14
34
  }
15
35
 
16
36
  /**
17
37
  * Adds a short expandable excerpt under each card's headline. The Kanbus card has no excerpt slot,
18
- * so each excerpt is rendered into the card's content block once the card is on the page.
38
+ * so each excerpt is written into the card's content block once the card is on the page.
19
39
  */
20
40
  export function BoardCardExcerpts({ excerpts, scope }: { excerpts: readonly Excerpt[]; scope: HTMLElement | null }) {
21
- const [hosts, setHosts] = useState<{ host: HTMLElement; text: string }[]>([]);
22
41
  useEffect(() => {
23
42
  if (!scope) return;
24
- const collect = () => {
25
- const found = excerpts.flatMap(({ issueId, text }) => {
26
- const card = Array.from(scope.querySelectorAll<HTMLElement>(".issue-card")).find(candidate => candidate.dataset.issueId === issueId);
27
- const content = card?.querySelector<HTMLElement>(".issue-card-content");
28
- if (!content) return [];
29
- let host = content.querySelector<HTMLElement>(":scope > [data-board-card-excerpt-host]");
30
- if (!host) {
31
- host = document.createElement("div");
32
- host.dataset.boardCardExcerptHost = "true";
33
- content.querySelector("h3")?.after(host);
34
- }
35
- return [{ host, text }];
36
- });
37
- setHosts(previous => previous.length === found.length && previous.every((entry, index) => entry.host === found[index].host && entry.text === found[index].text) ? previous : found);
43
+ const textByIssueId = new Map(excerpts.map(({ issueId, text }) => [issueId, text]));
44
+ const place = () => {
45
+ for (const card of Array.from(scope.querySelectorAll<HTMLElement>(".issue-card"))) {
46
+ const text = textByIssueId.get(card.dataset.issueId ?? "");
47
+ const content = card.querySelector<HTMLElement>(".issue-card-content");
48
+ if (!text || !content) continue;
49
+ const existing = content.querySelector<HTMLElement>(":scope > [data-board-card-excerpt]");
50
+ if (existing && existing.querySelector("p")?.textContent === text) continue;
51
+ existing?.remove();
52
+ content.querySelector("h3")?.after(buildExcerptElement(text));
53
+ }
38
54
  };
39
- collect();
40
- const observer = new MutationObserver(collect);
55
+ place();
56
+ const observer = new MutationObserver(place);
41
57
  observer.observe(scope, { childList: true, subtree: true });
42
58
  return () => observer.disconnect();
43
59
  }, [excerpts, scope]);
44
- return <>{hosts.map(({ host, text }, index) => createPortal(<CardExcerpt text={text} />, host, `${index}`))}</>;
60
+ return null;
45
61
  }
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import { generateClient } from "aws-amplify/data";
4
+ import { isLocalPreviewEnabled } from "../lib/local-preview";
5
+ import type { CurationDelegationRecord } from "../lib/curation-delegation-helpers";
6
+ import { configureAmplifyClient } from "./amplify-client-provider";
7
+
8
+ const USER_POOL_AUTH_MODE = "userPool";
9
+
10
+ export type CreatedCurationDelegation = CurationDelegationRecord & { token: string };
11
+
12
+ const CREATE_MUTATION = `
13
+ mutation CreateCurationDelegation($agentLabel: String!, $hours: Int) {
14
+ createCurationDelegation(agentLabel: $agentLabel, hours: $hours) {
15
+ delegationId token agentLabel grantorLabel expiresAt revokedAt
16
+ }
17
+ }
18
+ `;
19
+
20
+ const REVOKE_MUTATION = `
21
+ mutation RevokeCurationDelegation($delegationId: ID!) {
22
+ revokeCurationDelegation(delegationId: $delegationId) {
23
+ delegationId agentLabel grantorLabel expiresAt revokedAt
24
+ }
25
+ }
26
+ `;
27
+
28
+ const LIST_QUERY = `
29
+ query ListCurationDelegations {
30
+ listCurationDelegations {
31
+ delegationId agentLabel grantorLabel expiresAt revokedAt
32
+ }
33
+ }
34
+ `;
35
+
36
+ const previewDelegations: CurationDelegationRecord[] = [];
37
+
38
+ async function runGraphql<T>(query: string, variables: Record<string, unknown>): Promise<T> {
39
+ configureAmplifyClient();
40
+ const client = generateClient() as unknown as {
41
+ graphql: (options: Record<string, unknown>) => Promise<{ data?: T | null; errors?: Array<{ message?: string }> | null }>;
42
+ };
43
+ const response = await client.graphql({ query, variables, authMode: USER_POOL_AUTH_MODE });
44
+ if (response.errors?.length) throw Object.assign(new Error(response.errors[0]?.message ?? "Request failed."), { errors: response.errors });
45
+ if (!response.data) throw new Error("The server returned no result.");
46
+ return response.data;
47
+ }
48
+
49
+ export async function createCurationDelegation(agentLabel: string, hours: number): Promise<CreatedCurationDelegation> {
50
+ if (isLocalPreviewEnabled()) {
51
+ const delegationId = `curation-delegation-preview-${previewDelegations.length + 1}`;
52
+ const record: CurationDelegationRecord = {
53
+ delegationId,
54
+ agentLabel,
55
+ grantorLabel: "ryan@example.com",
56
+ expiresAt: new Date(Date.now() + hours * 3_600_000).toISOString(),
57
+ revokedAt: null,
58
+ };
59
+ previewDelegations.push(record);
60
+ return { ...record, token: `${delegationId}~preview-token-not-valid-anywhere` };
61
+ }
62
+ const data = await runGraphql<{ createCurationDelegation: CreatedCurationDelegation }>(CREATE_MUTATION, { agentLabel, hours });
63
+ return data.createCurationDelegation;
64
+ }
65
+
66
+ export async function revokeCurationDelegation(delegationId: string): Promise<CurationDelegationRecord> {
67
+ if (isLocalPreviewEnabled()) {
68
+ const record = previewDelegations.find((entry) => entry.delegationId === delegationId);
69
+ if (!record) throw new Error("That delegation was not found.");
70
+ record.revokedAt = record.revokedAt ?? new Date().toISOString();
71
+ return { ...record };
72
+ }
73
+ const data = await runGraphql<{ revokeCurationDelegation: CurationDelegationRecord }>(REVOKE_MUTATION, { delegationId });
74
+ return data.revokeCurationDelegation;
75
+ }
76
+
77
+ export async function listCurationDelegations(): Promise<CurationDelegationRecord[]> {
78
+ if (isLocalPreviewEnabled()) return previewDelegations.map((entry) => ({ ...entry })).reverse();
79
+ const data = await runGraphql<{ listCurationDelegations: CurationDelegationRecord[] }>(LIST_QUERY, {});
80
+ return data.listCurationDelegations ?? [];
81
+ }
@@ -3621,6 +3621,13 @@ export async function loadCyclotronStatus(cyclotronId: string): Promise<import("
3621
3621
  return latestCyclotronStatus(payloads.map((payload) => ({ json: payload.json, updatedAt: payload.attachment.updatedAt })));
3622
3622
  }
3623
3623
 
3624
+ /** The relevance cyclotron's latest cyclotron-shadow-agreement/v1 payload: suggestions compared with later human decisions. */
3625
+ export async function loadCyclotronShadowAgreement(cyclotronId: string): Promise<import("../lib/cyclotron-suggestions").ShadowAgreement | null> {
3626
+ const { latestShadowAgreement, shadowAgreementPayloadId } = await import("../lib/cyclotron-suggestions");
3627
+ const payloads = await loadModelPayloadsForOwner("knowledgeRawPayload", shadowAgreementPayloadId(cyclotronId), ["raw_payload"]);
3628
+ return latestShadowAgreement(payloads.map((payload) => ({ json: payload.json, updatedAt: payload.attachment.updatedAt })));
3629
+ }
3630
+
3624
3631
  /** Ask the next decide-relevance sweep to set or clear a manual review rate. */
3625
3632
  export async function requestCyclotronReviewRate(
3626
3633
  cyclotronId: string,
@@ -274,3 +274,25 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
274
274
  .organizer-account-chooser li button small,.organizer-account-chooser-note{font-size:12px;color:var(--org-ink);margin:0}
275
275
  .organizer-account-chooser-heading{margin:0;font:600 11px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:.07em;text-transform:uppercase}
276
276
  .organizer-account-chooser-link{color:var(--org-ink);text-decoration:underline;text-underline-offset:3px}
277
+ [data-reference-cyclotron-status] .cyclotron-status[data-variant]{border-radius:0;background:transparent;border-top:1px solid var(--org-ink);border-bottom:1px solid var(--org-ink);padding-left:0;padding-right:0}
278
+ [data-reference-cyclotron-status] :is(.cyclotron-status-note,.cyclotron-status-definition,.cyclotron-status-header p,.cyclotron-status-override){color:var(--org-ink)}
279
+ [data-reference-cyclotron-status] .cyclotron-status[data-variant="strip"]>span+span::before{color:var(--org-ink)}
280
+ [data-reference-cyclotron-status] .cyclotron-status-bar{border-radius:0;background:transparent;border:1px solid var(--org-ink)}
281
+ [data-reference-cyclotron-status] .cyclotron-status-bar>span{opacity:1}
282
+ [data-reference-cyclotron-status] :is(.cyclotron-review-secondary,.cyclotron-review-choice,.cyclotron-review-submit){border-radius:0;opacity:1}
283
+ .news-desk-columns--administration .news-desk-delegation-note{margin:0;color:var(--administration-strong);max-width:68ch}
284
+ .news-desk-columns--administration .news-desk-delegation-form{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr) auto;gap:12px;align-items:end}
285
+ .news-desk-columns--administration .news-desk-delegation-error{margin:0;padding:8px 0 8px 12px;border-left:3px solid var(--org-ink);color:var(--administration-strong);font-weight:600}
286
+ .news-desk-columns--administration .news-desk-delegation-token{display:grid;gap:12px;min-width:0;padding:16px;border:2px solid var(--org-ink)}
287
+ .news-desk-columns--administration .news-desk-delegation-token h3{margin:0;font-size:20px}
288
+ .news-desk-columns--administration .news-desk-delegation-token p{margin:0}
289
+ .news-desk-columns--administration .news-desk-delegation-token__row{display:flex;flex-wrap:wrap;gap:8px;align-items:stretch}
290
+ .news-desk-columns--administration .news-desk-delegation-token__row code{flex:1 1 280px;min-width:0;padding:8px 10px;border:1px solid var(--org-line);font:500 13px/1.5 ui-monospace,Menlo,monospace;overflow-wrap:anywhere;color:var(--administration-strong)}
291
+ .news-desk-columns--administration .news-desk-delegation-token__row button,.news-desk-columns--administration .news-desk-delegation-list button{min-height:36px;padding:4px 12px;border:1px solid var(--org-line);background:transparent;color:var(--administration-strong);font:600 12px/1.2 Newsroom UI,system-ui,sans-serif;letter-spacing:.05em;text-transform:uppercase;cursor:pointer}
292
+ .news-desk-columns--administration .news-desk-delegation-list{list-style:none;margin:0;padding:0;display:grid}
293
+ .news-desk-columns--administration .news-desk-delegation-list li{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:8px 16px;align-items:center;padding:12px 0;border-top:1px solid var(--org-line)}
294
+ .news-desk-columns--administration .news-desk-delegation-list li>div{display:grid;gap:2px}
295
+ .news-desk-columns--administration .news-desk-delegation-list li[data-delegation-live=true] .news-desk-delegation-state{font-weight:700;text-decoration:underline;text-underline-offset:4px}
296
+ .news-desk-columns--administration .news-desk-delegation-state{font:600 12px/1.4 Newsroom UI,system-ui,sans-serif;color:var(--administration-strong)}
297
+ .news-desk-columns--administration .news-desk-delegation-confirm{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
298
+ @container (max-width:640px){.news-desk-columns--administration .news-desk-delegation-form{grid-template-columns:minmax(0,1fr)}.news-desk-columns--administration .news-desk-delegation-list li{grid-template-columns:minmax(0,1fr)}}
@@ -49,6 +49,15 @@ import { normalizeReferenceCurationStatus } from "../lib/reference-policy";
49
49
  import { newsroomListRowClassName } from "../lib/newsroom-list-selection";
50
50
  import { ReferenceSourcePreview } from "./reference-source-preview";
51
51
  import { ReferenceRelevanceReview } from "./reference-relevance-review";
52
+ import {
53
+ CountsAsTrainingLabelNote,
54
+ DelegatedCurationNote,
55
+ ReferenceSuggestionBlock,
56
+ ReferenceSuggestionProvenance,
57
+ ReferenceSuggestionRowLine,
58
+ } from "./reference-cyclotron-suggestion";
59
+ import { ReferenceLiveAgreement } from "./reference-live-agreement";
60
+ import type { DelegatedCuration, ShadowAgreement } from "../lib/cyclotron-suggestions";
52
61
  import { ReferenceCyclotronPriming, type PrimingFormInput } from "./reference-cyclotron-priming";
53
62
  import { ReferenceCyclotronStatus, type ReviewRateChoice } from "./reference-cyclotron-status";
54
63
  import type { PrimingRequestView } from "../lib/relevance-status";
@@ -75,6 +84,12 @@ type NewsroomReferencesViewProps = {
75
84
  demo?: boolean;
76
85
  /** The relevance cyclotron's current decisions, keyed by reference lineage. */
77
86
  decisions?: Map<string, RelevanceDecisionView>;
87
+ /** Reference lineage ids whose latest curation Message was made by a person. */
88
+ humanCuratedReferenceLineageIds?: Set<string>;
89
+ /** Delegated curations by reference lineage. */
90
+ delegatedCurations?: Map<string, DelegatedCuration>;
91
+ /** The live-agreement payload; the strip is hidden without it. */
92
+ shadowAgreement?: ShadowAgreement | null;
78
93
  disabled?: boolean;
79
94
  onRelevanceReview?: (reference: ReferenceRecord, curation: RelevanceCuration) => void;
80
95
  initialReferenceLineageId?: string | null;
@@ -212,17 +227,14 @@ function useReferencePreviewAttachments(
212
227
  return previewAttachments;
213
228
  }
214
229
 
215
- function decisionSummary(decision: RelevanceDecisionView): string {
216
- const sure = decision.confidence == null ? "confidence unknown" : `${Math.round(decision.confidence * 100)}% sure`;
217
- return `${decision.label} · ${sure}${decision.version != null ? ` · version ${decision.version}` : ""}`;
218
- }
219
-
220
230
  function needsReview(reference: ReferenceRecord, decision: RelevanceDecisionView | undefined): boolean {
221
231
  return Boolean(decision?.reviewRecommended)
222
232
  && normalizeReferenceCurationStatus(reference.curationStatus) === "pending";
223
233
  }
224
234
 
225
235
  function ReferenceDetailPanel({
236
+ countsAsTrainingLabel,
237
+ delegated,
226
238
  decision,
227
239
  demo,
228
240
  disabled,
@@ -232,6 +244,8 @@ function ReferenceDetailPanel({
232
244
  reference,
233
245
  onClose,
234
246
  }: {
247
+ countsAsTrainingLabel?: boolean;
248
+ delegated?: DelegatedCuration;
235
249
  decision?: RelevanceDecisionView;
236
250
  demo?: boolean;
237
251
  disabled?: boolean;
@@ -267,25 +281,39 @@ function ReferenceDetailPanel({
267
281
  ) : null}
268
282
  </div>
269
283
 
270
- {decision && status === "pending" && onRelevanceReview ? (
271
- <ReferenceRelevanceReview
272
- decision={decision}
273
- disabled={disabled}
274
- onCurate={onRelevanceReview}
275
- referenceLineageId={lineageId}
276
- />
277
- ) : decision ? (
278
- <p className="text-sm text-muted-foreground" data-reference-relevance-decision={lineageId}>
279
- Cyclotron decision: {decisionSummary(decision)}
280
- </p>
284
+ {decision ? (
285
+ <div className="space-y-3" data-reference-relevance-decision={lineageId}>
286
+ <ReferenceSuggestionBlock
287
+ decision={decision}
288
+ disabled={disabled}
289
+ onCurate={onRelevanceReview}
290
+ referenceLineageId={lineageId}
291
+ settled={status !== "pending"}
292
+ statusLabel={referenceStatusLabel(status)}
293
+ />
294
+ <ReferenceSuggestionProvenance decision={decision} />
295
+ {status === "pending" && onRelevanceReview ? (
296
+ <details className="text-sm text-foreground">
297
+ <summary className="cursor-pointer font-semibold underline-offset-4 hover:underline">Review with a reason</summary>
298
+ <div className="mt-2">
299
+ <ReferenceRelevanceReview
300
+ decision={decision}
301
+ disabled={disabled}
302
+ onCurate={onRelevanceReview}
303
+ referenceLineageId={lineageId}
304
+ />
305
+ </div>
306
+ </details>
307
+ ) : null}
308
+ </div>
281
309
  ) : null}
310
+ {delegated ? <DelegatedCurationNote delegated={delegated} referenceLineageId={lineageId} /> : countsAsTrainingLabel ? <CountsAsTrainingLabelNote referenceLineageId={lineageId} /> : null}
282
311
 
283
312
  <div
284
313
  className="flex flex-wrap items-center gap-2"
285
314
  data-news-desk-reference-curation-cluster
286
315
  data-reference-curation-status={status}
287
316
  >
288
- {decision && status === "pending" && onRelevanceReview ? null : <>
289
317
  <Button
290
318
  aria-label="Accept reference"
291
319
  aria-pressed={status === "accepted"}
@@ -312,7 +340,6 @@ function ReferenceDetailPanel({
312
340
  <ThumbsDownIcon className="size-4" />
313
341
  Reject
314
342
  </Button>
315
- </>}
316
343
  <Button
317
344
  aria-label="Archive reference"
318
345
  disabled={archiveDisabled}
@@ -409,6 +436,9 @@ export function NewsroomReferencesView({
409
436
  cyclotronStatus,
410
437
  cyclotronPriming,
411
438
  decisions,
439
+ humanCuratedReferenceLineageIds,
440
+ delegatedCurations,
441
+ shadowAgreement = null,
412
442
  onRequestReviewRate,
413
443
  demo = false,
414
444
  disabled = false,
@@ -523,6 +553,7 @@ export function NewsroomReferencesView({
523
553
  Review source intake before it becomes accepted evidence.
524
554
  </p>
525
555
  </div>
556
+ <ReferenceLiveAgreement agreement={shadowAgreement} />
526
557
  <ReferenceCurationControls disabled={disabled} />
527
558
  {priming && !disabled ? <ReferencePriming {...priming} /> : null}
528
559
  {cyclotronPriming && !disabled ? (
@@ -595,12 +626,12 @@ export function NewsroomReferencesView({
595
626
  </div>
596
627
  <p className="mt-2 text-xs text-muted-foreground">{formatReferenceListDate(reference)}</p>
597
628
  {decision ? (
598
- <p className="mt-1 flex flex-wrap items-center gap-2 text-xs text-muted-foreground" data-reference-relevance-row={lineageId}>
599
- <span>Decision: {decisionSummary(decision)}</span>
629
+ <div className="flex flex-wrap items-center gap-2" data-reference-relevance-row={lineageId}>
630
+ <ReferenceSuggestionRowLine decision={decision} referenceLineageId={lineageId} />
600
631
  {needsReview(reference, decision) ? (
601
632
  <Badge variant="outline"><span aria-hidden="true">●</span> Needs review</Badge>
602
633
  ) : null}
603
- </p>
634
+ </div>
604
635
  ) : null}
605
636
  </button>
606
637
  );
@@ -650,6 +681,8 @@ export function NewsroomReferencesView({
650
681
  <ScrollArea className="min-h-0 flex-1">
651
682
  {selectedReference ? (
652
683
  <ReferenceDetailPanel
684
+ countsAsTrainingLabel={humanCuratedReferenceLineageIds?.has(referenceLineageId(selectedReference))}
685
+ delegated={delegatedCurations?.get(referenceLineageId(selectedReference))}
653
686
  decision={decisions?.get(referenceLineageId(selectedReference))}
654
687
  demo={demo}
655
688
  disabled={disabled}
@@ -681,6 +714,8 @@ export function NewsroomReferencesView({
681
714
  {selectedReference ? (
682
715
  <ScrollArea className="max-h-[75dvh]">
683
716
  <ReferenceDetailPanel
717
+ countsAsTrainingLabel={humanCuratedReferenceLineageIds?.has(referenceLineageId(selectedReference))}
718
+ delegated={delegatedCurations?.get(referenceLineageId(selectedReference))}
684
719
  decision={decisions?.get(referenceLineageId(selectedReference))}
685
720
  demo={demo}
686
721
  disabled={disabled}
@@ -48,14 +48,14 @@ export function ReferenceCyclotronStatus({
48
48
  <section aria-label="Relevance cyclotron" className="space-y-2" data-reference-cyclotron-status>
49
49
  <CyclotronStatusView status={status} />
50
50
  {stale ? (
51
- <p className="text-xs text-muted-foreground" data-reference-cyclotron-status-stale>
51
+ <p className="text-xs text-foreground" data-reference-cyclotron-status-stale>
52
52
  <span aria-hidden="true">⚠ </span>
53
53
  Snapshot from {Math.round(ageHours)} hours ago; the next sweep refreshes it.
54
54
  </p>
55
55
  ) : null}
56
56
  <button
57
57
  aria-expanded={open}
58
- className="text-xs text-muted-foreground underline underline-offset-4"
58
+ className="text-xs text-foreground underline underline-offset-4"
59
59
  onClick={() => setOpen((value) => !value)}
60
60
  type="button"
61
61
  >
@@ -75,7 +75,7 @@ export function ReferenceCyclotronStatus({
75
75
  }}
76
76
  >
77
77
  <label className="flex flex-col gap-1">
78
- <span className="text-xs text-muted-foreground">Review rate for confident decisions</span>
78
+ <span className="text-xs text-foreground">Review rate for confident decisions</span>
79
79
  <select aria-label="Manual review rate" onChange={(event) => setRate(Number(event.target.value))} value={rate}>
80
80
  {RATE_CHOICES.map((choice) => (
81
81
  <option key={choice} value={choice}>{Math.round(choice * 100)}%</option>
@@ -83,7 +83,7 @@ export function ReferenceCyclotronStatus({
83
83
  </select>
84
84
  </label>
85
85
  <label className="flex flex-col gap-1">
86
- <span className="text-xs text-muted-foreground">For</span>
86
+ <span className="text-xs text-foreground">For</span>
87
87
  <select aria-label="How long the manual rate lasts" onChange={(event) => setDays(Number(event.target.value))} value={days}>
88
88
  {DAY_CHOICES.map((choice) => (
89
89
  <option key={choice} value={choice}>{choice} day{choice === 1 ? "" : "s"}</option>
@@ -96,7 +96,7 @@ export function ReferenceCyclotronStatus({
96
96
  Clear manual rate
97
97
  </button>
98
98
  ) : null}
99
- <p className="basis-full text-xs text-muted-foreground">
99
+ <p className="basis-full text-xs text-foreground">
100
100
  Low-confidence decisions and the random audit share are always reviewed.
101
101
  </p>
102
102
  {message ? <p className="basis-full text-xs" role="status">{message}</p> : null}