@anthusai/papyrus 5.43.0-next.1 → 5.43.0-next.10
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.
- package/amplify/functions/category-action/handler.ts +2 -1
- package/amplify/functions/monitoring-view/schema.js +16 -0
- package/amplify/functions/monitoring-view/schema.ts +16 -0
- package/amplify/functions/procedure-action/handler.ts +2 -10
- package/amplify/functions/proposal-actions/proposals.py +20 -3
- package/amplify/plugins/reader-steering/function/handler.py +1 -3
- package/amplify/plugins/reader-steering/function/moderation.py +19 -0
- package/bin/papyrus-infra.mjs +21 -1
- package/components/dev-sandbox-editor-sign-in.tsx +2 -2
- package/components/monitoring-overview-view.tsx +133 -21
- package/components/monitoring-panel.tsx +64 -4
- package/components/news-desk-client-provider.tsx +8 -2
- package/components/news-desk-taxonomy-client.ts +2 -1
- package/components/newsroom-ops-overview.tsx +6 -1
- package/components/newsroom-references-view.tsx +2 -4
- package/components/notification-center.tsx +345 -63
- package/components/papyrus-console-shell.tsx +2 -1
- package/components/proposals-panel.tsx +21 -24
- package/components/proposals-view.tsx +4 -2
- package/components/reference-review-card.tsx +80 -63
- package/infra/amplify-app-shell.js +18 -2
- package/infra/amplify-app-shell.ts +18 -2
- package/infra/chat-stack.js +222 -0
- package/infra/chat-stack.ts +250 -0
- package/infra/chat.test.js +142 -0
- package/infra/chat.test.ts +151 -0
- package/infra/index.js +1 -0
- package/infra/site-config.js +58 -1
- package/infra/site-config.ts +78 -1
- package/lib/monitoring-client.ts +29 -2
- package/lib/monitoring-overview.ts +46 -1
- package/lib/monitoring-paging.ts +58 -0
- package/lib/newsroom-chat-identity.ts +2 -1
- package/lib/newsroom-organizer-nav.ts +1 -0
- package/lib/proposals-client.ts +29 -0
- package/lib/reader-subject.ts +3 -2
- package/lib/review-client.ts +38 -1
- package/lib/review-drafts.ts +231 -0
- package/lib/review-fixture.ts +36 -2
- package/lib/review-requests.ts +64 -24
- package/lib/staff-groups.ts +23 -0
- package/lib/staging-gate.ts +2 -2
- package/package.json +5 -1
- package/routes.manifest.json +1 -1
|
@@ -3,13 +3,22 @@
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import { useSearchParams } from "next/navigation";
|
|
5
5
|
import {
|
|
6
|
+
decideMonitoringEmailSubmission,
|
|
6
7
|
importMonitoringEmailCandidates,
|
|
7
8
|
loadMonitoringEmailSubmission,
|
|
8
9
|
loadMonitoringOverview,
|
|
10
|
+
loadMonitoringPage,
|
|
9
11
|
resolveMonitoringEmailArticleLink,
|
|
10
12
|
} from "../lib/monitoring-client";
|
|
11
13
|
import { FIXTURE_ALL_PIPELINE_OVERVIEW, FIXTURE_MONITORING_OVERVIEW } from "../lib/monitoring-overview-fixture";
|
|
12
|
-
import
|
|
14
|
+
import { withLoadedPages } from "../lib/monitoring-paging";
|
|
15
|
+
import type {
|
|
16
|
+
EmailReferenceImportItem,
|
|
17
|
+
MonitoringEmailDetails,
|
|
18
|
+
MonitoringListName,
|
|
19
|
+
MonitoringResult,
|
|
20
|
+
} from "../lib/monitoring-overview";
|
|
21
|
+
import type { MonitoringLoadedPages } from "../lib/monitoring-paging";
|
|
13
22
|
import { EmailSubmissions, MonitoringOverviewView, type MonitoringMode } from "./monitoring-overview-view";
|
|
14
23
|
|
|
15
24
|
const REFRESH_MILLISECONDS = 60_000;
|
|
@@ -39,7 +48,8 @@ function MonitoringEmailReceipt({ receiptId, isDemo }: { receiptId: string; isDe
|
|
|
39
48
|
return <div data-news-desk-admin-panel="monitoring">
|
|
40
49
|
{detail === null ? <p role="status">Loading email submission…</p> :
|
|
41
50
|
<EmailSubmissions items={"submission" in detail ? [detail.submission] : []} detail={detail}
|
|
42
|
-
onResolveArticleLink={resolveMonitoringEmailArticleLink} onImportCandidates={importMonitoringEmailCandidates}
|
|
51
|
+
onResolveArticleLink={resolveMonitoringEmailArticleLink} onImportCandidates={importMonitoringEmailCandidates}
|
|
52
|
+
onDecide={decideMonitoringEmailSubmission} />}
|
|
43
53
|
</div>;
|
|
44
54
|
}
|
|
45
55
|
|
|
@@ -48,6 +58,18 @@ function MonitoringOverviewPanel({ isAdmin, isDemo }: { isAdmin: boolean; isDemo
|
|
|
48
58
|
const [mode, setMode] = useState<MonitoringMode>("publication");
|
|
49
59
|
const [result, setResult] = useState<MonitoringResult | null>(null);
|
|
50
60
|
const [emailDetail, setEmailDetail] = useState<MonitoringEmailDetails | null>(null);
|
|
61
|
+
// Pages the editor loaded beyond the newest page of each list. The 60 second refresh re-reads only the newest
|
|
62
|
+
// page, so what was loaded stays on screen.
|
|
63
|
+
const [loaded, setLoaded] = useState<MonitoringLoadedPages>({});
|
|
64
|
+
const [heldOnly, setHeldOnly] = useState(false);
|
|
65
|
+
const [held, setHeld] = useState<MonitoringLoadedPages["emailSubmissions"] | null>(null);
|
|
66
|
+
const [loadingList, setLoadingList] = useState<MonitoringListName | null>(null);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
setLoaded({});
|
|
70
|
+
setHeldOnly(false);
|
|
71
|
+
setHeld(null);
|
|
72
|
+
}, [mode]);
|
|
51
73
|
|
|
52
74
|
useEffect(() => {
|
|
53
75
|
if (isDemo) {
|
|
@@ -78,13 +100,51 @@ function MonitoringOverviewPanel({ isAdmin, isDemo }: { isAdmin: boolean; isDemo
|
|
|
78
100
|
setEmailDetail(next);
|
|
79
101
|
};
|
|
80
102
|
|
|
103
|
+
// Appends the next page of a list, or (held filter) the next page of held messages.
|
|
104
|
+
const loadMore = async (list: MonitoringListName) => {
|
|
105
|
+
if (isDemo || !result || !("scope" in result) || loadingList) return;
|
|
106
|
+
const current = withLoadedPages(result as never, loaded, heldOnly ? held : null);
|
|
107
|
+
const cursor = current.cursors?.[list];
|
|
108
|
+
if (!cursor) return;
|
|
109
|
+
setLoadingList(list);
|
|
110
|
+
try {
|
|
111
|
+
const page = await loadMonitoringPage(list, cursor, mode, heldOnly && list === "emailSubmissions");
|
|
112
|
+
if (!("rows" in page)) return;
|
|
113
|
+
if (heldOnly && list === "emailSubmissions") {
|
|
114
|
+
setHeld((existing) => ({ rows: [...(existing?.rows ?? []), ...page.rows] as never, cursor: page.cursor }));
|
|
115
|
+
} else {
|
|
116
|
+
setLoaded((existing) => ({ ...existing, [list]: { rows: [...(existing[list]?.rows ?? []), ...page.rows], cursor: page.cursor } }) as MonitoringLoadedPages);
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
setLoadingList(null);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Held for review restarts the email list at its newest held message, however old.
|
|
124
|
+
const changeHeldOnly = async (next: boolean) => {
|
|
125
|
+
setHeldOnly(next);
|
|
126
|
+
setHeld(null);
|
|
127
|
+
if (!next || isDemo) return;
|
|
128
|
+
setLoadingList("emailSubmissions");
|
|
129
|
+
try {
|
|
130
|
+
const page = await loadMonitoringPage("emailSubmissions", null, mode, true);
|
|
131
|
+
if ("rows" in page) setHeld({ rows: page.rows as never, cursor: page.cursor });
|
|
132
|
+
else setHeldOnly(false);
|
|
133
|
+
} finally {
|
|
134
|
+
setLoadingList(null);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const shown = result && "scope" in result ? withLoadedPages(result as never, loaded, heldOnly ? held ?? { rows: [], cursor: null } : null) : result;
|
|
139
|
+
|
|
81
140
|
const resolveEmailArticleLink = (receiptId: string, candidateIndex: number) =>
|
|
82
141
|
resolveMonitoringEmailArticleLink(receiptId, candidateIndex);
|
|
83
142
|
|
|
84
143
|
const importEmailCandidates = (receiptId: string, candidates: EmailReferenceImportItem[]) =>
|
|
85
144
|
importMonitoringEmailCandidates(receiptId, candidates);
|
|
86
145
|
|
|
87
|
-
return <MonitoringOverviewView result={
|
|
146
|
+
return <MonitoringOverviewView result={shown} canViewAll={isAdmin} mode={mode} onModeChange={setMode} demo={isDemo}
|
|
88
147
|
emailDetail={emailDetail} onEmailOpen={isDemo ? () => {} : openEmail} onResolveArticleLink={isDemo ? undefined : resolveEmailArticleLink}
|
|
89
|
-
onImportCandidates={importEmailCandidates}
|
|
148
|
+
onImportCandidates={importEmailCandidates} onDecide={isDemo ? undefined : decideMonitoringEmailSubmission}
|
|
149
|
+
paging={isDemo ? undefined : { onLoadMore: (list) => void loadMore(list), loadingList, heldOnly, onHeldOnlyChange: (next) => void changeHeldOnly(next) }} />;
|
|
90
150
|
}
|
|
@@ -113,7 +113,8 @@ export function NewsDeskClientProvider({
|
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
115
115
|
if (isAuthorizationError(error)) {
|
|
116
|
-
|
|
116
|
+
// The session already has editor rights, so the backend refusing a read is not a missing role. Say what was refused.
|
|
117
|
+
setShell((current) => resolveDashboardFailure(current, access.auth, refusedReadMessage(error)));
|
|
117
118
|
return;
|
|
118
119
|
}
|
|
119
120
|
setShell((current) => resolveDashboardFailure(
|
|
@@ -215,9 +216,14 @@ function isAuthorizationError(error: unknown): boolean {
|
|
|
215
216
|
return /not authorized|unauthorized|access denied|forbidden/i.test(message);
|
|
216
217
|
}
|
|
217
218
|
|
|
219
|
+
function refusedReadMessage(error: unknown): string {
|
|
220
|
+
const detail = error instanceof Error ? error.message : String(error ?? "");
|
|
221
|
+
return `The Newsroom backend refused a read for this signed-in account (${detail}). The account has editor or admin rights, so this is a backend authorization rule, not a missing role.`;
|
|
222
|
+
}
|
|
223
|
+
|
|
218
224
|
function resolvePrivateReadFailure(state: NewsDeskShellState, auth: NewsDeskShellState["auth"], error: unknown, fallback: string): NewsDeskShellState {
|
|
219
225
|
if (isUnauthenticatedError(error)) return resolveSignedOut(state, { status: "signedOut", label: "Signed out" });
|
|
220
|
-
if (isAuthorizationError(error)) return
|
|
226
|
+
if (isAuthorizationError(error)) return resolveDashboardFailure(state, auth, refusedReadMessage(error));
|
|
221
227
|
return resolveDashboardFailure(state, auth, error instanceof Error ? error.message : fallback);
|
|
222
228
|
}
|
|
223
229
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { loadReferenceReviewDetails } from "../lib/reference-review-details";
|
|
4
|
+
import { hasEditorRights } from "../lib/staff-groups";
|
|
4
5
|
|
|
5
6
|
import { isEditorialWorkAssignment } from "../lib/newsroom-assignments";
|
|
6
7
|
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
@@ -838,7 +839,7 @@ export async function loadEditorResolvedAccessState(): Promise<EditorAccessState
|
|
|
838
839
|
if (!refreshed.hasSession || refreshed.auth.status === "signedOut") {
|
|
839
840
|
return { status: "signedOut", isEditor: false, isAdmin: false, auth: refreshed.auth, error: null };
|
|
840
841
|
}
|
|
841
|
-
const isEditor = refreshed.groups
|
|
842
|
+
const isEditor = hasEditorRights(refreshed.groups);
|
|
842
843
|
if (!isEditor) {
|
|
843
844
|
return {
|
|
844
845
|
status: "forbidden",
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { newsroomInboxRowClassName } from "../lib/newsroom-list-selection";
|
|
25
25
|
import type { RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
26
26
|
import { NotificationCenter } from "./notification-center";
|
|
27
|
+
import { CyclotronRow, predictionFromDecision } from "./reference-review-card";
|
|
27
28
|
|
|
28
29
|
type NewsroomOpsOverviewProps = {
|
|
29
30
|
assignments: AssignmentRecord[];
|
|
@@ -105,9 +106,13 @@ export function NewsroomOpsOverview({
|
|
|
105
106
|
href={href}
|
|
106
107
|
key={lineageId}
|
|
107
108
|
>
|
|
108
|
-
<div className="min-w-0 flex-1">
|
|
109
|
+
<div className="min-w-0 flex-1 space-y-1">
|
|
109
110
|
<p className="truncate text-sm font-medium text-foreground">{referenceDisplayTitle(reference)}</p>
|
|
110
111
|
<p className="text-xs text-muted-foreground">Review source intake · {formatReferenceListDate(reference)}</p>
|
|
112
|
+
<div data-needs-attention-suggestion={lineageId}>
|
|
113
|
+
<CyclotronRow prediction={decisions.get(lineageId) ? predictionFromDecision(decisions.get(lineageId)!) : { state: "none" }} />
|
|
114
|
+
</div>
|
|
115
|
+
<p className="text-xs font-medium text-foreground">Open it to Accept or Reject in the review queue</p>
|
|
111
116
|
</div>
|
|
112
117
|
<Badge className="mt-0.5 shrink-0" variant="outline">Pending</Badge>
|
|
113
118
|
</Link>
|
|
@@ -44,6 +44,7 @@ import { newsroomListRowClassName } from "../lib/newsroom-list-selection";
|
|
|
44
44
|
import { ReferenceSourcePreview } from "./reference-source-preview";
|
|
45
45
|
import {
|
|
46
46
|
ReviewCard,
|
|
47
|
+
predictionFromDecision,
|
|
47
48
|
ReviewQueueBar,
|
|
48
49
|
useQueueGestures,
|
|
49
50
|
type CardPrediction,
|
|
@@ -269,10 +270,7 @@ function summaryOf(reference: ReferenceRecord): string | null {
|
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
function cardPrediction(decision: RelevanceDecisionView | undefined, predictionState: PredictionState): CardPrediction {
|
|
272
|
-
if (decision)
|
|
273
|
-
const positive = decision.positiveLabel ?? "include";
|
|
274
|
-
return { state: "ready", choice: decision.label === positive ? "accept" : "reject", confidence: decision.confidence };
|
|
275
|
-
}
|
|
273
|
+
if (decision) return predictionFromDecision(decision);
|
|
276
274
|
if (predictionState === "loading") return { state: "loading" };
|
|
277
275
|
if (predictionState === "error") return { state: "error" };
|
|
278
276
|
return { state: "none" };
|