@anthusai/papyrus 1.9.0 → 1.11.0
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 +3 -3
- package/amplify/data/resource.ts +3 -3
- package/amplify/functions/category-action/handler.ts +18 -18
- 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/renditions/requirements.txt +1 -1
- 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/api/local-preview/derive/route.ts +28 -0
- package/app/api/local-preview/media/route.ts +25 -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 +0 -1
- package/app/newsroom/insights/[threadId]/page.tsx +5 -16
- package/app/newsroom/insights/page.tsx +4 -12
- package/components/board-card-excerpts.tsx +45 -0
- package/components/markus-ir-preview.tsx +47 -2
- package/components/news-desk-page.tsx +0 -1
- package/components/news-desk-taxonomy-client.ts +28 -222
- package/components/newsroom-article-editor.tsx +138 -40
- package/components/newsroom-assignment-detail.tsx +79 -0
- package/components/newsroom-board-view.tsx +44 -9
- package/components/newsroom-newsprint-board.css +9 -2
- package/components/newsroom-ops-overview.tsx +27 -5
- package/components/newsroom-organizer-shell.tsx +25 -7
- package/components/newsroom-organizer.css +217 -6
- package/components/newsroom-topics-view.tsx +1 -1
- package/components/priming-proof-page.tsx +17 -8
- package/components/prospectus-account-chooser.tsx +103 -0
- package/components/story-reporting-trail-client.ts +91 -0
- package/components/story-reporting-trail.tsx +115 -0
- package/components/topic-steering-workspace.tsx +154 -671
- package/components/ui/sheet.tsx +10 -4
- package/components/wiki-concepts-view.tsx +153 -0
- package/components/wiki-index-client.ts +51 -0
- package/lib/approved-priming-proof-reports.ts +6 -3
- package/lib/approved-priming-proofs/papyrus.json +90 -0
- package/lib/content-actions-client.ts +3 -0
- package/lib/content-actions-demo.ts +55 -0
- package/lib/content-item-lifecycle.ts +4 -2
- package/lib/front-matter-fields.ts +42 -0
- package/lib/front-mosaic.ts +2 -1
- package/lib/layout-plan.ts +3 -2
- package/lib/layout-warnings.ts +8 -0
- package/lib/local-preview-seed.ts +53 -0
- package/lib/newsroom-articles.ts +2 -0
- package/lib/newsroom-assignments.ts +13 -0
- package/lib/newsroom-board.ts +4 -0
- 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 +5 -12
- 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/priming-proof-report.ts +1 -0
- package/lib/semantic-graph.ts +5 -5
- package/lib/site-brand.ts +9 -0
- package/lib/story-reporting-trail.ts +106 -0
- package/lib/wiki.ts +56 -0
- package/package.json +14 -1
- package/routes.manifest.json +23 -7
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { permanentRedirect } from "next/navigation";
|
|
2
|
+
import { toPublicNewsroomPath } from "../../../lib/newsroom-base-path";
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
type NewsroomInsightsPageProps = {
|
|
6
|
-
searchParams?: Promise<{
|
|
7
|
-
demo?: string | string[];
|
|
8
|
-
domain?: string | string[];
|
|
9
|
-
}>;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export default async function NewsroomInsightsPage({ searchParams }: NewsroomInsightsPageProps) {
|
|
13
|
-
return <NewsDeskPage section="insights" searchParams={searchParams} />;
|
|
4
|
+
export default function NewsroomMessagesRedirectPage() {
|
|
5
|
+
permanentRedirect(toPublicNewsroomPath("/newsroom/messages"));
|
|
14
6
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
|
|
6
|
+
type Excerpt = { issueId: string; text: string };
|
|
7
|
+
|
|
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>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 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.
|
|
19
|
+
*/
|
|
20
|
+
export function BoardCardExcerpts({ excerpts, scope }: { excerpts: readonly Excerpt[]; scope: HTMLElement | null }) {
|
|
21
|
+
const [hosts, setHosts] = useState<{ host: HTMLElement; text: string }[]>([]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
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);
|
|
38
|
+
};
|
|
39
|
+
collect();
|
|
40
|
+
const observer = new MutationObserver(collect);
|
|
41
|
+
observer.observe(scope, { childList: true, subtree: true });
|
|
42
|
+
return () => observer.disconnect();
|
|
43
|
+
}, [excerpts, scope]);
|
|
44
|
+
return <>{hosts.map(({ host, text }, index) => createPortal(<CardExcerpt text={text} />, host, `${index}`))}</>;
|
|
45
|
+
}
|
|
@@ -21,6 +21,8 @@ type PreviewContext = {
|
|
|
21
21
|
sidecar: PreviewSidecar;
|
|
22
22
|
citationNumbers: Map<string, number>;
|
|
23
23
|
imageUrls: Record<string, string>;
|
|
24
|
+
dynamicDirectives: Record<string, (attributes: Record<string, unknown>) => ReactNode>;
|
|
25
|
+
renderLink?: (href: string, children: ReactNode) => ReactNode | null;
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -62,7 +64,7 @@ function imagePlaceholder(token: string, context: PreviewContext): ReactNode {
|
|
|
62
64
|
// eslint-disable-next-line @next/next/no-img-element
|
|
63
65
|
<img
|
|
64
66
|
alt={alt}
|
|
65
|
-
className="my-3 h-auto max-w-full
|
|
67
|
+
className="my-3 h-auto max-w-full"
|
|
66
68
|
data-markus-preview-image-loaded
|
|
67
69
|
loading="lazy"
|
|
68
70
|
src={url}
|
|
@@ -112,7 +114,9 @@ function renderInlines(inlines: MarkusInline[], context: PreviewContext): ReactN
|
|
|
112
114
|
return <s key={index}>{renderInlines(node.children, context)}</s>;
|
|
113
115
|
case "code_span":
|
|
114
116
|
return <code className="rounded bg-muted px-1 py-0.5 font-mono text-[0.85em]" key={index}>{node.code}</code>;
|
|
115
|
-
case "link":
|
|
117
|
+
case "link": {
|
|
118
|
+
const custom = node.href ? context.renderLink?.(node.href, renderInlines(node.children, context)) : null;
|
|
119
|
+
if (custom) return <span key={index}>{custom}</span>;
|
|
116
120
|
return node.href ? (
|
|
117
121
|
<a className="text-primary underline" href={node.href} key={index} rel="noopener noreferrer" target="_blank">
|
|
118
122
|
{renderInlines(node.children, context)}
|
|
@@ -120,6 +124,7 @@ function renderInlines(inlines: MarkusInline[], context: PreviewContext): ReactN
|
|
|
120
124
|
) : (
|
|
121
125
|
<span key={index}>{renderInlines(node.children, context)}</span>
|
|
122
126
|
);
|
|
127
|
+
}
|
|
123
128
|
case "image":
|
|
124
129
|
return (
|
|
125
130
|
<span className="rounded border border-dashed border-border px-1 text-xs text-muted-foreground" key={index}>
|
|
@@ -228,6 +233,36 @@ function renderBlock(block: MarkusBlock, index: number, context: PreviewContext)
|
|
|
228
233
|
}
|
|
229
234
|
}
|
|
230
235
|
|
|
236
|
+
function normalizedFigureSource(src: string): string {
|
|
237
|
+
return src.replace(/^(\.\.?\/)+/, "");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function renderFigure(directive: MarkusDirective, index: number, context: PreviewContext): ReactNode {
|
|
241
|
+
const attribute = (name: string) => typeof directive.attributes[name] === "string" ? String(directive.attributes[name]) : "";
|
|
242
|
+
const src = attribute("src");
|
|
243
|
+
const alt = attribute("alt");
|
|
244
|
+
const caption = attribute("caption");
|
|
245
|
+
const credit = attribute("credit");
|
|
246
|
+
const url = context.imageUrls[src] ?? context.imageUrls[normalizedFigureSource(src)];
|
|
247
|
+
return (
|
|
248
|
+
<figure className="my-4" data-markus-preview-directive="figure" key={index}>
|
|
249
|
+
{url ? (
|
|
250
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
251
|
+
<img alt={alt} className="h-auto max-w-full" data-markus-preview-image-loaded loading="lazy" src={url} />
|
|
252
|
+
) : (
|
|
253
|
+
<div className="border border-dashed border-border bg-muted/30 px-3 py-6 text-center text-xs text-muted-foreground" data-markus-preview-image>
|
|
254
|
+
{`Figure: ${src}${alt ? ` (${alt})` : ""}`}
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
{caption || credit ? (
|
|
258
|
+
<figcaption className="mt-1 text-sm text-muted-foreground">
|
|
259
|
+
{caption}{caption && credit ? " " : ""}{credit ? <span className="text-xs">{credit}</span> : null}
|
|
260
|
+
</figcaption>
|
|
261
|
+
) : null}
|
|
262
|
+
</figure>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
231
266
|
function renderDirective(directive: MarkusDirective, index: number, context: PreviewContext): ReactNode {
|
|
232
267
|
if (directive.name === "pull-quote") {
|
|
233
268
|
const attribution = directive.attributes.attribution;
|
|
@@ -244,6 +279,9 @@ function renderDirective(directive: MarkusDirective, index: number, context: Pre
|
|
|
244
279
|
</blockquote>
|
|
245
280
|
);
|
|
246
281
|
}
|
|
282
|
+
if (directive.name === "figure") return renderFigure(directive, index, context);
|
|
283
|
+
const dynamicRenderer = context.dynamicDirectives[directive.name];
|
|
284
|
+
if (dynamicRenderer) return <div data-markus-preview-directive={directive.name} key={index}>{dynamicRenderer(directive.attributes)}</div>;
|
|
247
285
|
const attributeEntries = Object.entries(directive.attributes).filter(([, value]) => value !== null && value !== "");
|
|
248
286
|
return (
|
|
249
287
|
<section
|
|
@@ -275,9 +313,14 @@ function renderBlocks(nodes: MarkusNode[], context: PreviewContext): ReactNode[]
|
|
|
275
313
|
export function MarkusIrPreview({
|
|
276
314
|
bodyIr,
|
|
277
315
|
imageUrls = {},
|
|
316
|
+
dynamicDirectives = {},
|
|
317
|
+
renderLink,
|
|
278
318
|
}: {
|
|
279
319
|
bodyIr: unknown;
|
|
280
320
|
imageUrls?: Record<string, string>;
|
|
321
|
+
/** Live components for leaf directives whose content the host app computes, keyed by directive name. */
|
|
322
|
+
dynamicDirectives?: PreviewContext["dynamicDirectives"];
|
|
323
|
+
renderLink?: PreviewContext["renderLink"];
|
|
281
324
|
}) {
|
|
282
325
|
try {
|
|
283
326
|
const envelope = typeof bodyIr === "string" ? JSON.parse(bodyIr) : bodyIr;
|
|
@@ -288,6 +331,8 @@ export function MarkusIrPreview({
|
|
|
288
331
|
sidecar,
|
|
289
332
|
citationNumbers: numberCitationsByFirstAppearance(envelope.document, sidecar),
|
|
290
333
|
imageUrls,
|
|
334
|
+
dynamicDirectives,
|
|
335
|
+
renderLink,
|
|
291
336
|
};
|
|
292
337
|
return <div data-markus-preview>{renderBlocks(document.children, context)}</div>;
|
|
293
338
|
} catch (error) {
|
|
@@ -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,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { isEditorialWorkAssignment } from "../lib/newsroom-assignments";
|
|
3
4
|
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
4
|
-
import { createLocalPreviewAssignments, createLocalPreviewContentItemVersions, createLocalPreviewReferences } from "../lib/local-preview-seed";
|
|
5
|
+
import { createLocalPreviewRealPublishedVersions, createLocalPreviewAssignments, createLocalPreviewContentItemVersions, createLocalPreviewReferences } from "../lib/local-preview-seed";
|
|
5
6
|
import { generateClient } from "aws-amplify/data";
|
|
6
7
|
import { testEditorOverrideAllowed } from "../lib/test-editor-override";
|
|
7
8
|
import { downloadData, getUrl } from "aws-amplify/storage";
|
|
@@ -690,16 +691,11 @@ type NewsroomSemanticRelationPageOptions = NewsroomPageOptions & {
|
|
|
690
691
|
|
|
691
692
|
export type ForumThreadWithMessages = MessageThreadRecord & {
|
|
692
693
|
messages: MessageRecord[];
|
|
693
|
-
scope: "edition" | "section"
|
|
694
|
+
scope: "edition" | "section";
|
|
694
695
|
sectionKey?: string | null;
|
|
695
696
|
sectionTitle?: string | null;
|
|
696
697
|
};
|
|
697
698
|
|
|
698
|
-
export type InsightForumThreadsPage = {
|
|
699
|
-
threads: ForumThreadWithMessages[];
|
|
700
|
-
nextToken: string | null;
|
|
701
|
-
};
|
|
702
|
-
|
|
703
699
|
export type EditionForumThreadsResult = {
|
|
704
700
|
editionId: string;
|
|
705
701
|
editionThreads: ForumThreadWithMessages[];
|
|
@@ -1097,6 +1093,27 @@ export async function loadEditorProcedureData(): Promise<{
|
|
|
1097
1093
|
};
|
|
1098
1094
|
}
|
|
1099
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
|
+
|
|
1100
1117
|
export async function saveProcedureDefinitionRecord(input: Record<string, unknown>) {
|
|
1101
1118
|
const dataClient = generateClient<Schema>();
|
|
1102
1119
|
const graphClient = dataClient as unknown as {
|
|
@@ -1277,148 +1294,6 @@ export async function loadEditionForumThreads(options: {
|
|
|
1277
1294
|
};
|
|
1278
1295
|
}
|
|
1279
1296
|
|
|
1280
|
-
export async function loadInsightForumThreads(options: {
|
|
1281
|
-
domain?: string;
|
|
1282
|
-
limit?: number;
|
|
1283
|
-
nextToken?: string | null;
|
|
1284
|
-
includeMessages?: boolean;
|
|
1285
|
-
} = {}): Promise<InsightForumThreadsPage> {
|
|
1286
|
-
const testMock = getTestEditorNewsroomMock();
|
|
1287
|
-
if (testMock?.insightForumThreads) {
|
|
1288
|
-
const domain = String(options.domain || "").trim();
|
|
1289
|
-
let threads = testMock.insightForumThreads;
|
|
1290
|
-
if (domain) threads = threads.filter((thread) => thread.sectionKey === domain);
|
|
1291
|
-
return { threads, nextToken: null };
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
const limit = Math.max(1, Math.min(options.limit ?? NEWSROOM_PAGE_LIMIT, 100));
|
|
1295
|
-
const domain = String(options.domain || "").trim();
|
|
1296
|
-
const includeMessages = options.includeMessages !== false;
|
|
1297
|
-
type KindConnectionResponse = {
|
|
1298
|
-
listMessagesByKindAndCreatedAt?: {
|
|
1299
|
-
items?: Array<MessageRecord | null> | null;
|
|
1300
|
-
nextToken?: string | null;
|
|
1301
|
-
} | null;
|
|
1302
|
-
};
|
|
1303
|
-
const response: KindConnectionResponse = await runGraphql<KindConnectionResponse>(
|
|
1304
|
-
LIST_MESSAGES_BY_KIND_AND_CREATED_AT_QUERY,
|
|
1305
|
-
{
|
|
1306
|
-
messageKind: "insight",
|
|
1307
|
-
sortDirection: "DESC",
|
|
1308
|
-
limit,
|
|
1309
|
-
nextToken: options.nextToken ?? null,
|
|
1310
|
-
},
|
|
1311
|
-
);
|
|
1312
|
-
const connection = response.listMessagesByKindAndCreatedAt;
|
|
1313
|
-
const roots = (connection?.items ?? [])
|
|
1314
|
-
.filter(Boolean)
|
|
1315
|
-
.map((item) => normalizeMessageRecord(item as MessageRecord))
|
|
1316
|
-
.filter(isInsightThreadRootMessage)
|
|
1317
|
-
.filter((message) => !domain || message.messageDomain === domain);
|
|
1318
|
-
|
|
1319
|
-
const threads = await Promise.all(roots.map(async (root) => {
|
|
1320
|
-
const messages = includeMessages
|
|
1321
|
-
? await loadInsightThreadMessages(root.id, root)
|
|
1322
|
-
: [root];
|
|
1323
|
-
return buildInsightForumThread(root, messages);
|
|
1324
|
-
}));
|
|
1325
|
-
|
|
1326
|
-
return {
|
|
1327
|
-
threads,
|
|
1328
|
-
nextToken: connection?.nextToken ?? null,
|
|
1329
|
-
};
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
export async function loadInsightForumThreadById(
|
|
1333
|
-
threadId: string,
|
|
1334
|
-
options: { includeMessages?: boolean } = {},
|
|
1335
|
-
): Promise<ForumThreadWithMessages | null> {
|
|
1336
|
-
const normalized = String(threadId || "").trim();
|
|
1337
|
-
if (!normalized) return null;
|
|
1338
|
-
const testMock = getTestEditorNewsroomMock();
|
|
1339
|
-
if (testMock?.insightForumThreads) {
|
|
1340
|
-
return testMock.insightForumThreads.find((thread) => thread.id === normalized) ?? null;
|
|
1341
|
-
}
|
|
1342
|
-
const root = await getMessageRecordById(normalized);
|
|
1343
|
-
if (!root || !isInsightThreadRootMessage(root)) return null;
|
|
1344
|
-
const includeMessages = options.includeMessages !== false;
|
|
1345
|
-
const messages = includeMessages
|
|
1346
|
-
? await loadInsightThreadMessages(normalized, root)
|
|
1347
|
-
: [root];
|
|
1348
|
-
return buildInsightForumThread(root, messages);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
export async function appendInsightThreadReplyRecord(input: {
|
|
1352
|
-
threadId: string;
|
|
1353
|
-
summary: string;
|
|
1354
|
-
content: string;
|
|
1355
|
-
role?: string;
|
|
1356
|
-
authorLabel?: string;
|
|
1357
|
-
parentMessageId?: string | null;
|
|
1358
|
-
metadata?: Record<string, unknown>;
|
|
1359
|
-
}): Promise<MessageRecord> {
|
|
1360
|
-
const threadId = String(input.threadId || "").trim();
|
|
1361
|
-
const summary = String(input.summary || "").trim();
|
|
1362
|
-
const content = String(input.content || "").trim();
|
|
1363
|
-
if (!threadId) throw new Error("threadId is required");
|
|
1364
|
-
if (!summary) throw new Error("summary is required");
|
|
1365
|
-
if (!content) throw new Error("content is required");
|
|
1366
|
-
const root = await getMessageRecordById(threadId);
|
|
1367
|
-
if (!root || !isInsightThreadRootMessage(root)) throw new Error(`Insight thread not found: ${threadId}`);
|
|
1368
|
-
const existing = await listMessagesByThreadId(threadId, 1000);
|
|
1369
|
-
const nextSequence = Math.max(1, ...existing.map((message) => Number(message.sequenceNumber || 0))) + 1;
|
|
1370
|
-
const now = new Date().toISOString();
|
|
1371
|
-
const message: MessageRecord = {
|
|
1372
|
-
id: `message-insight-reply-${safeForumId(threadId)}-${String(nextSequence).padStart(4, "0")}`,
|
|
1373
|
-
messageKind: "insight_reply",
|
|
1374
|
-
messageDomain: root.messageDomain ?? "knowledge",
|
|
1375
|
-
status: "active",
|
|
1376
|
-
summary,
|
|
1377
|
-
source: "newsroom.insights",
|
|
1378
|
-
authorLabel: input.authorLabel?.trim() || "editor",
|
|
1379
|
-
threadId,
|
|
1380
|
-
parentMessageId: input.parentMessageId?.trim() || null,
|
|
1381
|
-
sequenceNumber: nextSequence,
|
|
1382
|
-
role: input.role || "human",
|
|
1383
|
-
messageType: "insight_forum_reply",
|
|
1384
|
-
content,
|
|
1385
|
-
metadata: input.metadata ?? {},
|
|
1386
|
-
createdAt: now,
|
|
1387
|
-
updatedAt: now,
|
|
1388
|
-
newsroomFeedKey: "messages",
|
|
1389
|
-
};
|
|
1390
|
-
const created = await runGraphql<{ createMessage?: MessageRecord | null }>(
|
|
1391
|
-
CREATE_MESSAGE_MUTATION,
|
|
1392
|
-
{ input: message },
|
|
1393
|
-
);
|
|
1394
|
-
return normalizeMessageRecord(created.createMessage ?? message);
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
export async function deleteInsightThreadMessageRecord(input: {
|
|
1398
|
-
threadId: string;
|
|
1399
|
-
messageId: string;
|
|
1400
|
-
}): Promise<MessageRecord> {
|
|
1401
|
-
const threadId = String(input.threadId || "").trim();
|
|
1402
|
-
const messageId = String(input.messageId || "").trim();
|
|
1403
|
-
if (!threadId) throw new Error("threadId is required");
|
|
1404
|
-
if (!messageId) throw new Error("messageId is required");
|
|
1405
|
-
const existing = await listMessagesByThreadId(threadId, 1000);
|
|
1406
|
-
const target = existing.find((message) => message.id === messageId);
|
|
1407
|
-
if (!target) throw new Error(`Message not found in thread: ${messageId}`);
|
|
1408
|
-
const now = new Date().toISOString();
|
|
1409
|
-
const updatedMessage = await runGraphql<{ updateMessage?: MessageRecord | null }>(
|
|
1410
|
-
UPDATE_MESSAGE_MUTATION,
|
|
1411
|
-
{
|
|
1412
|
-
input: {
|
|
1413
|
-
id: target.id,
|
|
1414
|
-
status: "deleted",
|
|
1415
|
-
updatedAt: now,
|
|
1416
|
-
},
|
|
1417
|
-
},
|
|
1418
|
-
);
|
|
1419
|
-
return normalizeMessageRecord(updatedMessage.updateMessage ?? { ...target, status: "deleted", updatedAt: now });
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
1297
|
export async function ensureEditionForumThreadRecord(input: {
|
|
1423
1298
|
editionId: string;
|
|
1424
1299
|
title?: string;
|
|
@@ -1682,14 +1557,6 @@ async function listMessagesByThreadId(threadId: string, limit = 400): Promise<Me
|
|
|
1682
1557
|
return records;
|
|
1683
1558
|
}
|
|
1684
1559
|
|
|
1685
|
-
function isInsightThreadRootMessage(message: MessageRecord): boolean {
|
|
1686
|
-
if (String(message.messageKind || "") !== "insight") return false;
|
|
1687
|
-
const threadId = String(message.threadId || "").trim();
|
|
1688
|
-
const sequenceNumber = Number(message.sequenceNumber || 0);
|
|
1689
|
-
if (threadId && threadId !== message.id && sequenceNumber > 1) return false;
|
|
1690
|
-
return true;
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
1560
|
async function getMessageRecordById(messageId: string): Promise<MessageRecord | null> {
|
|
1694
1561
|
const normalized = String(messageId || "").trim();
|
|
1695
1562
|
if (!normalized) return null;
|
|
@@ -1701,66 +1568,6 @@ async function getMessageRecordById(messageId: string): Promise<MessageRecord |
|
|
|
1701
1568
|
return message ? normalizeMessageRecord(message) : null;
|
|
1702
1569
|
}
|
|
1703
1570
|
|
|
1704
|
-
async function loadInsightThreadMessages(threadId: string, root: MessageRecord): Promise<MessageRecord[]> {
|
|
1705
|
-
const threadMessages = await listMessagesByThreadId(threadId, 500);
|
|
1706
|
-
const byId = new Map<string, MessageRecord>();
|
|
1707
|
-
byId.set(root.id, root);
|
|
1708
|
-
for (const message of threadMessages) byId.set(message.id, message);
|
|
1709
|
-
const merged = [...byId.values()].sort(
|
|
1710
|
-
(left, right) => Number(left.sequenceNumber ?? 0) - Number(right.sequenceNumber ?? 0),
|
|
1711
|
-
);
|
|
1712
|
-
return hydrateMessageRecordsContent(merged);
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
function insightForumThreadTitle(root: MessageRecord): string {
|
|
1716
|
-
const metadata = normalizeJsonValue(root.metadata);
|
|
1717
|
-
if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
|
|
1718
|
-
const explicit = String((metadata as Record<string, unknown>).insightTitle || "").trim();
|
|
1719
|
-
if (explicit) return explicit;
|
|
1720
|
-
}
|
|
1721
|
-
const summary = String(root.summary || "").trim();
|
|
1722
|
-
return summary || "Insight";
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
function buildInsightForumThread(root: MessageRecord, messages: MessageRecord[]): ForumThreadWithMessages {
|
|
1726
|
-
const activeMessages = messages.filter((message) => String(message.status || "active") === "active");
|
|
1727
|
-
const sorted = [...activeMessages].sort(
|
|
1728
|
-
(left, right) => Number(left.sequenceNumber ?? 0) - Number(right.sequenceNumber ?? 0),
|
|
1729
|
-
);
|
|
1730
|
-
const lastMessage = sorted.at(-1) ?? root;
|
|
1731
|
-
const activityAt = lastMessage.updatedAt ?? lastMessage.createdAt ?? root.updatedAt ?? root.createdAt;
|
|
1732
|
-
return {
|
|
1733
|
-
id: root.id,
|
|
1734
|
-
threadKind: "insight_forum",
|
|
1735
|
-
status: "active",
|
|
1736
|
-
title: insightForumThreadTitle(root),
|
|
1737
|
-
summary: formatInsightForumDomainLabel(root.messageDomain),
|
|
1738
|
-
primaryAnchorKind: "message",
|
|
1739
|
-
primaryAnchorId: root.id,
|
|
1740
|
-
primaryAnchorLineageId: root.id,
|
|
1741
|
-
primaryAnchorKey: `message#${root.id}`,
|
|
1742
|
-
createdByLabel: root.authorLabel ?? null,
|
|
1743
|
-
messageCount: sorted.length,
|
|
1744
|
-
lastMessageId: lastMessage.id,
|
|
1745
|
-
lastMessageAt: activityAt,
|
|
1746
|
-
metadata: normalizeJsonValue(root.metadata) as Record<string, unknown>,
|
|
1747
|
-
createdAt: root.createdAt,
|
|
1748
|
-
updatedAt: activityAt,
|
|
1749
|
-
newsroomFeedKey: "messages",
|
|
1750
|
-
messages: sorted.length ? sorted : [root],
|
|
1751
|
-
scope: "insight",
|
|
1752
|
-
sectionKey: root.messageDomain ?? null,
|
|
1753
|
-
};
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
function formatInsightForumDomainLabel(domain: string | null | undefined): string {
|
|
1757
|
-
const value = String(domain || "").trim();
|
|
1758
|
-
if (value === "assignment_work") return "Assignment research";
|
|
1759
|
-
if (value === "knowledge") return "Reference knowledge";
|
|
1760
|
-
if (!value) return "Insight";
|
|
1761
|
-
return value.replace(/_/g, " ");
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
1571
|
async function hydrateMessageRecordsContent(messages: MessageRecord[]): Promise<MessageRecord[]> {
|
|
1765
1572
|
return Promise.all(messages.map(async (message) => {
|
|
1766
1573
|
try {
|
|
@@ -2156,17 +1963,17 @@ export async function loadNewsroomAssignmentPage(options: NewsroomAssignmentPage
|
|
|
2156
1963
|
newsroomFeedKey: "assignments",
|
|
2157
1964
|
limit: options.limit,
|
|
2158
1965
|
nextToken: options.nextToken,
|
|
2159
|
-
matches: equalityMatcher({
|
|
1966
|
+
matches: (assignment) => isEditorialWorkAssignment(assignment) && (equalityMatcher<AssignmentRecord>({
|
|
2160
1967
|
assignmentTypeKey: options.type,
|
|
2161
1968
|
status: options.status,
|
|
2162
|
-
}),
|
|
1969
|
+
})?.(assignment) ?? true),
|
|
2163
1970
|
});
|
|
2164
1971
|
}
|
|
2165
1972
|
|
|
2166
1973
|
const CONTENT_ITEMS_BY_STAGE_QUERY = `
|
|
2167
1974
|
query ListItemsByStageAndUpdatedAt($stage: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
|
|
2168
1975
|
listItemsByStageAndUpdatedAt(stage: $stage, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
|
|
2169
|
-
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 }
|
|
2170
1977
|
nextToken
|
|
2171
1978
|
}
|
|
2172
1979
|
}
|
|
@@ -2236,7 +2043,7 @@ type PublishedItemBoardRow = {
|
|
|
2236
2043
|
};
|
|
2237
2044
|
|
|
2238
2045
|
export async function loadNewsroomPublishedContentItemVersions(): Promise<ContentItemVersionRecord[]> {
|
|
2239
|
-
if (isLocalPreviewEnabled()) return
|
|
2046
|
+
if (isLocalPreviewEnabled()) return createLocalPreviewRealPublishedVersions();
|
|
2240
2047
|
configureAmplifyClient();
|
|
2241
2048
|
const client = generateClient<Schema>() as unknown as {
|
|
2242
2049
|
graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<PublishedItemBoardRow>>;
|
|
@@ -2690,7 +2497,6 @@ export function hasTestEditorOverride(): boolean {
|
|
|
2690
2497
|
|
|
2691
2498
|
type TestEditorNewsroomMock = {
|
|
2692
2499
|
forumThreadsByEdition?: Record<string, ForumThreadWithMessages[]>;
|
|
2693
|
-
insightForumThreads?: ForumThreadWithMessages[];
|
|
2694
2500
|
messages?: MessageRecord[];
|
|
2695
2501
|
payloads?: Record<string, HydratedModelPayload[]>;
|
|
2696
2502
|
referenceAttachments?: ReferenceAttachmentRecord[];
|