@anthusai/papyrus 1.3.0 → 1.4.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/data/resource.js +134 -3
- package/amplify/data/resource.ts +145 -3
- package/amplify/data/schema.js +131 -0
- package/amplify/data/schema.ts +142 -0
- package/amplify/functions/category-action/handler.ts +272 -20
- package/amplify/functions/console-chat-responder/src/main.rs +1 -1
- package/amplify/functions/content-actions/handler.py +23 -0
- package/amplify/functions/content-actions/requirements.txt +1 -1
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/functions/model-attachment-upload/handler.ts +1 -0
- package/amplify/functions/renditions/requirements.txt +1 -1
- package/amplify/functions/shared/curation-delegation-flow.ts +147 -0
- package/amplify/functions/shared/curation-delegation.ts +115 -0
- package/amplify/functions/shared/python-bundle.js +3 -3
- package/amplify/functions/shared/python-bundle.ts +3 -3
- package/amplify/functions/shared/reference-curation-metadata.ts +51 -0
- package/amplify/functions/shared/slack-events.ts +2 -2
- package/amplify/publication/renditions-function.js +1 -1
- package/amplify/publication/renditions-function.ts +1 -1
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -1
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -1
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +2 -1
- package/app/[year]/[month]/[day]/page.tsx +3 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +2 -0
- package/app/api/archive/editions/route.ts +2 -0
- package/app/api/auth/prospectus/accounts/route.ts +9 -0
- package/app/api/local-preview/derive/route.ts +28 -0
- package/app/api/local-preview/media/route.ts +25 -0
- package/app/archive/page.tsx +3 -0
- package/app/globals.css +28 -28
- package/app/layout.tsx +8 -5
- package/app/newsroom/[section]/[...selection]/page.tsx +0 -1
- package/app/newsroom/[section]/page.tsx +8 -1
- package/app/newsroom/articles/page.tsx +10 -0
- package/app/newsroom/insights/[threadId]/page.tsx +5 -16
- package/app/newsroom/insights/page.tsx +4 -12
- package/app/newsroom/sections/[sectionId]/page.tsx +3 -0
- package/app/page.tsx +30 -1
- package/components/administration-delegations-panel.tsx +156 -0
- package/components/curation-delegation-client.ts +104 -0
- package/components/item-stream-view.tsx +63 -0
- package/components/markus-ir-preview.tsx +47 -2
- package/components/news-desk-client-provider.tsx +17 -2
- package/components/news-desk-page.tsx +0 -1
- package/components/news-desk-taxonomy-client.ts +154 -240
- package/components/newspaper.tsx +4 -4
- package/components/newsroom-article-editor.tsx +138 -40
- package/components/newsroom-assignment-detail.tsx +79 -0
- package/components/newsroom-assignments-view.tsx +1 -1
- package/components/newsroom-board-view.tsx +153 -43
- package/components/newsroom-client-shell.tsx +8 -1
- package/components/newsroom-newsprint-board.css +5 -46
- package/components/newsroom-ops-overview.tsx +27 -5
- package/components/newsroom-organizer-shell.tsx +36 -13
- package/components/newsroom-organizer.css +269 -13
- package/components/newsroom-references-view.tsx +118 -23
- package/components/newsroom-topics-view.tsx +1 -1
- package/components/priming-proof-page.tsx +17 -8
- package/components/prospectus-account-chooser.tsx +131 -0
- package/components/reference-cyclotron-priming.tsx +85 -0
- package/components/reference-cyclotron-status.tsx +5 -5
- package/components/reference-cyclotron-suggestion.tsx +143 -0
- package/components/reference-live-agreement.tsx +62 -0
- package/components/reference-section-suggestion.tsx +104 -0
- package/components/story-reporting-trail-client.ts +91 -0
- package/components/story-reporting-trail.tsx +115 -0
- package/components/topic-steering-workspace.tsx +280 -679
- package/components/ui/sheet.tsx +10 -4
- package/components/use-paged-content-item-versions.ts +151 -0
- package/components/wiki-concepts-view.tsx +153 -0
- package/components/wiki-index-client.ts +51 -0
- package/infra/amplify-app-shell.js +17 -3
- package/infra/amplify-app-shell.ts +18 -3
- package/infra/site-config.js +6 -1
- package/infra/site-config.ts +7 -1
- package/lib/approved-priming-proof-reports.ts +6 -3
- package/lib/approved-priming-proofs/papyrus.json +90 -0
- package/lib/article-flow.ts +95 -0
- package/lib/article-frame-solver.ts +519 -0
- package/lib/blog-feature-solver.ts +1 -109
- package/lib/category-repository.ts +1 -1
- package/lib/chrome-text.ts +504 -0
- package/lib/content-actions-client.ts +3 -0
- package/lib/content-actions-demo.ts +55 -0
- package/lib/content-item-lifecycle.ts +34 -5
- package/lib/content-source-context.ts +9 -1
- package/lib/curation-delegation-helpers.ts +69 -0
- package/lib/cyclotron-suggestions.ts +263 -0
- package/lib/front-matter-fields.ts +42 -0
- package/lib/front-mosaic.ts +1133 -0
- package/lib/furniture.ts +385 -0
- package/lib/graphql-content-repository.ts +42 -7
- package/lib/item-stream.ts +47 -0
- package/lib/labels.ts +37 -0
- package/lib/layout-config.ts +237 -0
- package/lib/layout-plan.ts +40 -11
- package/lib/layout-scenarios.ts +21 -11
- package/lib/layout-types.ts +457 -0
- package/lib/layout-warnings.ts +8 -0
- package/lib/local-preview-section-fixtures.ts +72 -0
- package/lib/local-preview-seed.ts +286 -0
- package/lib/local-preview-tenant.ts +27 -0
- package/lib/local-preview.ts +3 -0
- package/lib/newspaper-layout.ts +17 -3410
- package/lib/newsroom-articles.ts +2 -0
- package/lib/newsroom-assignments.ts +14 -1
- package/lib/newsroom-board.ts +99 -47
- package/lib/newsroom-demo-dashboard.ts +1 -1
- package/lib/newsroom-forum-routes.ts +2 -43
- package/lib/newsroom-index-filters.ts +3 -40
- package/lib/newsroom-media-backend.ts +5 -0
- package/lib/newsroom-nav.ts +1 -3
- package/lib/newsroom-organizer-nav.ts +9 -16
- package/lib/newsroom-section-routing.ts +1 -1
- package/lib/newsroom-sections.ts +12 -0
- package/lib/papyrus-web-locations.ts +3 -24
- package/lib/pretext-layout.ts +22 -0
- package/lib/priming-proof-report.ts +1 -0
- package/lib/prospectus-account-client.ts +90 -0
- package/lib/prospectus-sign-in-routes.ts +45 -0
- package/lib/prospectus-site-identity.ts +13 -1
- package/lib/prospectus-tenant.ts +22 -0
- package/lib/reader-static-params.ts +2 -1
- package/lib/relevance-decisions.ts +29 -0
- package/lib/relevance-status.ts +73 -0
- package/lib/reporting-story-budget.ts +2 -2
- package/lib/semantic-graph.ts +5 -5
- package/lib/site-brand.ts +28 -1
- package/lib/site-env.ts +5 -0
- package/lib/site-section-suggestions.ts +120 -0
- package/lib/site-stack.ts +1 -1
- package/lib/story-reporting-trail.ts +106 -0
- package/lib/tenant-keys.ts +32 -0
- package/lib/text-columns.ts +251 -0
- package/lib/wiki.ts +56 -0
- package/package.json +46 -3
- package/renderers/pretext/presentation-shell.tsx +1 -1
- package/renderers/pretext/rhythm-article-page.tsx +1 -18
- package/routes.manifest.json +34 -15
- package/app/newsroom/work/page.tsx +0 -10
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Hub } from "aws-amplify/utils";
|
|
4
4
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import type { DevSandboxEditorAuth } from "../lib/dev-sandbox-editor-auth";
|
|
6
|
+
import type { CategorySteeringDashboard } from "../lib/category-repository";
|
|
6
7
|
import { ensureDevSandboxEditorSession } from "./dev-sandbox-editor-sign-in";
|
|
7
8
|
import {
|
|
8
9
|
loadEditorAssignmentsData,
|
|
@@ -46,7 +47,12 @@ const NewsDeskClientContext = createContext<NewsDeskClientContextValue | null>(n
|
|
|
46
47
|
export function NewsDeskClientProvider({
|
|
47
48
|
children,
|
|
48
49
|
devSandboxEditorAuth = null,
|
|
49
|
-
|
|
50
|
+
localPreviewDashboard = null,
|
|
51
|
+
}: Readonly<{
|
|
52
|
+
children: React.ReactNode;
|
|
53
|
+
devSandboxEditorAuth?: DevSandboxEditorAuth | null;
|
|
54
|
+
localPreviewDashboard?: CategorySteeringDashboard | null;
|
|
55
|
+
}>) {
|
|
50
56
|
const [shell, setShell] = useState<NewsDeskShellState>(() => createInitialNewsDeskShellState());
|
|
51
57
|
const [searchTransition, setSearchTransition] = useState<NewsDeskSearchTransition | null>(null);
|
|
52
58
|
const shellRef = useRef(shell);
|
|
@@ -58,6 +64,15 @@ export function NewsDeskClientProvider({
|
|
|
58
64
|
|
|
59
65
|
const refreshDashboard = useCallback(async () => {
|
|
60
66
|
const sequence = ++bootstrapSequenceRef.current;
|
|
67
|
+
if (localPreviewDashboard) {
|
|
68
|
+
setShell((current) => resolveDashboardReady(
|
|
69
|
+
current,
|
|
70
|
+
localPreviewDashboard,
|
|
71
|
+
{ status: "signedIn", label: "Local preview editor", email: null },
|
|
72
|
+
new Date().toISOString(),
|
|
73
|
+
));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
61
76
|
setShell((current) => beginAccessCheck(current));
|
|
62
77
|
|
|
63
78
|
try {
|
|
@@ -101,7 +116,7 @@ export function NewsDeskClientProvider({
|
|
|
101
116
|
error instanceof Error ? error.message : "Could not load Newsroom data.",
|
|
102
117
|
));
|
|
103
118
|
}
|
|
104
|
-
}, [devSandboxEditorAuth]);
|
|
119
|
+
}, [devSandboxEditorAuth, localPreviewDashboard]);
|
|
105
120
|
|
|
106
121
|
const refreshAssignments = useCallback(async () => {
|
|
107
122
|
try {
|
|
@@ -86,7 +86,6 @@ function parseRouteSelection(tab: NewsDeskTab, selectionPath: string[] | null |
|
|
|
86
86
|
if (segments[0] === "forum" && segments[1]) return { forumThread: segments[1] };
|
|
87
87
|
return { message: segments[0] ?? null };
|
|
88
88
|
}
|
|
89
|
-
if (tab === "insights") return { forumThread: segments[0] ?? null };
|
|
90
89
|
if (tab === "assignments") return { assignment: segments[0] ?? null };
|
|
91
90
|
if (tab === "overview" && segments[0] === "forum" && segments[1]) return { forumThread: segments[1] };
|
|
92
91
|
return {};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { isEditorialWorkAssignment } from "../lib/newsroom-assignments";
|
|
4
|
+
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
5
|
+
import { createLocalPreviewRealPublishedVersions, createLocalPreviewAssignments, createLocalPreviewContentItemVersions, createLocalPreviewReferences } from "../lib/local-preview-seed";
|
|
3
6
|
import { generateClient } from "aws-amplify/data";
|
|
4
7
|
import { testEditorOverrideAllowed } from "../lib/test-editor-override";
|
|
5
8
|
import { downloadData, getUrl } from "aws-amplify/storage";
|
|
@@ -39,7 +42,7 @@ import type {
|
|
|
39
42
|
UserDirectoryEntry,
|
|
40
43
|
} from "../lib/category-repository";
|
|
41
44
|
import { relationTypeKey } from "../lib/semantic-graph";
|
|
42
|
-
import {
|
|
45
|
+
import { legacyStageKeysToLoad, type ContentItemVersionRecord } from "../lib/content-item-lifecycle";
|
|
43
46
|
import { getContentItemStageDefinitions } from "../lib/site-brand";
|
|
44
47
|
import { DOCTRINE_DEFINITIONS, getCategoryDoctrineDefinitions, type DoctrineCategory } from "../lib/doctrine";
|
|
45
48
|
import { configureAmplifyClient } from "./amplify-client-provider";
|
|
@@ -688,16 +691,11 @@ type NewsroomSemanticRelationPageOptions = NewsroomPageOptions & {
|
|
|
688
691
|
|
|
689
692
|
export type ForumThreadWithMessages = MessageThreadRecord & {
|
|
690
693
|
messages: MessageRecord[];
|
|
691
|
-
scope: "edition" | "section"
|
|
694
|
+
scope: "edition" | "section";
|
|
692
695
|
sectionKey?: string | null;
|
|
693
696
|
sectionTitle?: string | null;
|
|
694
697
|
};
|
|
695
698
|
|
|
696
|
-
export type InsightForumThreadsPage = {
|
|
697
|
-
threads: ForumThreadWithMessages[];
|
|
698
|
-
nextToken: string | null;
|
|
699
|
-
};
|
|
700
|
-
|
|
701
699
|
export type EditionForumThreadsResult = {
|
|
702
700
|
editionId: string;
|
|
703
701
|
editionThreads: ForumThreadWithMessages[];
|
|
@@ -1095,6 +1093,27 @@ export async function loadEditorProcedureData(): Promise<{
|
|
|
1095
1093
|
};
|
|
1096
1094
|
}
|
|
1097
1095
|
|
|
1096
|
+
const UPDATE_ASSIGNMENT_DESK_MUTATION = `
|
|
1097
|
+
mutation UpdateAssignmentDesk($input: UpdateAssignmentInput!) {
|
|
1098
|
+
updateAssignment(input: $input) { id sectionId sectionKey }
|
|
1099
|
+
}
|
|
1100
|
+
`;
|
|
1101
|
+
|
|
1102
|
+
/** Puts an assignment on a desk's board, or takes it off every desk when `deskId` is null. */
|
|
1103
|
+
export async function saveNewsroomAssignmentDesk(assignmentId: string, deskId: string | null): Promise<void> {
|
|
1104
|
+
if (isLocalPreviewEnabled()) return;
|
|
1105
|
+
const dataClient = generateClient<Schema>();
|
|
1106
|
+
const graphClient = dataClient as unknown as {
|
|
1107
|
+
graphql: (options: Record<string, unknown>) => Promise<{ data?: Record<string, unknown> | null; errors?: unknown[] | null }>;
|
|
1108
|
+
};
|
|
1109
|
+
const response = await graphClient.graphql({
|
|
1110
|
+
query: UPDATE_ASSIGNMENT_DESK_MUTATION,
|
|
1111
|
+
variables: { input: { id: assignmentId, sectionId: deskId, sectionKey: deskId } },
|
|
1112
|
+
authMode: USER_POOL_AUTH_MODE,
|
|
1113
|
+
});
|
|
1114
|
+
assertNoGraphQLErrors(response.errors);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1098
1117
|
export async function saveProcedureDefinitionRecord(input: Record<string, unknown>) {
|
|
1099
1118
|
const dataClient = generateClient<Schema>();
|
|
1100
1119
|
const graphClient = dataClient as unknown as {
|
|
@@ -1275,148 +1294,6 @@ export async function loadEditionForumThreads(options: {
|
|
|
1275
1294
|
};
|
|
1276
1295
|
}
|
|
1277
1296
|
|
|
1278
|
-
export async function loadInsightForumThreads(options: {
|
|
1279
|
-
domain?: string;
|
|
1280
|
-
limit?: number;
|
|
1281
|
-
nextToken?: string | null;
|
|
1282
|
-
includeMessages?: boolean;
|
|
1283
|
-
} = {}): Promise<InsightForumThreadsPage> {
|
|
1284
|
-
const testMock = getTestEditorNewsroomMock();
|
|
1285
|
-
if (testMock?.insightForumThreads) {
|
|
1286
|
-
const domain = String(options.domain || "").trim();
|
|
1287
|
-
let threads = testMock.insightForumThreads;
|
|
1288
|
-
if (domain) threads = threads.filter((thread) => thread.sectionKey === domain);
|
|
1289
|
-
return { threads, nextToken: null };
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
const limit = Math.max(1, Math.min(options.limit ?? NEWSROOM_PAGE_LIMIT, 100));
|
|
1293
|
-
const domain = String(options.domain || "").trim();
|
|
1294
|
-
const includeMessages = options.includeMessages !== false;
|
|
1295
|
-
type KindConnectionResponse = {
|
|
1296
|
-
listMessagesByKindAndCreatedAt?: {
|
|
1297
|
-
items?: Array<MessageRecord | null> | null;
|
|
1298
|
-
nextToken?: string | null;
|
|
1299
|
-
} | null;
|
|
1300
|
-
};
|
|
1301
|
-
const response: KindConnectionResponse = await runGraphql<KindConnectionResponse>(
|
|
1302
|
-
LIST_MESSAGES_BY_KIND_AND_CREATED_AT_QUERY,
|
|
1303
|
-
{
|
|
1304
|
-
messageKind: "insight",
|
|
1305
|
-
sortDirection: "DESC",
|
|
1306
|
-
limit,
|
|
1307
|
-
nextToken: options.nextToken ?? null,
|
|
1308
|
-
},
|
|
1309
|
-
);
|
|
1310
|
-
const connection = response.listMessagesByKindAndCreatedAt;
|
|
1311
|
-
const roots = (connection?.items ?? [])
|
|
1312
|
-
.filter(Boolean)
|
|
1313
|
-
.map((item) => normalizeMessageRecord(item as MessageRecord))
|
|
1314
|
-
.filter(isInsightThreadRootMessage)
|
|
1315
|
-
.filter((message) => !domain || message.messageDomain === domain);
|
|
1316
|
-
|
|
1317
|
-
const threads = await Promise.all(roots.map(async (root) => {
|
|
1318
|
-
const messages = includeMessages
|
|
1319
|
-
? await loadInsightThreadMessages(root.id, root)
|
|
1320
|
-
: [root];
|
|
1321
|
-
return buildInsightForumThread(root, messages);
|
|
1322
|
-
}));
|
|
1323
|
-
|
|
1324
|
-
return {
|
|
1325
|
-
threads,
|
|
1326
|
-
nextToken: connection?.nextToken ?? null,
|
|
1327
|
-
};
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
export async function loadInsightForumThreadById(
|
|
1331
|
-
threadId: string,
|
|
1332
|
-
options: { includeMessages?: boolean } = {},
|
|
1333
|
-
): Promise<ForumThreadWithMessages | null> {
|
|
1334
|
-
const normalized = String(threadId || "").trim();
|
|
1335
|
-
if (!normalized) return null;
|
|
1336
|
-
const testMock = getTestEditorNewsroomMock();
|
|
1337
|
-
if (testMock?.insightForumThreads) {
|
|
1338
|
-
return testMock.insightForumThreads.find((thread) => thread.id === normalized) ?? null;
|
|
1339
|
-
}
|
|
1340
|
-
const root = await getMessageRecordById(normalized);
|
|
1341
|
-
if (!root || !isInsightThreadRootMessage(root)) return null;
|
|
1342
|
-
const includeMessages = options.includeMessages !== false;
|
|
1343
|
-
const messages = includeMessages
|
|
1344
|
-
? await loadInsightThreadMessages(normalized, root)
|
|
1345
|
-
: [root];
|
|
1346
|
-
return buildInsightForumThread(root, messages);
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
export async function appendInsightThreadReplyRecord(input: {
|
|
1350
|
-
threadId: string;
|
|
1351
|
-
summary: string;
|
|
1352
|
-
content: string;
|
|
1353
|
-
role?: string;
|
|
1354
|
-
authorLabel?: string;
|
|
1355
|
-
parentMessageId?: string | null;
|
|
1356
|
-
metadata?: Record<string, unknown>;
|
|
1357
|
-
}): Promise<MessageRecord> {
|
|
1358
|
-
const threadId = String(input.threadId || "").trim();
|
|
1359
|
-
const summary = String(input.summary || "").trim();
|
|
1360
|
-
const content = String(input.content || "").trim();
|
|
1361
|
-
if (!threadId) throw new Error("threadId is required");
|
|
1362
|
-
if (!summary) throw new Error("summary is required");
|
|
1363
|
-
if (!content) throw new Error("content is required");
|
|
1364
|
-
const root = await getMessageRecordById(threadId);
|
|
1365
|
-
if (!root || !isInsightThreadRootMessage(root)) throw new Error(`Insight thread not found: ${threadId}`);
|
|
1366
|
-
const existing = await listMessagesByThreadId(threadId, 1000);
|
|
1367
|
-
const nextSequence = Math.max(1, ...existing.map((message) => Number(message.sequenceNumber || 0))) + 1;
|
|
1368
|
-
const now = new Date().toISOString();
|
|
1369
|
-
const message: MessageRecord = {
|
|
1370
|
-
id: `message-insight-reply-${safeForumId(threadId)}-${String(nextSequence).padStart(4, "0")}`,
|
|
1371
|
-
messageKind: "insight_reply",
|
|
1372
|
-
messageDomain: root.messageDomain ?? "knowledge",
|
|
1373
|
-
status: "active",
|
|
1374
|
-
summary,
|
|
1375
|
-
source: "newsroom.insights",
|
|
1376
|
-
authorLabel: input.authorLabel?.trim() || "editor",
|
|
1377
|
-
threadId,
|
|
1378
|
-
parentMessageId: input.parentMessageId?.trim() || null,
|
|
1379
|
-
sequenceNumber: nextSequence,
|
|
1380
|
-
role: input.role || "human",
|
|
1381
|
-
messageType: "insight_forum_reply",
|
|
1382
|
-
content,
|
|
1383
|
-
metadata: input.metadata ?? {},
|
|
1384
|
-
createdAt: now,
|
|
1385
|
-
updatedAt: now,
|
|
1386
|
-
newsroomFeedKey: "messages",
|
|
1387
|
-
};
|
|
1388
|
-
const created = await runGraphql<{ createMessage?: MessageRecord | null }>(
|
|
1389
|
-
CREATE_MESSAGE_MUTATION,
|
|
1390
|
-
{ input: message },
|
|
1391
|
-
);
|
|
1392
|
-
return normalizeMessageRecord(created.createMessage ?? message);
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
export async function deleteInsightThreadMessageRecord(input: {
|
|
1396
|
-
threadId: string;
|
|
1397
|
-
messageId: string;
|
|
1398
|
-
}): Promise<MessageRecord> {
|
|
1399
|
-
const threadId = String(input.threadId || "").trim();
|
|
1400
|
-
const messageId = String(input.messageId || "").trim();
|
|
1401
|
-
if (!threadId) throw new Error("threadId is required");
|
|
1402
|
-
if (!messageId) throw new Error("messageId is required");
|
|
1403
|
-
const existing = await listMessagesByThreadId(threadId, 1000);
|
|
1404
|
-
const target = existing.find((message) => message.id === messageId);
|
|
1405
|
-
if (!target) throw new Error(`Message not found in thread: ${messageId}`);
|
|
1406
|
-
const now = new Date().toISOString();
|
|
1407
|
-
const updatedMessage = await runGraphql<{ updateMessage?: MessageRecord | null }>(
|
|
1408
|
-
UPDATE_MESSAGE_MUTATION,
|
|
1409
|
-
{
|
|
1410
|
-
input: {
|
|
1411
|
-
id: target.id,
|
|
1412
|
-
status: "deleted",
|
|
1413
|
-
updatedAt: now,
|
|
1414
|
-
},
|
|
1415
|
-
},
|
|
1416
|
-
);
|
|
1417
|
-
return normalizeMessageRecord(updatedMessage.updateMessage ?? { ...target, status: "deleted", updatedAt: now });
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
1297
|
export async function ensureEditionForumThreadRecord(input: {
|
|
1421
1298
|
editionId: string;
|
|
1422
1299
|
title?: string;
|
|
@@ -1680,14 +1557,6 @@ async function listMessagesByThreadId(threadId: string, limit = 400): Promise<Me
|
|
|
1680
1557
|
return records;
|
|
1681
1558
|
}
|
|
1682
1559
|
|
|
1683
|
-
function isInsightThreadRootMessage(message: MessageRecord): boolean {
|
|
1684
|
-
if (String(message.messageKind || "") !== "insight") return false;
|
|
1685
|
-
const threadId = String(message.threadId || "").trim();
|
|
1686
|
-
const sequenceNumber = Number(message.sequenceNumber || 0);
|
|
1687
|
-
if (threadId && threadId !== message.id && sequenceNumber > 1) return false;
|
|
1688
|
-
return true;
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
1560
|
async function getMessageRecordById(messageId: string): Promise<MessageRecord | null> {
|
|
1692
1561
|
const normalized = String(messageId || "").trim();
|
|
1693
1562
|
if (!normalized) return null;
|
|
@@ -1699,66 +1568,6 @@ async function getMessageRecordById(messageId: string): Promise<MessageRecord |
|
|
|
1699
1568
|
return message ? normalizeMessageRecord(message) : null;
|
|
1700
1569
|
}
|
|
1701
1570
|
|
|
1702
|
-
async function loadInsightThreadMessages(threadId: string, root: MessageRecord): Promise<MessageRecord[]> {
|
|
1703
|
-
const threadMessages = await listMessagesByThreadId(threadId, 500);
|
|
1704
|
-
const byId = new Map<string, MessageRecord>();
|
|
1705
|
-
byId.set(root.id, root);
|
|
1706
|
-
for (const message of threadMessages) byId.set(message.id, message);
|
|
1707
|
-
const merged = [...byId.values()].sort(
|
|
1708
|
-
(left, right) => Number(left.sequenceNumber ?? 0) - Number(right.sequenceNumber ?? 0),
|
|
1709
|
-
);
|
|
1710
|
-
return hydrateMessageRecordsContent(merged);
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
function insightForumThreadTitle(root: MessageRecord): string {
|
|
1714
|
-
const metadata = normalizeJsonValue(root.metadata);
|
|
1715
|
-
if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
|
|
1716
|
-
const explicit = String((metadata as Record<string, unknown>).insightTitle || "").trim();
|
|
1717
|
-
if (explicit) return explicit;
|
|
1718
|
-
}
|
|
1719
|
-
const summary = String(root.summary || "").trim();
|
|
1720
|
-
return summary || "Insight";
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
function buildInsightForumThread(root: MessageRecord, messages: MessageRecord[]): ForumThreadWithMessages {
|
|
1724
|
-
const activeMessages = messages.filter((message) => String(message.status || "active") === "active");
|
|
1725
|
-
const sorted = [...activeMessages].sort(
|
|
1726
|
-
(left, right) => Number(left.sequenceNumber ?? 0) - Number(right.sequenceNumber ?? 0),
|
|
1727
|
-
);
|
|
1728
|
-
const lastMessage = sorted.at(-1) ?? root;
|
|
1729
|
-
const activityAt = lastMessage.updatedAt ?? lastMessage.createdAt ?? root.updatedAt ?? root.createdAt;
|
|
1730
|
-
return {
|
|
1731
|
-
id: root.id,
|
|
1732
|
-
threadKind: "insight_forum",
|
|
1733
|
-
status: "active",
|
|
1734
|
-
title: insightForumThreadTitle(root),
|
|
1735
|
-
summary: formatInsightForumDomainLabel(root.messageDomain),
|
|
1736
|
-
primaryAnchorKind: "message",
|
|
1737
|
-
primaryAnchorId: root.id,
|
|
1738
|
-
primaryAnchorLineageId: root.id,
|
|
1739
|
-
primaryAnchorKey: `message#${root.id}`,
|
|
1740
|
-
createdByLabel: root.authorLabel ?? null,
|
|
1741
|
-
messageCount: sorted.length,
|
|
1742
|
-
lastMessageId: lastMessage.id,
|
|
1743
|
-
lastMessageAt: activityAt,
|
|
1744
|
-
metadata: normalizeJsonValue(root.metadata) as Record<string, unknown>,
|
|
1745
|
-
createdAt: root.createdAt,
|
|
1746
|
-
updatedAt: activityAt,
|
|
1747
|
-
newsroomFeedKey: "messages",
|
|
1748
|
-
messages: sorted.length ? sorted : [root],
|
|
1749
|
-
scope: "insight",
|
|
1750
|
-
sectionKey: root.messageDomain ?? null,
|
|
1751
|
-
};
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
function formatInsightForumDomainLabel(domain: string | null | undefined): string {
|
|
1755
|
-
const value = String(domain || "").trim();
|
|
1756
|
-
if (value === "assignment_work") return "Assignment research";
|
|
1757
|
-
if (value === "knowledge") return "Reference knowledge";
|
|
1758
|
-
if (!value) return "Insight";
|
|
1759
|
-
return value.replace(/_/g, " ");
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
1571
|
async function hydrateMessageRecordsContent(messages: MessageRecord[]): Promise<MessageRecord[]> {
|
|
1763
1572
|
return Promise.all(messages.map(async (message) => {
|
|
1764
1573
|
try {
|
|
@@ -1863,6 +1672,7 @@ export function normalizeNewsroomReferencePageOrder(value?: string | null): News
|
|
|
1863
1672
|
}
|
|
1864
1673
|
|
|
1865
1674
|
export async function loadNewsroomReferencePage(options: NewsroomReferencePageOptions = {}): Promise<NewsroomRecordPage<ReferenceRecord>> {
|
|
1675
|
+
if (isLocalPreviewEnabled()) return { items: createLocalPreviewReferences(), nextToken: null, hasMore: false };
|
|
1866
1676
|
configureAmplifyClient();
|
|
1867
1677
|
const order = normalizeNewsroomReferencePageOrder(options.order);
|
|
1868
1678
|
if (order === "imported") {
|
|
@@ -2146,52 +1956,68 @@ export async function loadEditorOverviewEditionData(): Promise<{
|
|
|
2146
1956
|
}
|
|
2147
1957
|
|
|
2148
1958
|
export async function loadNewsroomAssignmentPage(options: NewsroomAssignmentPageOptions = {}): Promise<NewsroomRecordPage<AssignmentRecord>> {
|
|
1959
|
+
if (isLocalPreviewEnabled()) return { items: createLocalPreviewAssignments(), nextToken: null, hasMore: false };
|
|
2149
1960
|
return loadNewsroomFeedPage<AssignmentRecord>({
|
|
2150
1961
|
query: NEWSROOM_ASSIGNMENT_FEED_QUERY,
|
|
2151
1962
|
field: "listAssignmentsByNewsroomFeedAndCreatedAt",
|
|
2152
1963
|
newsroomFeedKey: "assignments",
|
|
2153
1964
|
limit: options.limit,
|
|
2154
1965
|
nextToken: options.nextToken,
|
|
2155
|
-
matches: equalityMatcher({
|
|
1966
|
+
matches: (assignment) => isEditorialWorkAssignment(assignment) && (equalityMatcher<AssignmentRecord>({
|
|
2156
1967
|
assignmentTypeKey: options.type,
|
|
2157
1968
|
status: options.status,
|
|
2158
|
-
}),
|
|
1969
|
+
})?.(assignment) ?? true),
|
|
2159
1970
|
});
|
|
2160
1971
|
}
|
|
2161
1972
|
|
|
2162
1973
|
const CONTENT_ITEMS_BY_STAGE_QUERY = `
|
|
2163
1974
|
query ListItemsByStageAndUpdatedAt($stage: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
|
|
2164
1975
|
listItemsByStageAndUpdatedAt(stage: $stage, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
|
|
2165
|
-
items { id lineageId versionNumber stage type status title headline slug section updatedAt }
|
|
1976
|
+
items { id lineageId versionNumber stage type status title headline deck metadata slug section updatedAt }
|
|
2166
1977
|
nextToken
|
|
2167
1978
|
}
|
|
2168
1979
|
}
|
|
2169
1980
|
`;
|
|
2170
1981
|
|
|
2171
|
-
export
|
|
1982
|
+
export const CONTENT_ITEM_COLUMN_PAGE_LIMIT = 25;
|
|
1983
|
+
|
|
1984
|
+
export type ContentItemStagePage = {
|
|
1985
|
+
versions: ContentItemVersionRecord[];
|
|
1986
|
+
nextToken: string | null;
|
|
1987
|
+
};
|
|
1988
|
+
|
|
1989
|
+
/** One newest-updated-first page of the content items in one stage, so a board column can load as it is scrolled. */
|
|
1990
|
+
export async function loadNewsroomContentItemStagePage(
|
|
1991
|
+
stage: string,
|
|
1992
|
+
nextToken: string | null = null,
|
|
1993
|
+
limit: number = CONTENT_ITEM_COLUMN_PAGE_LIMIT,
|
|
1994
|
+
): Promise<ContentItemStagePage> {
|
|
1995
|
+
if (isLocalPreviewEnabled()) {
|
|
1996
|
+
const versionsInStage = createLocalPreviewContentItemVersions().filter(version => version.stage === stage);
|
|
1997
|
+
const offset = nextToken ? Number(nextToken) : 0;
|
|
1998
|
+
const followingOffset = offset + limit;
|
|
1999
|
+
return {
|
|
2000
|
+
versions: versionsInStage.slice(offset, followingOffset),
|
|
2001
|
+
nextToken: followingOffset < versionsInStage.length ? String(followingOffset) : null,
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2172
2004
|
configureAmplifyClient();
|
|
2173
2005
|
const client = generateClient<Schema>() as unknown as {
|
|
2174
2006
|
graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<ContentItemVersionRecord>>;
|
|
2175
2007
|
};
|
|
2176
2008
|
const field = "listItemsByStageAndUpdatedAt";
|
|
2177
|
-
const
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
versions.push(...((connection.items ?? []).filter(Boolean) as ContentItemVersionRecord[]));
|
|
2190
|
-
nextToken = connection.nextToken ?? null;
|
|
2191
|
-
} while (nextToken);
|
|
2192
|
-
return versions;
|
|
2193
|
-
}));
|
|
2194
|
-
return versionsPerStage.flat();
|
|
2009
|
+
const response: GraphQLConnectionResponse<ContentItemVersionRecord> = await client.graphql({
|
|
2010
|
+
query: CONTENT_ITEMS_BY_STAGE_QUERY,
|
|
2011
|
+
variables: { stage, sortDirection: "DESC", limit, nextToken },
|
|
2012
|
+
authMode: USER_POOL_AUTH_MODE,
|
|
2013
|
+
});
|
|
2014
|
+
assertNoGraphQLErrors(response.errors);
|
|
2015
|
+
const connection = response.data?.[field];
|
|
2016
|
+
if (!connection) throw new Error(`Missing GraphQL connection payload for ${field}.`);
|
|
2017
|
+
return {
|
|
2018
|
+
versions: (connection.items ?? []).filter(Boolean) as ContentItemVersionRecord[],
|
|
2019
|
+
nextToken: connection.nextToken ?? null,
|
|
2020
|
+
};
|
|
2195
2021
|
}
|
|
2196
2022
|
|
|
2197
2023
|
const PUBLISHED_ITEMS_QUERY = `
|
|
@@ -2217,6 +2043,7 @@ type PublishedItemBoardRow = {
|
|
|
2217
2043
|
};
|
|
2218
2044
|
|
|
2219
2045
|
export async function loadNewsroomPublishedContentItemVersions(): Promise<ContentItemVersionRecord[]> {
|
|
2046
|
+
if (isLocalPreviewEnabled()) return createLocalPreviewRealPublishedVersions();
|
|
2220
2047
|
configureAmplifyClient();
|
|
2221
2048
|
const client = generateClient<Schema>() as unknown as {
|
|
2222
2049
|
graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<PublishedItemBoardRow>>;
|
|
@@ -2670,7 +2497,6 @@ export function hasTestEditorOverride(): boolean {
|
|
|
2670
2497
|
|
|
2671
2498
|
type TestEditorNewsroomMock = {
|
|
2672
2499
|
forumThreadsByEdition?: Record<string, ForumThreadWithMessages[]>;
|
|
2673
|
-
insightForumThreads?: ForumThreadWithMessages[];
|
|
2674
2500
|
messages?: MessageRecord[];
|
|
2675
2501
|
payloads?: Record<string, HydratedModelPayload[]>;
|
|
2676
2502
|
referenceAttachments?: ReferenceAttachmentRecord[];
|
|
@@ -3795,6 +3621,25 @@ export async function loadCyclotronStatus(cyclotronId: string): Promise<import("
|
|
|
3795
3621
|
return latestCyclotronStatus(payloads.map((payload) => ({ json: payload.json, updatedAt: payload.attachment.updatedAt })));
|
|
3796
3622
|
}
|
|
3797
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
|
+
|
|
3631
|
+
/** The site cyclotron's latest per-Section agreement payload, as the Section sweep recorded it. */
|
|
3632
|
+
export async function loadSiteSectionAgreement(cyclotronId: string): Promise<import("../lib/site-section-suggestions").SiteSectionAgreement | null> {
|
|
3633
|
+
const { shadowAgreementPayloadId } = await import("../lib/cyclotron-suggestions");
|
|
3634
|
+
const { parseSiteSectionAgreement } = await import("../lib/site-section-suggestions");
|
|
3635
|
+
const payloads = await loadModelPayloadsForOwner("knowledgeRawPayload", shadowAgreementPayloadId(cyclotronId), ["raw_payload"]);
|
|
3636
|
+
const parsed = payloads
|
|
3637
|
+
.map((payload) => ({ agreement: parseSiteSectionAgreement(payload.json), updatedAt: payload.attachment.updatedAt }))
|
|
3638
|
+
.filter((entry) => entry.agreement !== null)
|
|
3639
|
+
.sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")));
|
|
3640
|
+
return parsed.length ? parsed[0].agreement : null;
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3798
3643
|
/** Ask the next decide-relevance sweep to set or clear a manual review rate. */
|
|
3799
3644
|
export async function requestCyclotronReviewRate(
|
|
3800
3645
|
cyclotronId: string,
|
|
@@ -3826,3 +3671,72 @@ export async function requestCyclotronReviewRate(
|
|
|
3826
3671
|
content: JSON.stringify(request),
|
|
3827
3672
|
});
|
|
3828
3673
|
}
|
|
3674
|
+
|
|
3675
|
+
const LIST_PRIMING_REQUEST_PAYLOADS_QUERY = `
|
|
3676
|
+
query ListKnowledgeRawPayloadsByOwnerAndKind($ownerId: ID!, $payloadKind: ModelStringKeyConditionInput, $limit: Int, $nextToken: String) {
|
|
3677
|
+
listKnowledgeRawPayloadsByOwnerAndKind(ownerId: $ownerId, payloadKind: $payloadKind, limit: $limit, nextToken: $nextToken) {
|
|
3678
|
+
items { id }
|
|
3679
|
+
nextToken
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
`;
|
|
3683
|
+
|
|
3684
|
+
/** Ask the next decide-relevance sweep to prime the running cyclotron with one pasted URL. */
|
|
3685
|
+
export async function requestCyclotronPriming(
|
|
3686
|
+
cyclotronId: string,
|
|
3687
|
+
request: import("../lib/relevance-status").PrimingRequest,
|
|
3688
|
+
): Promise<void> {
|
|
3689
|
+
const { primingRequestPayloadId, PRIMING_REQUEST_KIND, safeId } = await import("../lib/relevance-status");
|
|
3690
|
+
const id = primingRequestPayloadId(cyclotronId, request.requestId);
|
|
3691
|
+
await runGraphql(CREATE_KNOWLEDGE_RAW_PAYLOAD_MUTATION, {
|
|
3692
|
+
input: {
|
|
3693
|
+
id,
|
|
3694
|
+
ownerType: "cyclotron",
|
|
3695
|
+
ownerId: safeId(cyclotronId),
|
|
3696
|
+
payloadKind: PRIMING_REQUEST_KIND,
|
|
3697
|
+
createdAt: request.requestedAt,
|
|
3698
|
+
updatedAt: request.requestedAt,
|
|
3699
|
+
},
|
|
3700
|
+
});
|
|
3701
|
+
await uploadModelPayloadForOwner({
|
|
3702
|
+
ownerKind: "knowledgeRawPayload",
|
|
3703
|
+
ownerId: id,
|
|
3704
|
+
ownerLineageId: id,
|
|
3705
|
+
role: "raw_payload",
|
|
3706
|
+
sortKey: "priming-request",
|
|
3707
|
+
filename: "priming-request.json",
|
|
3708
|
+
mediaType: "application/json",
|
|
3709
|
+
content: JSON.stringify(request),
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
/** The saved priming requests of a cyclotron, newest first, each with the sweep's outcome once there is one. */
|
|
3714
|
+
export async function loadCyclotronPrimingRequests(
|
|
3715
|
+
cyclotronId: string,
|
|
3716
|
+
): Promise<import("../lib/relevance-status").PrimingRequestView[]> {
|
|
3717
|
+
const { PRIMING_OUTCOME_ROLE, PRIMING_REQUEST_KIND, isPrimingOutcome, safeId } = await import("../lib/relevance-status");
|
|
3718
|
+
const ids: string[] = [];
|
|
3719
|
+
let nextToken: string | null = null;
|
|
3720
|
+
do {
|
|
3721
|
+
const page: { listKnowledgeRawPayloadsByOwnerAndKind?: { items?: Array<{ id: string } | null>; nextToken?: string | null } | null } =
|
|
3722
|
+
await runGraphql(LIST_PRIMING_REQUEST_PAYLOADS_QUERY, {
|
|
3723
|
+
ownerId: safeId(cyclotronId), payloadKind: { eq: PRIMING_REQUEST_KIND }, limit: 100, nextToken,
|
|
3724
|
+
});
|
|
3725
|
+
const connection = page.listKnowledgeRawPayloadsByOwnerAndKind;
|
|
3726
|
+
ids.push(...(connection?.items ?? []).filter((item): item is { id: string } => Boolean(item)).map((item) => item.id));
|
|
3727
|
+
nextToken = connection?.nextToken ?? null;
|
|
3728
|
+
} while (nextToken);
|
|
3729
|
+
const views = await Promise.all(ids.map(async (id) => {
|
|
3730
|
+
const payloads = await loadModelPayloadsForOwner("knowledgeRawPayload", id, ["raw_payload", PRIMING_OUTCOME_ROLE]);
|
|
3731
|
+
const request = payloads.find((payload) => payload.attachment.role === "raw_payload")?.json;
|
|
3732
|
+
const outcome = payloads.find((payload) => payload.attachment.role === PRIMING_OUTCOME_ROLE)?.json;
|
|
3733
|
+
if (!request || typeof request !== "object") return null;
|
|
3734
|
+
return {
|
|
3735
|
+
request: request as import("../lib/relevance-status").PrimingRequest,
|
|
3736
|
+
outcome: isPrimingOutcome(outcome) ? outcome : null,
|
|
3737
|
+
};
|
|
3738
|
+
}));
|
|
3739
|
+
return views
|
|
3740
|
+
.filter((view): view is import("../lib/relevance-status").PrimingRequestView => view !== null)
|
|
3741
|
+
.sort((left, right) => right.request.requestedAt.localeCompare(left.request.requestedAt));
|
|
3742
|
+
}
|
package/components/newspaper.tsx
CHANGED
|
@@ -1139,10 +1139,10 @@ function FrontStoryBlock({
|
|
|
1139
1139
|
<div className="jump-line">
|
|
1140
1140
|
{block.jumpTargetPage ? (
|
|
1141
1141
|
disableLinks ? (
|
|
1142
|
-
<span>{block.jumpLabel
|
|
1142
|
+
<span>{block.jumpLabel}</span>
|
|
1143
1143
|
) : (
|
|
1144
1144
|
<Link href={getPageHref(block.jumpTargetPage, editionBasePath)} onClick={(event) => handleJumpClick(event, block.jumpTargetPage ?? 1, scrollToPage)}>
|
|
1145
|
-
{block.jumpLabel
|
|
1145
|
+
{block.jumpLabel}
|
|
1146
1146
|
</Link>
|
|
1147
1147
|
)
|
|
1148
1148
|
) : (
|
|
@@ -1178,10 +1178,10 @@ function FrontStoryBlock({
|
|
|
1178
1178
|
<div className="jump-line">
|
|
1179
1179
|
{block.jumpTargetPage ? (
|
|
1180
1180
|
disableLinks ? (
|
|
1181
|
-
<span>{block.jumpLabel
|
|
1181
|
+
<span>{block.jumpLabel}</span>
|
|
1182
1182
|
) : (
|
|
1183
1183
|
<Link href={getPageHref(block.jumpTargetPage, editionBasePath)} onClick={(event) => handleJumpClick(event, block.jumpTargetPage ?? 1, scrollToPage)}>
|
|
1184
|
-
{block.jumpLabel
|
|
1184
|
+
{block.jumpLabel}
|
|
1185
1185
|
</Link>
|
|
1186
1186
|
)
|
|
1187
1187
|
) : (
|