@anthusai/papyrus 5.43.0-next.7 → 5.43.0-next.8

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.
@@ -3,6 +3,7 @@
3
3
  * papyrus-infra synth --site infra/site.json [--out cdk.out] [--account A] [--region R]
4
4
  * papyrus-infra synth --account-stack github-oidc [--out cdk.out] [--account A] [--region R]
5
5
  * papyrus-infra print-buildspec --site infra/site.json --app cms-production|cms-staging|reader
6
+ * papyrus-infra synth --site infra/site.json --part chat [--chat-lambdas DIR] [--out cdk.out] (the private chat data stack)
6
7
  * papyrus-infra deploy --site infra/site.json [--only-branch main] [--only-app-id ID] [--out cdk.out] [--account A] [--region R]
7
8
  * Synthesizes the app-shell stack for a publication from its own site.json,
8
9
  * prints one of the generated Amplify build specs, or deploys the stack from
@@ -16,6 +17,7 @@ import { resolveCdkEnvironment } from "../src/cdk-environment.mjs";
16
17
 
17
18
  const USAGE = [
18
19
  "Usage: papyrus-infra synth --site infra/site.json [--out cdk.out] [--account A] [--region R]",
20
+ " papyrus-infra synth --site infra/site.json --part chat [--chat-lambdas DIR] [--out cdk.out] [--account A] [--region R]",
19
21
  " papyrus-infra synth --account-stack github-oidc [--out cdk.out] [--account A] [--region R]",
20
22
  " papyrus-infra print-buildspec --site infra/site.json --app cms-production|cms-staging|reader",
21
23
  " papyrus-infra deploy-edge --stack NAME --template FILE [--adoption-template FILE --adopt-resources FILE] [--parameter KEY=VALUE]... [--delete-function NAME] [--only-branch main] [--only-app-id ID] [--region R]",
@@ -186,8 +188,9 @@ if (command === "deploy") {
186
188
  }
187
189
 
188
190
  let AmplifyAppShellStack;
191
+ let PapyrusChatStack;
189
192
  try {
190
- ({ AmplifyAppShellStack } = await import("../infra/index.js"));
193
+ ({ AmplifyAppShellStack, PapyrusChatStack } = await import("../infra/index.js"));
191
194
  } catch (error) {
192
195
  console.error(`papyrus-infra: ${error instanceof Error ? error.message : error}`);
193
196
  process.exit(1);
@@ -195,6 +198,23 @@ try {
195
198
 
196
199
  const { App } = createRequire(path.join(process.cwd(), "noop.js"))("aws-cdk-lib");
197
200
  const app = new App({ outdir: path.resolve(opt("out", "cdk.out")) });
201
+ if (opt("part", "shell") === "chat") {
202
+ // The site's private newsroom chat data stack (docs/private-newsroom-chat-per-site.md); deployed before the app shell enables chat.
203
+ if (!config.chat?.enabled) {
204
+ console.error(`papyrus-infra: site ${config.siteId} has no enabled chat block`);
205
+ process.exit(1);
206
+ }
207
+ const lambdaAssetDirectory = opt("chat-lambdas");
208
+ const chatStack = new PapyrusChatStack(app, {
209
+ config,
210
+ env: resolveEnvironmentOrExit(),
211
+ lambdaAssetDirectory: lambdaAssetDirectory ? path.resolve(lambdaAssetDirectory) : undefined,
212
+ description: `Private Papyrus newsroom chat data plane for ${config.siteId}.`,
213
+ });
214
+ app.synth();
215
+ console.log(`synthesized ${chatStack.stackName} -> ${path.resolve(opt("out", "cdk.out"))}`);
216
+ process.exit(0);
217
+ }
198
218
  new AmplifyAppShellStack(app, `AmplifyAppShell-${config.siteId}`, config, {
199
219
  env: resolveEnvironmentOrExit(),
200
220
  stackName: resolveStackName(config),
@@ -139,7 +139,7 @@ export function EmailSubmissions({ items, detail, onOpen, onResolveArticleLink,
139
139
  if (!isCurrentEmailSubmissionAction(activeReceiptId.current, requestReceiptId)) return;
140
140
  if (!("url" in result)) {
141
141
  const reason = "reason" in result && typeof result.reason === "string"
142
- && /^(?:redirect_[a-z_]+|resolution_record_invalid|resolution_not_recorded)$/.test(result.reason)
142
+ && /^(?:redirect_[a-z_]+|resolution_record_invalid|resolution_not_recorded|invalid_url|not_public|dns|timeout|connection|tls|loop|http_4xx|http_5xx|blocked|too_large|unreadable|unsafe_destination|no_query|search_unavailable|search_error|no_match)$/.test(result.reason)
143
143
  ? ` (${result.reason})` : "";
144
144
  setNoticeByReceipt((current) => ({ ...current, [requestReceiptId]: `The article destination could not be resolved safely${reason}. The original link remains private and unchanged.` }));
145
145
  return;
@@ -147,7 +147,9 @@ export function EmailSubmissions({ items, detail, onOpen, onResolveArticleLink,
147
147
  const candidate = detail && "submission" in detail ? detail.candidateLinks[index] : null;
148
148
  setResolvedByReceipt((current) => ({ ...current, [requestReceiptId]: { ...current[requestReceiptId], [index]: { url: result.url, title: result.title || candidate?.title || "Article" } } }));
149
149
  setSelectedByReceipt((current) => ({ ...current, [requestReceiptId]: { ...current[requestReceiptId], [index]: true } }));
150
- setNoticeByReceipt((current) => ({ ...current, [requestReceiptId]: "Resolved. This request may be recorded as a click by the newsletter sender." }));
150
+ setNoticeByReceipt((current) => ({ ...current, [requestReceiptId]: result.foundVia === "search"
151
+ ? `The newsletter link could not be followed, so this article was found by searching the headline (match ${Math.round((result.search?.matchScore ?? 0) * 100)}%). Check that it is the right article before importing it.`
152
+ : "Resolved. This request may be recorded as a click by the newsletter sender." }));
151
153
  } catch {
152
154
  if (isCurrentEmailSubmissionAction(activeReceiptId.current, requestReceiptId)) {
153
155
  setNoticeByReceipt((current) => ({ ...current, [requestReceiptId]: "The article destination could not be resolved. Try again later." }));
@@ -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" };