@anthusai/papyrus 1.10.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 +25 -220
- package/components/newsroom-article-editor.tsx +135 -39
- package/components/newsroom-assignment-detail.tsx +35 -1
- package/components/newsroom-board-view.tsx +29 -7
- 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 +196 -4
- 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 +1 -0
- package/components/wiki-concepts-view.tsx +153 -0
- package/components/wiki-index-client.ts +51 -0
- package/lib/content-actions-client.ts +3 -0
- package/lib/content-actions-demo.ts +55 -3
- package/lib/content-item-lifecycle.ts +4 -2
- package/lib/front-matter-fields.ts +42 -0
- package/lib/local-preview-seed.ts +33 -0
- package/lib/newsroom-articles.ts +2 -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 +12 -1
- package/routes.manifest.json +23 -7
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { browserPathToInternalNewsroomPath, toPublicNewsroomPath } from "./newsroom-base-path";
|
|
2
|
-
export type NewsroomIndexTab = "references" | "messages" | "assignments"
|
|
2
|
+
export type NewsroomIndexTab = "references" | "messages" | "assignments";
|
|
3
3
|
|
|
4
4
|
export type ReferenceIndexOrder = "published" | "imported";
|
|
5
5
|
|
|
6
6
|
export type ReferencesIndexFilters = { status: string; processing: string; order: ReferenceIndexOrder };
|
|
7
7
|
export type MessagesIndexFilters = { kind: string; domain: string };
|
|
8
8
|
export type AssignmentsIndexFilters = { status: string; type: string; view: string };
|
|
9
|
-
export type InsightsIndexFilters = { domain: string };
|
|
10
9
|
|
|
11
10
|
export const DEFAULT_REFERENCES_INDEX_FILTERS: ReferencesIndexFilters = {
|
|
12
11
|
status: "exclude-pending",
|
|
@@ -25,10 +24,6 @@ export const DEFAULT_ASSIGNMENTS_INDEX_FILTERS: AssignmentsIndexFilters = {
|
|
|
25
24
|
view: "queue",
|
|
26
25
|
};
|
|
27
26
|
|
|
28
|
-
export const DEFAULT_INSIGHTS_INDEX_FILTERS: InsightsIndexFilters = {
|
|
29
|
-
domain: "",
|
|
30
|
-
};
|
|
31
|
-
|
|
32
27
|
export function referencesStatusFromUrl(value: string): string {
|
|
33
28
|
return value === "exclude-pending" ? "__exclude_pending" : value;
|
|
34
29
|
}
|
|
@@ -72,12 +67,6 @@ export function effectiveAssignmentsIndexFilters(partial?: Partial<AssignmentsIn
|
|
|
72
67
|
};
|
|
73
68
|
}
|
|
74
69
|
|
|
75
|
-
export function effectiveInsightsIndexFilters(partial?: Partial<InsightsIndexFilters>): InsightsIndexFilters {
|
|
76
|
-
return {
|
|
77
|
-
domain: partial?.domain?.trim() ?? DEFAULT_INSIGHTS_INDEX_FILTERS.domain,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
70
|
export function readReferencesIndexFilters(searchParams: URLSearchParams): ReferencesIndexFilters {
|
|
82
71
|
const statusParam = searchParams.get("status")?.trim() ?? "";
|
|
83
72
|
const orderParam = searchParams.get("order")?.trim();
|
|
@@ -103,12 +92,6 @@ export function readAssignmentsIndexFilters(searchParams: URLSearchParams): Assi
|
|
|
103
92
|
});
|
|
104
93
|
}
|
|
105
94
|
|
|
106
|
-
export function readInsightsIndexFilters(searchParams: URLSearchParams): InsightsIndexFilters {
|
|
107
|
-
return effectiveInsightsIndexFilters({
|
|
108
|
-
domain: searchParams.get("domain")?.trim() ?? undefined,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
95
|
export function buildReferencesIndexQuery(filters: ReferencesIndexFilters): string {
|
|
113
96
|
const params = new URLSearchParams();
|
|
114
97
|
if (filters.status && filters.status !== DEFAULT_REFERENCES_INDEX_FILTERS.status) {
|
|
@@ -137,26 +120,18 @@ export function buildAssignmentsIndexQuery(filters: AssignmentsIndexFilters): st
|
|
|
137
120
|
return query ? `?${query}` : "";
|
|
138
121
|
}
|
|
139
122
|
|
|
140
|
-
export function buildInsightsIndexQuery(filters: InsightsIndexFilters): string {
|
|
141
|
-
const params = new URLSearchParams();
|
|
142
|
-
if (filters.domain.trim()) params.set("domain", filters.domain.trim());
|
|
143
|
-
const query = params.toString();
|
|
144
|
-
return query ? `?${query}` : "";
|
|
145
|
-
}
|
|
146
|
-
|
|
147
123
|
export function buildNewsroomIndexWebPath(
|
|
148
124
|
tab: NewsroomIndexTab,
|
|
149
|
-
filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters
|
|
125
|
+
filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters,
|
|
150
126
|
): string {
|
|
151
127
|
if (tab === "references") return toPublicNewsroomPath(`/newsroom/references${buildReferencesIndexQuery(filters as ReferencesIndexFilters)}`);
|
|
152
128
|
if (tab === "messages") return toPublicNewsroomPath(`/newsroom/messages${buildMessagesIndexQuery(filters as MessagesIndexFilters)}`);
|
|
153
|
-
if (tab === "insights") return toPublicNewsroomPath(`/newsroom/insights${buildInsightsIndexQuery(filters as InsightsIndexFilters)}`);
|
|
154
129
|
return toPublicNewsroomPath(`/newsroom/assignments${buildAssignmentsIndexQuery(filters as AssignmentsIndexFilters)}`);
|
|
155
130
|
}
|
|
156
131
|
|
|
157
132
|
export function syncBrowserNewsroomIndexUrl(
|
|
158
133
|
tab: NewsroomIndexTab,
|
|
159
|
-
filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters
|
|
134
|
+
filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters,
|
|
160
135
|
options?: { replace?: boolean },
|
|
161
136
|
) {
|
|
162
137
|
if (typeof window === "undefined") return;
|
|
@@ -192,15 +167,3 @@ export function parseAssignmentIdFromNewsroomPathname(pathname: string | null |
|
|
|
192
167
|
return segment;
|
|
193
168
|
}
|
|
194
169
|
}
|
|
195
|
-
|
|
196
|
-
export function parseInsightThreadIdFromNewsroomPathname(pathname: string | null | undefined): string | null {
|
|
197
|
-
const internalPathname = pathname ? browserPathToInternalNewsroomPath(pathname) : null;
|
|
198
|
-
if (!internalPathname?.startsWith("/newsroom/insights/")) return null;
|
|
199
|
-
const segment = internalPathname.slice("/newsroom/insights/".length).split("/")[0]?.trim() ?? "";
|
|
200
|
-
if (!segment) return null;
|
|
201
|
-
try {
|
|
202
|
-
return decodeURIComponent(segment);
|
|
203
|
-
} catch {
|
|
204
|
-
return segment;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { generateClient } from "aws-amplify/data";
|
|
2
2
|
import { getUrl, uploadData } from "aws-amplify/storage";
|
|
3
3
|
import type { Schema } from "../amplify/data/resource";
|
|
4
|
+
import { isLocalPreviewEnabled } from "./local-preview";
|
|
5
|
+
import { createLocalPreviewMediaAssets } from "./local-preview-seed";
|
|
4
6
|
import type { MediaAssetFields, MediaAssetRecord, NewsroomMediaBackend } from "./newsroom-media";
|
|
5
7
|
|
|
6
8
|
type MediaListPage = {
|
|
@@ -113,6 +115,8 @@ export function createDemoMediaBackend(): NewsroomMediaBackend {
|
|
|
113
115
|
rows.set(fields.itemId, [...(rows.get(fields.itemId) ?? []), fields]);
|
|
114
116
|
},
|
|
115
117
|
async listMediaAssets(itemId) {
|
|
118
|
+
const previewRows = createLocalPreviewMediaAssets(itemId);
|
|
119
|
+
if (previewRows.length) return previewRows;
|
|
116
120
|
return (rows.get(itemId) ?? []).map((fields) => ({
|
|
117
121
|
id: fields.id,
|
|
118
122
|
sortKey: fields.sortKey,
|
|
@@ -122,6 +126,7 @@ export function createDemoMediaBackend(): NewsroomMediaBackend {
|
|
|
122
126
|
}));
|
|
123
127
|
},
|
|
124
128
|
async imageUrl(storagePath) {
|
|
129
|
+
if (isLocalPreviewEnabled() && storagePath.startsWith("media/")) return `/api/local-preview/media?key=${encodeURIComponent(storagePath)}`;
|
|
125
130
|
const object = objects.get(storagePath);
|
|
126
131
|
if (!object) throw new Error(`No demo object at ${storagePath}.`);
|
|
127
132
|
return URL.createObjectURL(object);
|
package/lib/newsroom-nav.ts
CHANGED
|
@@ -5,7 +5,6 @@ export type NewsroomNavTabId =
|
|
|
5
5
|
| "overview"
|
|
6
6
|
| "articles"
|
|
7
7
|
| "messages"
|
|
8
|
-
| "insights"
|
|
9
8
|
| "assignments"
|
|
10
9
|
| "references"
|
|
11
10
|
| "topics"
|
|
@@ -28,9 +27,8 @@ export const NEWSROOM_OPS_NAV: NewsroomNavItem[] = [
|
|
|
28
27
|
{ id: "assignments", label: "Assignments", detail: "Work queue", href: newsroomHref("assignments"), mobilePrimary: true },
|
|
29
28
|
{ id: "references", label: "References", detail: "Knowledge base", href: newsroomHref("references"), mobilePrimary: true },
|
|
30
29
|
{ id: "messages", label: "Messages", detail: "Forum", href: newsroomHref("messages") },
|
|
31
|
-
{ id: "insights", label: "Insights", detail: "Research threads", href: newsroomHref("insights") },
|
|
32
30
|
{ id: "topics", label: "Topics", detail: "Taxonomy", href: newsroomHref("topics"), mobilePrimary: true },
|
|
33
|
-
{ id: "concepts", label: "
|
|
31
|
+
{ id: "concepts", label: "Concept wiki", detail: "Knowledge graph", href: newsroomHref("concepts") },
|
|
34
32
|
{ id: "search", label: "Search", detail: "Knowledge base", href: newsroomHref("search") },
|
|
35
33
|
{ id: "administration", label: "Administration", detail: "Users & policies", href: newsroomHref("administration") },
|
|
36
34
|
];
|
|
@@ -8,29 +8,22 @@ export type OrganizerDestination = {
|
|
|
8
8
|
};
|
|
9
9
|
export const ORGANIZER_NAV_GROUPS: readonly { title: string; items: readonly OrganizerDestination[] }[] = [
|
|
10
10
|
{ title: "Editorial work", items: [
|
|
11
|
-
{ key: "overview", label: "Overview", description: "See the desk and choose where to begin.", path: "/newsroom", primary: true },
|
|
12
11
|
{ key: "articles", label: getContentItemName().plural, description: `Follow ${contentItemPluralLowercase} across their stages and open one to write or revise it.`, path: `/newsroom/${contentItemBoardPathSegment()}`, alsoActiveOnPaths: ["/newsroom/articles"], primary: true },
|
|
13
12
|
{ key: "assignments", label: "Assignments", description: "Follow research and reporting work across its statuses and claim it.", path: "/newsroom/assignments" },
|
|
14
13
|
{ key: "messages", label: "Messages", description: "Read and reply to newsroom conversations.", path: "/newsroom/messages", primary: true },
|
|
15
|
-
{ key: "insights", label: "Insights", description: "Discuss findings and research threads.", path: "/newsroom/insights" },
|
|
16
14
|
] },
|
|
17
15
|
{ title: "Research & knowledge", items: [
|
|
18
16
|
{ key: "references", label: "References", description: "Inspect sources and make editorial inclusion decisions.", path: "/newsroom/references", primary: true },
|
|
19
|
-
{ key: "
|
|
20
|
-
{ key: "
|
|
21
|
-
{ key: "
|
|
22
|
-
{ key: "search", label: "Knowledge search", description: "Find evidence across the knowledge base.", path: "/newsroom/search" },
|
|
17
|
+
{ key: "concepts", label: "Concepts", description: "A wiki of the entities in the knowledge graph.", path: "/newsroom/concepts" },
|
|
18
|
+
{ key: "sections", label: "Sections", description: "Organize the publication’s sections and its ontology.", path: "/newsroom/administration/sections" },
|
|
19
|
+
{ key: "desks", label: "Taxonomy", description: "Maintain topic sets and category versions, and review topic proposals.", path: "/newsroom/desks", alsoActiveOnPaths: ["/newsroom/topics"] },
|
|
23
20
|
] },
|
|
24
21
|
{ title: "Administration", items: [
|
|
25
|
-
{ key: "
|
|
26
|
-
{ key: "sections", label: "Sections", description: "Organize the publication’s sections.", path: "/newsroom/administration/sections" },
|
|
27
|
-
{ key: "procedures", label: "Procedures", description: "Manage procedure definitions and versions.", path: "/newsroom/administration/procedures", access: "admin" },
|
|
28
|
-
{ key: "users", label: "Users & identity merging", description: "Review people, roles and identity merges.", path: "/newsroom/administration/users", access: "admin" },
|
|
29
|
-
{ key: "administration", label: "Administration", description: "Open the existing CMS settings workspace.", path: "/newsroom/administration" },
|
|
22
|
+
{ key: "administration", label: "Administration", description: "Policies, procedures, users and settings.", path: "/newsroom/administration", alsoActiveOnPaths: ["/newsroom/administration/policies", "/newsroom/administration/procedures", "/newsroom/administration/users"] },
|
|
30
23
|
] },
|
|
31
24
|
];
|
|
32
25
|
const all = ORGANIZER_NAV_GROUPS.flatMap(group => group.items);
|
|
33
|
-
export const ORGANIZER_PRIMARY_NAV = ["
|
|
26
|
+
export const ORGANIZER_PRIMARY_NAV = ["articles", "references", "messages"].map(key => all.find(item => item.key === key)!);
|
|
34
27
|
|
|
35
28
|
export const ORGANIZER_SETTINGS_DESTINATION = {
|
|
36
29
|
label: "Settings", path: "/newsroom/settings", description: "Theme, motion and interface preferences.",
|
|
@@ -13,7 +13,7 @@ export function resolveNewsDeskTab(
|
|
|
13
13
|
: raw === "desks" && !organizer ? "topics" : raw;
|
|
14
14
|
switch (value) {
|
|
15
15
|
case "desks": case "administration": case "topics": case "concepts":
|
|
16
|
-
case "references": case "
|
|
16
|
+
case "references": case "messages": case "assignments": case "search":
|
|
17
17
|
return value;
|
|
18
18
|
default: return "overview";
|
|
19
19
|
}
|
package/lib/newsroom-sections.ts
CHANGED
|
@@ -50,3 +50,15 @@ export function displayNewsroomSectionShortTitle(section: NewsroomSectionRecord)
|
|
|
50
50
|
export function buildNewsroomSectionHref(sectionId: string, demo?: boolean): string {
|
|
51
51
|
return getNewsroomNavHref(`/newsroom/sections/${encodeURIComponent(sectionId)}`, demo);
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
export type NewsroomDeskKind = "core" | "rotating";
|
|
55
|
+
export type NewsroomDeskOption = { value: string; label: string; kind: NewsroomDeskKind };
|
|
56
|
+
|
|
57
|
+
export function newsroomDeskKind(section: NewsroomSectionRecord): NewsroomDeskKind {
|
|
58
|
+
return normalizeNewsroomSectionType(section.type) === "canonical" ? "core" : "rotating";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The desks a story or assignment can be placed on: the same enabled, ordered desks the Overview page lists. */
|
|
62
|
+
export function newsroomDeskOptions(sections: NewsroomSectionRecord[]): NewsroomDeskOption[] {
|
|
63
|
+
return sortNewsroomSections(sections).filter(isEnabledNewsroomSection).map(section => ({ value: section.id, label: section.title, kind: newsroomDeskKind(section) }));
|
|
64
|
+
}
|
|
@@ -3,12 +3,10 @@ import { PAPYRUS_OBJECT_KINDS, type PapyrusObjectKind } from "./papyrus-object-k
|
|
|
3
3
|
import {
|
|
4
4
|
buildNewsroomIndexWebPath,
|
|
5
5
|
effectiveAssignmentsIndexFilters,
|
|
6
|
-
effectiveInsightsIndexFilters,
|
|
7
6
|
effectiveMessagesIndexFilters,
|
|
8
7
|
effectiveReferencesIndexFilters,
|
|
9
8
|
normalizeReferenceIndexOrder,
|
|
10
9
|
type AssignmentsIndexFilters,
|
|
11
|
-
type InsightsIndexFilters,
|
|
12
10
|
type MessagesIndexFilters,
|
|
13
11
|
type NewsroomIndexTab,
|
|
14
12
|
type ReferencesIndexFilters,
|
|
@@ -27,7 +25,6 @@ export type PapyrusWebUiContext = {
|
|
|
27
25
|
const NEWSROOM_TAB_IDS = new Set([
|
|
28
26
|
"overview",
|
|
29
27
|
"messages",
|
|
30
|
-
"insights",
|
|
31
28
|
"assignments",
|
|
32
29
|
"references",
|
|
33
30
|
"topics",
|
|
@@ -123,21 +120,10 @@ export function webPathToPapyrusLocation(webPath: string): PapyrusWebUiContext {
|
|
|
123
120
|
});
|
|
124
121
|
}
|
|
125
122
|
|
|
126
|
-
if ((tab === "references" || tab === "messages" || tab === "assignments"
|
|
123
|
+
if ((tab === "references" || tab === "messages" || tab === "assignments") && segments.length === 2) {
|
|
127
124
|
return newsroomIndexLocation(tab as NewsroomIndexTab, normalized, url.searchParams);
|
|
128
125
|
}
|
|
129
126
|
|
|
130
|
-
if (tab === "insights" && segments[2]) {
|
|
131
|
-
const threadId = decodeURIComponent(segments[2]);
|
|
132
|
-
const objectUri = `papyrus://message/${encodeURIComponent(threadId)}`;
|
|
133
|
-
return location(objectUri, normalized, {
|
|
134
|
-
papyrusObjectUri: objectUri,
|
|
135
|
-
newsroomTab: "insights",
|
|
136
|
-
viewMode: "detail",
|
|
137
|
-
label: `Insight thread ${threadId}`,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
127
|
if ((tab === "references" || tab === "messages" || tab === "assignments") && segments[2]) {
|
|
142
128
|
const objectId = decodeURIComponent(segments[2]);
|
|
143
129
|
const kind = tab === "references" ? "reference" : tab === "messages" ? "message" : "assignment";
|
|
@@ -277,7 +263,7 @@ export function papyrusUriToWebPath(uri: string): PapyrusUriToWebPathResult {
|
|
|
277
263
|
}
|
|
278
264
|
|
|
279
265
|
if (NEWSROOM_TAB_IDS.has(id)) {
|
|
280
|
-
if (id === "references" || id === "messages" || id === "assignments"
|
|
266
|
+
if (id === "references" || id === "messages" || id === "assignments") {
|
|
281
267
|
const mapped = newsroomIndexUriToWebPath(raw, `${id}/index`);
|
|
282
268
|
if (mapped) return mapped;
|
|
283
269
|
}
|
|
@@ -347,9 +333,6 @@ function readIndexFiltersFromSearchParams(tab: NewsroomIndexTab, searchParams: U
|
|
|
347
333
|
kind: searchParams.get("kind")?.trim() ?? undefined,
|
|
348
334
|
domain: searchParams.get("domain")?.trim() ?? undefined,
|
|
349
335
|
});
|
|
350
|
-
if (tab === "insights") return effectiveInsightsIndexFilters({
|
|
351
|
-
domain: searchParams.get("domain")?.trim() ?? undefined,
|
|
352
|
-
});
|
|
353
336
|
return effectiveAssignmentsIndexFilters({
|
|
354
337
|
status: searchParams.get("status")?.trim() ?? undefined,
|
|
355
338
|
type: searchParams.get("type")?.trim() ?? undefined,
|
|
@@ -368,9 +351,6 @@ function buildNewsroomIndexLocationUri(tab: NewsroomIndexTab, filters: Record<st
|
|
|
368
351
|
const normalized = filters as MessagesIndexFilters;
|
|
369
352
|
if (normalized.kind?.trim()) segments.push("kind", normalized.kind.trim());
|
|
370
353
|
if (normalized.domain?.trim()) segments.push("domain", normalized.domain.trim());
|
|
371
|
-
} else if (tab === "insights") {
|
|
372
|
-
const normalized = filters as InsightsIndexFilters;
|
|
373
|
-
if (normalized.domain?.trim()) segments.push("domain", normalized.domain.trim());
|
|
374
354
|
} else {
|
|
375
355
|
const normalized = filters as AssignmentsIndexFilters;
|
|
376
356
|
if (normalized.status?.trim()) segments.push("status", normalized.status.trim());
|
|
@@ -400,7 +380,7 @@ function parseIndexUriTail(objectId: string): { tab: NewsroomIndexTab; filters:
|
|
|
400
380
|
const segments = objectId.split("/").filter(Boolean);
|
|
401
381
|
if (!segments.length) return null;
|
|
402
382
|
const tab = segments[0];
|
|
403
|
-
if (tab !== "references" && tab !== "messages" && tab !== "assignments"
|
|
383
|
+
if (tab !== "references" && tab !== "messages" && tab !== "assignments") return null;
|
|
404
384
|
if (segments.length === 1) {
|
|
405
385
|
return { tab, filters: readIndexFiltersFromSearchParams(tab, new URLSearchParams()) };
|
|
406
386
|
}
|
|
@@ -414,7 +394,6 @@ function parseIndexUriTail(objectId: string): { tab: NewsroomIndexTab; filters:
|
|
|
414
394
|
}
|
|
415
395
|
if (tab === "references") return { tab, filters: effectiveReferencesIndexFilters(filters) };
|
|
416
396
|
if (tab === "messages") return { tab, filters: effectiveMessagesIndexFilters(filters) };
|
|
417
|
-
if (tab === "insights") return { tab, filters: effectiveInsightsIndexFilters(filters) };
|
|
418
397
|
return { tab, filters: effectiveAssignmentsIndexFilters(filters) };
|
|
419
398
|
}
|
|
420
399
|
|
|
@@ -36,6 +36,7 @@ export const primingProofReportSchema = z.object({
|
|
|
36
36
|
fingerprint: z.string().min(1),
|
|
37
37
|
frozenAt: z.string().min(1),
|
|
38
38
|
source: z.string().min(1),
|
|
39
|
+
design: z.string().min(1).optional(),
|
|
39
40
|
}),
|
|
40
41
|
priming: z.object({
|
|
41
42
|
acceptedExamples: z.number().int().min(0),
|
package/lib/semantic-graph.ts
CHANGED
|
@@ -41,7 +41,7 @@ export type SemanticPredicateId =
|
|
|
41
41
|
| "has_editorial_form"
|
|
42
42
|
| "about"
|
|
43
43
|
| "comment"
|
|
44
|
-
| "
|
|
44
|
+
| "note_about"
|
|
45
45
|
| "ingestion_rationale"
|
|
46
46
|
| "uses_evidence"
|
|
47
47
|
| "uses_signal"
|
|
@@ -77,7 +77,7 @@ export const SEMANTIC_PREDICATES: SemanticPredicateDefinition[] = [
|
|
|
77
77
|
{ id: "reference_summary_1000_tokens", label: "1000-token reference summary", group: "summarization", inverseLabel: "1000-token summary for", contextPackTags: ["reference_curation", "research", "context_ranking"] },
|
|
78
78
|
{ id: "mentions", label: "mentions", group: "ontology", inverseLabel: "mentioned by" },
|
|
79
79
|
{ id: "has_editorial_form", label: "has editorial form", group: "editorial", inverseLabel: "items by editorial form", contextPackTags: ["editing", "publication", "assignment_context"] },
|
|
80
|
-
{ id: "
|
|
80
|
+
{ id: "note_about", label: "note about", group: "knowledge", inverseLabel: "notes", contextPackTags: ["research", "reference_graph", "editing"] },
|
|
81
81
|
{ id: "about", label: "about", group: "commentary", inverseLabel: "commentary" },
|
|
82
82
|
{ id: "comment", label: "comments on", group: "commentary", inverseLabel: "commented on by", contextPackTags: ["reference_curation", "editing", "research", "assignment_context"] },
|
|
83
83
|
{ id: "ingestion_rationale", label: "ingestion rationale for", group: "commentary", inverseLabel: "ingestion rationale", contextPackTags: ["reference_curation", "editing", "research", "assignment_context"] },
|
|
@@ -317,13 +317,13 @@ export class SemanticGraphSnapshot {
|
|
|
317
317
|
return ranked.map((entry) => entry.message);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
|
|
320
|
+
notesFor(kind: string, lineageId: string): MessageRecord[] {
|
|
321
321
|
return this.incoming(kind, lineageId)
|
|
322
322
|
.filter((relation) => relation.subjectKind === "message")
|
|
323
|
-
.filter((relation) => relationTypeKey(relation) === "
|
|
323
|
+
.filter((relation) => relationTypeKey(relation) === "note_about" || relation.predicate === "note_about")
|
|
324
324
|
.map((relation) => this.messagesById.get(relation.subjectId))
|
|
325
325
|
.filter((message): message is MessageRecord => Boolean(message))
|
|
326
|
-
.filter((message) => message.messageKind === "
|
|
326
|
+
.filter((message) => message.messageKind === "note" && message.messageDomain === "knowledge")
|
|
327
327
|
.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
328
328
|
}
|
|
329
329
|
|
package/lib/site-brand.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type SiteBrandId = string;
|
|
|
23
23
|
|
|
24
24
|
/** Brand-specific replacements for generic Pretext components. */
|
|
25
25
|
export type SiteBrandComponents = {
|
|
26
|
+
/** The mark shown at the top of the newsroom CMS sidebar in place of the app title. */
|
|
27
|
+
NewsroomLogo?: ComponentType<{ className?: string }>;
|
|
26
28
|
PictogramFigure?: ComponentType<PictogramFigureProps>;
|
|
27
29
|
BlogPageBackground?: ComponentType<BlogPageBackgroundProps>;
|
|
28
30
|
};
|
|
@@ -79,8 +81,15 @@ export type ContentItemName = {
|
|
|
79
81
|
|
|
80
82
|
export const DEFAULT_CONTENT_ITEM_NAME: ContentItemName = { singular: "Story", plural: "Stories" };
|
|
81
83
|
|
|
84
|
+
/** Browser-tab icon files under `public/`, as site-root paths. The dark variant defaults to the light one. */
|
|
85
|
+
export type SiteBrandIcons = { light: string; dark?: string; appleTouch?: string };
|
|
86
|
+
|
|
87
|
+
export const DEFAULT_SITE_BRAND_ICONS: Required<Pick<SiteBrandIcons, "light" | "dark">> & { fallback: string } = { light: "/icon-light.png", dark: "/icon-dark.png", fallback: "/icon.png" };
|
|
88
|
+
|
|
82
89
|
export type SiteBrand = {
|
|
83
90
|
id: SiteBrandId;
|
|
91
|
+
/** The browser-tab icon. Omit to use the Papyrus plant. */
|
|
92
|
+
icons?: SiteBrandIcons;
|
|
84
93
|
appTitle: string;
|
|
85
94
|
appDescription: string;
|
|
86
95
|
mastheadTitle: string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything the reporting process recorded about one story, gathered by the story's lineage id:
|
|
3
|
+
* the draft versions of the copy, the research, reporting and copywriting assignments with their events,
|
|
4
|
+
* the notes and packets in the threads anchored to the story or its assignments, the references it draws on,
|
|
5
|
+
* and attached files. See docs/content-item-lifecycle.md for how these records join.
|
|
6
|
+
*/
|
|
7
|
+
export type TrailVersion = {
|
|
8
|
+
id: string;
|
|
9
|
+
versionNumber: number;
|
|
10
|
+
versionState: string | null;
|
|
11
|
+
status: string | null;
|
|
12
|
+
stage: string | null;
|
|
13
|
+
headline: string | null;
|
|
14
|
+
changeReason: string | null;
|
|
15
|
+
versionCreatedBy: string | null;
|
|
16
|
+
versionCreatedAt: string | null;
|
|
17
|
+
updatedAt: string | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type TrailAssignmentEvent = {
|
|
21
|
+
id: string;
|
|
22
|
+
eventType: string;
|
|
23
|
+
fromStatus: string | null;
|
|
24
|
+
toStatus: string | null;
|
|
25
|
+
actorLabel: string | null;
|
|
26
|
+
note: string | null;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type TrailAssignment = {
|
|
31
|
+
id: string;
|
|
32
|
+
title: string;
|
|
33
|
+
assignmentTypeKey: string;
|
|
34
|
+
status: string;
|
|
35
|
+
assigneeKey: string | null;
|
|
36
|
+
summary: string | null;
|
|
37
|
+
createdAt: string;
|
|
38
|
+
events: TrailAssignmentEvent[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type TrailMessage = {
|
|
42
|
+
id: string;
|
|
43
|
+
sequenceNumber?: number | null;
|
|
44
|
+
messageKind: string | null;
|
|
45
|
+
authorLabel: string | null;
|
|
46
|
+
summary: string | null;
|
|
47
|
+
content: string | null;
|
|
48
|
+
createdAt: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type TrailThread = {
|
|
52
|
+
id: string;
|
|
53
|
+
title: string | null;
|
|
54
|
+
threadKind: string | null;
|
|
55
|
+
summary: string | null;
|
|
56
|
+
messages: TrailMessage[];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type TrailReference = {
|
|
60
|
+
relationId: string;
|
|
61
|
+
predicate: string;
|
|
62
|
+
referenceId: string;
|
|
63
|
+
referenceLineageId: string | null;
|
|
64
|
+
title: string | null;
|
|
65
|
+
sourceUri: string | null;
|
|
66
|
+
/** consulted: read during reporting; cited: a citation in the article. */
|
|
67
|
+
role: "consulted" | "cited";
|
|
68
|
+
note: string | null;
|
|
69
|
+
citationKey: string | null;
|
|
70
|
+
addedBy: string | null;
|
|
71
|
+
addedAt: string | null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type TrailAttachment = {
|
|
75
|
+
id: string;
|
|
76
|
+
role: string | null;
|
|
77
|
+
filename: string | null;
|
|
78
|
+
mediaType: string | null;
|
|
79
|
+
byteSize: number | null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type StoryReportingTrail = {
|
|
83
|
+
versions: TrailVersion[];
|
|
84
|
+
assignments: TrailAssignment[];
|
|
85
|
+
threads: TrailThread[];
|
|
86
|
+
references: TrailReference[];
|
|
87
|
+
attachments: TrailAttachment[];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const EMPTY_STORY_REPORTING_TRAIL: StoryReportingTrail = { versions: [], assignments: [], threads: [], references: [], attachments: [] };
|
|
91
|
+
|
|
92
|
+
export function storyReportingTrailIsEmpty(trail: StoryReportingTrail): boolean {
|
|
93
|
+
return trail.assignments.length === 0 && trail.threads.length === 0 && trail.references.length === 0 && trail.attachments.length === 0 && trail.versions.length <= 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type StoryFeedEntry =
|
|
97
|
+
| ({ entryKind: "message"; threadTitle: string | null } & TrailMessage)
|
|
98
|
+
| { entryKind: "reference"; id: string; createdAt: string; reference: TrailReference };
|
|
99
|
+
|
|
100
|
+
/** Every message of the story's threads and every reference found for it in one chronological feed, newest first. */
|
|
101
|
+
export function storyMessageFeed(trail: StoryReportingTrail): StoryFeedEntry[] {
|
|
102
|
+
const messages: StoryFeedEntry[] = trail.threads.flatMap(thread => thread.messages.map(message => ({ ...message, entryKind: "message" as const, threadTitle: thread.title })));
|
|
103
|
+
const references: StoryFeedEntry[] = trail.references.filter(reference => reference.addedAt).map(reference => ({ entryKind: "reference" as const, id: `reference-${reference.relationId}`, createdAt: reference.addedAt as string, reference }));
|
|
104
|
+
const sequenceOf = (entry: StoryFeedEntry) => (entry.entryKind === "message" ? entry.sequenceNumber ?? 0 : 0);
|
|
105
|
+
return [...messages, ...references].sort((left, right) => right.createdAt.localeCompare(left.createdAt) || sequenceOf(right) - sequenceOf(left));
|
|
106
|
+
}
|
package/lib/wiki.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { SemanticNodeRecord } from "./category-repository";
|
|
2
|
+
|
|
3
|
+
/** The wiki's front page is an ordinary content item with this slug; its Markus body holds the dynamic components. */
|
|
4
|
+
export const WIKI_INDEX_SLUG = "wiki-index";
|
|
5
|
+
export const WIKI_PAGE_ITEM_TYPE = "wiki_page";
|
|
6
|
+
export const WIKI_CONCEPT_LINK_SCHEME = "concept:";
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_WIKI_INDEX_MARKUS = `# Concept wiki
|
|
9
|
+
|
|
10
|
+
Every page in this wiki is a concept: an entity in the knowledge graph that the newsroom has reported on, researched or cited. Open one to see what it is, what it connects to, and the references behind it.
|
|
11
|
+
|
|
12
|
+
## Recently edited
|
|
13
|
+
|
|
14
|
+
::recent-concepts{limit=10}
|
|
15
|
+
|
|
16
|
+
## Browse all concepts
|
|
17
|
+
|
|
18
|
+
::concept-list
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
export function conceptEditedAt(node: SemanticNodeRecord): string {
|
|
22
|
+
return node.updatedAt ?? node.versionCreatedAt ?? node.createdAt ?? "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function conceptTitle(node: SemanticNodeRecord): string {
|
|
26
|
+
return node.displayName?.trim() || node.nodeKey;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function conceptId(node: SemanticNodeRecord): string {
|
|
30
|
+
return node.lineageId ?? node.id;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function recentlyEditedConcepts(nodes: readonly SemanticNodeRecord[], options: { limit?: number; kind?: string | null } = {}): SemanticNodeRecord[] {
|
|
34
|
+
const { limit = 10, kind = null } = options;
|
|
35
|
+
return nodes
|
|
36
|
+
.filter(node => !kind || node.nodeKind === kind)
|
|
37
|
+
.slice()
|
|
38
|
+
.sort((left, right) => conceptEditedAt(right).localeCompare(conceptEditedAt(left)) || conceptTitle(left).localeCompare(conceptTitle(right)))
|
|
39
|
+
.slice(0, limit);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function conceptsByTitle(nodes: readonly SemanticNodeRecord[], options: { kind?: string | null; limit?: number | null } = {}): SemanticNodeRecord[] {
|
|
43
|
+
const filtered = nodes.filter(node => !options.kind || node.nodeKind === options.kind).slice().sort((left, right) => conceptTitle(left).localeCompare(conceptTitle(right)));
|
|
44
|
+
return options.limit ? filtered.slice(0, options.limit) : filtered;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Finds the concept a `concept:<key>` wiki link names, by lineage id, node key or title. */
|
|
48
|
+
export function resolveConceptLink(href: string, nodes: readonly SemanticNodeRecord[]): SemanticNodeRecord | null {
|
|
49
|
+
if (!href.startsWith(WIKI_CONCEPT_LINK_SCHEME)) return null;
|
|
50
|
+
const reference = decodeURIComponent(href.slice(WIKI_CONCEPT_LINK_SCHEME.length)).trim().toLowerCase();
|
|
51
|
+
return nodes.find(node => conceptId(node).toLowerCase() === reference || node.nodeKey.toLowerCase() === reference || conceptTitle(node).toLowerCase() === reference) ?? null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function conceptWikiPath(node: SemanticNodeRecord): string {
|
|
55
|
+
return `/newsroom/concepts/${encodeURIComponent(conceptId(node))}`;
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"./components/archive-shell": "./components/archive-shell.tsx",
|
|
76
76
|
"./components/article-video": "./components/article-video.tsx",
|
|
77
77
|
"./components/blog-page-background": "./components/blog-page-background.tsx",
|
|
78
|
+
"./components/board-card-excerpts": "./components/board-card-excerpts.tsx",
|
|
78
79
|
"./components/dev-sandbox-editor-sign-in": "./components/dev-sandbox-editor-sign-in.tsx",
|
|
79
80
|
"./components/markus-ir-preview": "./components/markus-ir-preview.tsx",
|
|
80
81
|
"./components/news-desk-client-provider": "./components/news-desk-client-provider.tsx",
|
|
@@ -100,6 +101,7 @@
|
|
|
100
101
|
"./components/presentation-header": "./components/presentation-header.tsx",
|
|
101
102
|
"./components/presentation-rhythm-hrule": "./components/presentation-rhythm-hrule.tsx",
|
|
102
103
|
"./components/priming-proof-page": "./components/priming-proof-page.tsx",
|
|
104
|
+
"./components/prospectus-account-chooser": "./components/prospectus-account-chooser.tsx",
|
|
103
105
|
"./components/publication-cyclotron-panel": "./components/publication-cyclotron-panel.tsx",
|
|
104
106
|
"./components/reader-auth-control": "./components/reader-auth-control.tsx",
|
|
105
107
|
"./components/reader-auth-state": "./components/reader-auth-state.ts",
|
|
@@ -114,6 +116,8 @@
|
|
|
114
116
|
"./components/settings-page": "./components/settings-page.tsx",
|
|
115
117
|
"./components/signup-entries-panel": "./components/signup-entries-panel.tsx",
|
|
116
118
|
"./components/staging-banner": "./components/staging-banner.tsx",
|
|
119
|
+
"./components/story-reporting-trail-client": "./components/story-reporting-trail-client.ts",
|
|
120
|
+
"./components/story-reporting-trail": "./components/story-reporting-trail.tsx",
|
|
117
121
|
"./components/topic-steering-workspace": "./components/topic-steering-workspace.tsx",
|
|
118
122
|
"./components/ui/badge": "./components/ui/badge.tsx",
|
|
119
123
|
"./components/ui/button-group": "./components/ui/button-group.tsx",
|
|
@@ -139,6 +143,8 @@
|
|
|
139
143
|
"./components/use-paged-content-item-versions": "./components/use-paged-content-item-versions.ts",
|
|
140
144
|
"./components/use-resolved-papyrus-theme": "./components/use-resolved-papyrus-theme.ts",
|
|
141
145
|
"./components/use-rhythm-overlay": "./components/use-rhythm-overlay.ts",
|
|
146
|
+
"./components/wiki-concepts-view": "./components/wiki-concepts-view.tsx",
|
|
147
|
+
"./components/wiki-index-client": "./components/wiki-index-client.ts",
|
|
142
148
|
"./lib/amplify-outputs-guard": "./lib/amplify-outputs-guard.ts",
|
|
143
149
|
"./lib/amplify-outputs-path": "./lib/amplify-outputs-path.ts",
|
|
144
150
|
"./lib/amplify-server-runtime": "./lib/amplify-server-runtime.ts",
|
|
@@ -174,6 +180,7 @@
|
|
|
174
180
|
"./lib/edition-sections": "./lib/edition-sections.ts",
|
|
175
181
|
"./lib/empty-edition-layout-plan": "./lib/empty-edition-layout-plan.ts",
|
|
176
182
|
"./lib/excerpts": "./lib/excerpts.ts",
|
|
183
|
+
"./lib/front-matter-fields": "./lib/front-matter-fields.ts",
|
|
177
184
|
"./lib/front-mosaic": "./lib/front-mosaic.ts",
|
|
178
185
|
"./lib/furniture": "./lib/furniture.ts",
|
|
179
186
|
"./lib/graphql-content-repository": "./lib/graphql-content-repository.ts",
|
|
@@ -259,6 +266,7 @@
|
|
|
259
266
|
"./lib/staging-gate": "./lib/staging-gate.ts",
|
|
260
267
|
"./lib/staging-gated-path": "./lib/staging-gated-path.ts",
|
|
261
268
|
"./lib/staging-preview-object": "./lib/staging-preview-object.ts",
|
|
269
|
+
"./lib/story-reporting-trail": "./lib/story-reporting-trail.ts",
|
|
262
270
|
"./lib/test-editor-override": "./lib/test-editor-override.ts",
|
|
263
271
|
"./lib/text-columns": "./lib/text-columns.ts",
|
|
264
272
|
"./lib/themed-image": "./lib/themed-image.ts",
|
|
@@ -266,6 +274,7 @@
|
|
|
266
274
|
"./lib/video-loading": "./lib/video-loading.ts",
|
|
267
275
|
"./lib/video-mode": "./lib/video-mode.ts",
|
|
268
276
|
"./lib/video-script": "./lib/video-script.ts",
|
|
277
|
+
"./lib/wiki": "./lib/wiki.ts",
|
|
269
278
|
"./renderers/markus/index": "./renderers/markus/index.ts",
|
|
270
279
|
"./renderers/markus/stubs": "./renderers/markus/stubs.tsx",
|
|
271
280
|
"./renderers/pretext/article-page": "./renderers/pretext/article-page.tsx",
|
|
@@ -288,6 +297,8 @@
|
|
|
288
297
|
"./app/api/auth/prospectus/sign-in/route": "./app/api/auth/prospectus/sign-in/route.ts",
|
|
289
298
|
"./app/api/auth/prospectus/sign-out/route": "./app/api/auth/prospectus/sign-out/route.ts",
|
|
290
299
|
"./app/api/cyclotron/priming-proof/route": "./app/api/cyclotron/priming-proof/route.ts",
|
|
300
|
+
"./app/api/local-preview/derive/route": "./app/api/local-preview/derive/route.ts",
|
|
301
|
+
"./app/api/local-preview/media/route": "./app/api/local-preview/media/route.ts",
|
|
291
302
|
"./app/api/media/[...path]/route": "./app/api/media/[...path]/route.ts",
|
|
292
303
|
"./app/api/revalidate/route": "./app/api/revalidate/route.ts",
|
|
293
304
|
"./app/archive/page": "./app/archive/page.tsx",
|
|
@@ -354,7 +365,7 @@
|
|
|
354
365
|
"optional": true
|
|
355
366
|
}
|
|
356
367
|
},
|
|
357
|
-
"version": "1.
|
|
368
|
+
"version": "1.11.0",
|
|
358
369
|
"dependencies": {
|
|
359
370
|
"@aws-amplify/adapter-nextjs": "^1.7.3",
|
|
360
371
|
"@aws-crypto/sha256-js": "5.2.0",
|