@anthusai/papyrus 0.0.0 → 1.0.0-next.1
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/LICENSE +21 -0
- package/amplify/auth/resource.js +61 -0
- package/amplify/auth/resource.ts +65 -0
- package/amplify/data/resource.js +2037 -0
- package/amplify/data/resource.ts +2148 -0
- package/amplify/data/schema.js +1850 -0
- package/amplify/data/schema.ts +1953 -0
- package/amplify/functions/assignment-action/handler.ts +1416 -0
- package/amplify/functions/assignment-action/resource.js +8 -0
- package/amplify/functions/assignment-action/resource.ts +9 -0
- package/amplify/functions/category-action/handler.ts +2259 -0
- package/amplify/functions/category-action/resource.js +8 -0
- package/amplify/functions/category-action/resource.ts +9 -0
- package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
- package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
- package/amplify/functions/console-chat-responder/Dockerfile +19 -0
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
- package/amplify/functions/console-chat-responder/resource.js +153 -0
- package/amplify/functions/console-chat-responder/resource.ts +179 -0
- package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
- package/amplify/functions/email-submission-processor/handler.py +46 -0
- package/amplify/functions/email-submission-processor/requirements.txt +2 -0
- package/amplify/functions/email-submission-processor/resource.js +56 -0
- package/amplify/functions/email-submission-processor/resource.ts +69 -0
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
- package/amplify/functions/knowledge-query/handler.py +1 -0
- package/amplify/functions/knowledge-query/requirements.txt +3 -0
- package/amplify/functions/knowledge-query/resource.js +50 -0
- package/amplify/functions/knowledge-query/resource.ts +61 -0
- package/amplify/functions/manage-user-role/handler.ts +799 -0
- package/amplify/functions/manage-user-role/resource.js +8 -0
- package/amplify/functions/manage-user-role/resource.ts +9 -0
- package/amplify/functions/model-attachment-upload/handler.ts +387 -0
- package/amplify/functions/model-attachment-upload/resource.js +8 -0
- package/amplify/functions/model-attachment-upload/resource.ts +9 -0
- package/amplify/functions/newsroom-summary/handler.ts +321 -0
- package/amplify/functions/newsroom-summary/resource.js +8 -0
- package/amplify/functions/newsroom-summary/resource.ts +9 -0
- package/amplify/functions/procedure-action/handler.ts +621 -0
- package/amplify/functions/procedure-action/resource.js +8 -0
- package/amplify/functions/procedure-action/resource.ts +9 -0
- package/amplify/functions/reader-settings/handler.ts +322 -0
- package/amplify/functions/reader-settings/resource.js +8 -0
- package/amplify/functions/reader-settings/resource.ts +9 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
- package/amplify/functions/ses-inbound-receive/resource.js +8 -0
- package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
- package/amplify/functions/shared/amplify-secrets.ts +65 -0
- package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
- package/amplify/functions/shared/email-mime-intake.ts +196 -0
- package/amplify/functions/shared/email-submission.ts +415 -0
- package/amplify/functions/shared/inbound-email-intake.ts +37 -0
- package/amplify/functions/shared/lambda-data-client.ts +21 -0
- package/amplify/functions/shared/model-payloads.ts +186 -0
- package/amplify/functions/shared/python-bundle.js +115 -0
- package/amplify/functions/shared/python-bundle.ts +141 -0
- package/amplify/functions/shared/slack-events.ts +111 -0
- package/amplify/functions/slack-delivery/handler.py +112 -0
- package/amplify/functions/slack-delivery/requirements.txt +2 -0
- package/amplify/functions/slack-delivery/resource.js +38 -0
- package/amplify/functions/slack-delivery/resource.ts +51 -0
- package/amplify/functions/slack-events/handler.ts +155 -0
- package/amplify/functions/slack-events/resource.js +11 -0
- package/amplify/functions/slack-events/resource.ts +12 -0
- package/amplify/inbound-email/stack.js +46 -0
- package/amplify/inbound-email/stack.ts +61 -0
- package/amplify/package.json +3 -0
- package/amplify/site-backend-config.js +1 -0
- package/amplify/site-backend-config.ts +30 -0
- package/amplify/site-backend.js +587 -0
- package/amplify/site-backend.ts +704 -0
- package/amplify/storage/resource.js +18 -0
- package/amplify/storage/resource.ts +19 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
- package/app/[year]/[month]/[day]/page.tsx +22 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
- package/app/api/archive/editions/route.ts +18 -0
- package/app/api/media/[...path]/route.ts +35 -0
- package/app/api/revalidate/route.ts +94 -0
- package/app/archive/page.tsx +24 -0
- package/app/articles/[slug]/page.tsx +35 -0
- package/app/globals.css +10329 -0
- package/app/layout.tsx +146 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
- package/app/newsroom/[section]/page.tsx +47 -0
- package/app/newsroom/forum/[threadId]/page.tsx +35 -0
- package/app/newsroom/insights/[threadId]/page.tsx +22 -0
- package/app/newsroom/insights/page.tsx +14 -0
- package/app/newsroom/layout.tsx +12 -0
- package/app/newsroom/page.tsx +31 -0
- package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
- package/app/page.tsx +272 -0
- package/app/settings/page.tsx +14 -0
- package/app/tailwind.css +65 -0
- package/bin/papyrus-app.mjs +10 -0
- package/bin/papyrus-infra.mjs +39 -0
- package/components/ai-elements/conversation.tsx +168 -0
- package/components/ai-elements/message.tsx +360 -0
- package/components/ai-elements/model-selector.tsx +30 -0
- package/components/ai-elements/prompt-input.tsx +1461 -0
- package/components/ai-elements/shimmer.tsx +76 -0
- package/components/ai-elements/suggestion.tsx +54 -0
- package/components/ai-elements/tool.tsx +173 -0
- package/components/amplify-client-provider.tsx +140 -0
- package/components/archive-grid.tsx +89 -0
- package/components/archive-shell.tsx +41 -0
- package/components/article-video.tsx +47 -0
- package/components/blog-page-background.tsx +20 -0
- package/components/dev-sandbox-editor-sign-in.tsx +60 -0
- package/components/news-desk-client-provider.tsx +211 -0
- package/components/news-desk-page.tsx +119 -0
- package/components/news-desk-taxonomy-client.ts +3684 -0
- package/components/newspaper.tsx +1929 -0
- package/components/newsroom-assignments-view.tsx +664 -0
- package/components/newsroom-ops-overview.tsx +226 -0
- package/components/newsroom-ops-shell.tsx +319 -0
- package/components/newsroom-references-view.tsx +598 -0
- package/components/newsroom-topics-view.tsx +425 -0
- package/components/papyrus-console-shell.tsx +1912 -0
- package/components/pictogram-figure.tsx +65 -0
- package/components/presentation-footer.tsx +146 -0
- package/components/reader-auth-control.tsx +162 -0
- package/components/reader-auth-state.ts +122 -0
- package/components/reader-settings.ts +332 -0
- package/components/reader-theme-control.tsx +85 -0
- package/components/reference-source-preview.tsx +163 -0
- package/components/settings-page.tsx +239 -0
- package/components/topic-steering-workspace.tsx +18063 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button-group.tsx +87 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/card.tsx +32 -0
- package/components/ui/command.tsx +194 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +266 -0
- package/components/ui/hover-card.tsx +51 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +16 -0
- package/components/ui/select.tsx +199 -0
- package/components/ui/separator.tsx +23 -0
- package/components/ui/sheet.tsx +92 -0
- package/components/ui/spinner.tsx +10 -0
- package/components/ui/tabs.tsx +113 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/use-resolved-papyrus-theme.ts +47 -0
- package/infra/amplify-app-shell.js +109 -0
- package/infra/amplify-app-shell.ts +155 -0
- package/infra/index.js +12 -0
- package/lib/amplify-outputs-guard.ts +24 -0
- package/lib/amplify-outputs-path.ts +16 -0
- package/lib/amplify-server-runtime.ts +63 -0
- package/lib/archive-data.ts +30 -0
- package/lib/archive-types.ts +15 -0
- package/lib/articles.ts +320 -0
- package/lib/cached-content-repository.ts +89 -0
- package/lib/category-dashboard.ts +37 -0
- package/lib/category-repository.ts +1907 -0
- package/lib/console-chat-client.ts +1316 -0
- package/lib/console-web-context.ts +118 -0
- package/lib/content-load-timing.ts +23 -0
- package/lib/content-repository.ts +40 -0
- package/lib/content-types.ts +104 -0
- package/lib/define-site.js +3 -0
- package/lib/define-site.ts +21 -0
- package/lib/demo-amplify-outputs.ts +15 -0
- package/lib/dev-sandbox-editor-auth.ts +18 -0
- package/lib/doctrine.ts +128 -0
- package/lib/edition-routes.ts +155 -0
- package/lib/edition-sections.ts +111 -0
- package/lib/empty-edition-layout-plan.ts +109 -0
- package/lib/excerpts.ts +8 -0
- package/lib/graphql-content-repository.ts +986 -0
- package/lib/image-url.ts +10 -0
- package/lib/layout-plan.ts +981 -0
- package/lib/layout-scenarios.ts +564 -0
- package/lib/markus-body.ts +168 -0
- package/lib/markus-ir.ts +235 -0
- package/lib/markus-projection.ts +243 -0
- package/lib/markus-to-article.ts +93 -0
- package/lib/news-desk-session.ts +160 -0
- package/lib/newspaper-layout.ts +3459 -0
- package/lib/newsroom-assignments.ts +173 -0
- package/lib/newsroom-base-path.ts +48 -0
- package/lib/newsroom-card-layout.ts +102 -0
- package/lib/newsroom-category-drilldown.ts +133 -0
- package/lib/newsroom-demo-dashboard.ts +267 -0
- package/lib/newsroom-demo-profile.ts +46 -0
- package/lib/newsroom-forum-routes.ts +153 -0
- package/lib/newsroom-index-filters.ts +202 -0
- package/lib/newsroom-knowledge-query-request.ts +51 -0
- package/lib/newsroom-list-selection.ts +42 -0
- package/lib/newsroom-nav.ts +42 -0
- package/lib/newsroom-references.ts +116 -0
- package/lib/newsroom-sections.ts +52 -0
- package/lib/newsroom-topics.ts +118 -0
- package/lib/papyrus-object-kinds.ts +24 -0
- package/lib/papyrus-web-locations.ts +480 -0
- package/lib/pilobol-sample.ts +167 -0
- package/lib/presentation-footer.ts +92 -0
- package/lib/pretext-layout.ts +179 -0
- package/lib/public-placeholder-config.ts +102 -0
- package/lib/publication-items.ts +181 -0
- package/lib/reader-route-config.ts +1 -0
- package/lib/reader-static-params.ts +56 -0
- package/lib/reader-storage-url.ts +63 -0
- package/lib/reference-display.ts +74 -0
- package/lib/reference-policy.ts +97 -0
- package/lib/reference-source-preview.ts +175 -0
- package/lib/renderer-config.ts +50 -0
- package/lib/renderer.ts +87 -0
- package/lib/reporting-story-budget.ts +638 -0
- package/lib/semantic-graph.ts +554 -0
- package/lib/site-brand-tenant.ts +41 -0
- package/lib/site-brand.ts +179 -0
- package/lib/site-renderer.ts +17 -0
- package/lib/site-stack.ts +195 -0
- package/lib/themed-image.ts +30 -0
- package/lib/utils.ts +6 -0
- package/middleware.ts +78 -0
- package/package.json +296 -1
- package/publications/anth_us/brand.ts +68 -0
- package/publications/anth_us/theme.css +66 -0
- package/publications/papyrus/theme.css +11 -0
- package/publications/pilobol_us/brand.ts +37 -0
- package/publications/pilobol_us/theme.css +140 -0
- package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
- package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
- package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
- package/publications/threat_intelligence/brand.ts +40 -0
- package/publications/threat_intelligence/pictograms/art.tsx +608 -0
- package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
- package/publications/threat_intelligence/pictograms/registry.ts +30 -0
- package/publications/threat_intelligence/pictograms/system.tsx +305 -0
- package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
- package/publications/threat_intelligence/theme.css +1440 -0
- package/renderers/markus/index.ts +25 -0
- package/renderers/markus/stubs.tsx +33 -0
- package/renderers/pretext/article-page.tsx +174 -0
- package/renderers/pretext/index.ts +30 -0
- package/renderers/pretext/layouts.ts +16 -0
- package/renderers/pretext/presentation-shell.tsx +477 -0
- package/routes.manifest.json +182 -0
- package/scripts/fixtures/pilobol-sample.ast.json +234 -0
- package/src/backend.js +1 -0
- package/src/backend.ts +3 -0
- package/src/sync.mjs +87 -0
- package/src/with-papyrus.d.mts +2 -0
- package/src/with-papyrus.mjs +47 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { AssignmentRecord, NewsroomSummaryRecord } from "./category-repository";
|
|
2
|
+
|
|
3
|
+
export type AssignmentStatusFilter = "all" | "open" | "claimed" | "completed" | "canceled";
|
|
4
|
+
|
|
5
|
+
export type AssignmentAction = "claim" | "release" | "complete" | "cancel" | "reopen" | "retry";
|
|
6
|
+
|
|
7
|
+
export type AssignmentTypeOption = {
|
|
8
|
+
key: string;
|
|
9
|
+
label: string;
|
|
10
|
+
count: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type AssignmentMetrics = {
|
|
14
|
+
total: number;
|
|
15
|
+
open: number;
|
|
16
|
+
claimed: number;
|
|
17
|
+
completed: number;
|
|
18
|
+
canceled: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function assignmentTypeKeyForFilter(assignment: AssignmentRecord): string {
|
|
22
|
+
return assignment.assignmentTypeKey?.trim() || "unknown";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatAssignmentTypeLabel(typeKey: string | null | undefined): string {
|
|
26
|
+
const key = typeKey?.trim();
|
|
27
|
+
if (!key) return "Uncategorized";
|
|
28
|
+
if (key === "analysis.reindex") return "Analysis re-index";
|
|
29
|
+
if (key === "newsroom.research" || key === "research") return "Research";
|
|
30
|
+
if (key === "curation.reference-intake") return "Reference intake";
|
|
31
|
+
if (key === "reporting.edition-candidate") return "Reporting candidate";
|
|
32
|
+
return key
|
|
33
|
+
.split(/[.:_-]+/)
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
36
|
+
.join(" ");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getAssignmentTypeOptions(
|
|
40
|
+
assignments: AssignmentRecord[],
|
|
41
|
+
summary?: NewsroomSummaryRecord | null,
|
|
42
|
+
): AssignmentTypeOption[] {
|
|
43
|
+
const summaryCounts = summary?.facets?.assignments?.byType ?? summary?.assignmentTypeCounts;
|
|
44
|
+
const countByType = summaryCounts && Object.keys(summaryCounts).length
|
|
45
|
+
? new Map(Object.entries(summaryCounts))
|
|
46
|
+
: new Map<string, number>();
|
|
47
|
+
if (!countByType.size) {
|
|
48
|
+
for (const assignment of assignments) {
|
|
49
|
+
const typeKey = assignmentTypeKeyForFilter(assignment);
|
|
50
|
+
countByType.set(typeKey, (countByType.get(typeKey) ?? 0) + 1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return Array.from(countByType.entries())
|
|
54
|
+
.map(([key, count]) => ({
|
|
55
|
+
key,
|
|
56
|
+
label: formatAssignmentTypeLabel(key),
|
|
57
|
+
count,
|
|
58
|
+
}))
|
|
59
|
+
.sort((left, right) => {
|
|
60
|
+
const countDiff = right.count - left.count;
|
|
61
|
+
if (countDiff !== 0) return countDiff;
|
|
62
|
+
return left.label.localeCompare(right.label);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getAssignmentMetrics(
|
|
67
|
+
assignments: AssignmentRecord[],
|
|
68
|
+
summary?: NewsroomSummaryRecord | null,
|
|
69
|
+
typeFilter?: string,
|
|
70
|
+
): AssignmentMetrics {
|
|
71
|
+
const statusCounts = typeFilter
|
|
72
|
+
? summary?.facets?.assignments?.statusByType?.[typeFilter]
|
|
73
|
+
: summary?.facets?.assignments?.byStatus ?? summary?.assignmentStatusCounts;
|
|
74
|
+
if (statusCounts && Object.keys(statusCounts).length) {
|
|
75
|
+
return {
|
|
76
|
+
total: Object.values(statusCounts).reduce((sum, count) => sum + count, 0),
|
|
77
|
+
open: statusCounts.open ?? 0,
|
|
78
|
+
claimed: statusCounts.claimed ?? 0,
|
|
79
|
+
completed: statusCounts.completed ?? 0,
|
|
80
|
+
canceled: statusCounts.canceled ?? 0,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
total: assignments.length,
|
|
85
|
+
open: assignments.filter((assignment) => assignment.status === "open").length,
|
|
86
|
+
claimed: assignments.filter((assignment) => assignment.status === "claimed").length,
|
|
87
|
+
completed: assignments.filter((assignment) => assignment.status === "completed").length,
|
|
88
|
+
canceled: assignments.filter((assignment) => assignment.status === "canceled").length,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function assignmentStatusRank(status: string): number {
|
|
93
|
+
if (status === "open") return 0;
|
|
94
|
+
if (status === "claimed") return 1;
|
|
95
|
+
if (status === "completed") return 6;
|
|
96
|
+
if (status === "canceled") return 8;
|
|
97
|
+
return 5;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function compareAssignments(left: AssignmentRecord, right: AssignmentRecord): number {
|
|
101
|
+
const leftStatus = assignmentStatusRank(left.status);
|
|
102
|
+
const rightStatus = assignmentStatusRank(right.status);
|
|
103
|
+
if (leftStatus !== rightStatus) return leftStatus - rightStatus;
|
|
104
|
+
const priorityDiff = (left.priority ?? 999999) - (right.priority ?? 999999);
|
|
105
|
+
if (priorityDiff !== 0) return priorityDiff;
|
|
106
|
+
return left.createdAt.localeCompare(right.createdAt);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function countAssignmentsByStatus(assignments: AssignmentRecord[]): Record<string, number> {
|
|
110
|
+
const counts: Record<string, number> = {};
|
|
111
|
+
for (const assignment of assignments) {
|
|
112
|
+
const status = assignment.status?.trim().toLowerCase() || "unknown";
|
|
113
|
+
counts[status] = (counts[status] ?? 0) + 1;
|
|
114
|
+
}
|
|
115
|
+
return counts;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function filterAssignmentsByStatus(
|
|
119
|
+
assignments: AssignmentRecord[],
|
|
120
|
+
statusFilter: AssignmentStatusFilter,
|
|
121
|
+
): AssignmentRecord[] {
|
|
122
|
+
const sorted = [...assignments].sort(compareAssignments);
|
|
123
|
+
if (statusFilter === "all") return sorted;
|
|
124
|
+
return sorted.filter((assignment) => assignment.status === statusFilter);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function formatAssignmentListDate(assignment: AssignmentRecord): string {
|
|
128
|
+
const value = assignment.updatedAt ?? assignment.createdAt ?? "";
|
|
129
|
+
if (!value) return "Undated";
|
|
130
|
+
const timestamp = Date.parse(value);
|
|
131
|
+
if (Number.isNaN(timestamp)) return value;
|
|
132
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
133
|
+
month: "short",
|
|
134
|
+
day: "numeric",
|
|
135
|
+
year: "numeric",
|
|
136
|
+
timeZone: "UTC",
|
|
137
|
+
}).format(new Date(timestamp));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function assignmentStatusLabel(status: string): string {
|
|
141
|
+
const normalized = status?.trim().toLowerCase() ?? "";
|
|
142
|
+
if (normalized === "open") return "Open";
|
|
143
|
+
if (normalized === "claimed") return "Claimed";
|
|
144
|
+
if (normalized === "completed") return "Completed";
|
|
145
|
+
if (normalized === "canceled") return "Canceled";
|
|
146
|
+
return status || "Unknown";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function assignmentStatusBadgeVariant(
|
|
150
|
+
status: string,
|
|
151
|
+
): "default" | "secondary" | "outline" | "muted" {
|
|
152
|
+
const normalized = status?.trim().toLowerCase() ?? "";
|
|
153
|
+
if (normalized === "open") return "default";
|
|
154
|
+
if (normalized === "claimed") return "secondary";
|
|
155
|
+
if (normalized === "completed") return "outline";
|
|
156
|
+
if (normalized === "canceled") return "outline";
|
|
157
|
+
return "muted";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function assignmentListSubtitle(assignment: AssignmentRecord): string {
|
|
161
|
+
const typeLabel = formatAssignmentTypeLabel(assignment.assignmentTypeKey);
|
|
162
|
+
const section = assignment.sectionKey?.trim() || assignment.sectionId?.trim();
|
|
163
|
+
if (section) return `${typeLabel} · ${section}`;
|
|
164
|
+
return typeLabel;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function selectedAssignmentById(
|
|
168
|
+
assignments: AssignmentRecord[],
|
|
169
|
+
assignmentId: string | null | undefined,
|
|
170
|
+
): AssignmentRecord | null {
|
|
171
|
+
if (!assignmentId) return null;
|
|
172
|
+
return assignments.find((assignment) => assignment.id === assignmentId) ?? null;
|
|
173
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { SITE_BRAND } from "./site-brand";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_NEWSROOM_BASE_PATH = "/newsroom";
|
|
4
|
+
|
|
5
|
+
/** Internal Next.js route prefix (always `/newsroom/...`). */
|
|
6
|
+
export function getInternalNewsroomBasePath(): string {
|
|
7
|
+
return DEFAULT_NEWSROOM_BASE_PATH;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Public browser path prefix (`""` on a CMS-only newsroom subdomain). */
|
|
11
|
+
export function getPublicNewsroomBasePath(): string {
|
|
12
|
+
return SITE_BRAND.newsroomBasePath ?? DEFAULT_NEWSROOM_BASE_PATH;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function usesNewsroomRootPaths(): boolean {
|
|
16
|
+
return getPublicNewsroomBasePath() === "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function toPublicNewsroomPath(internalPath: string): string {
|
|
20
|
+
const publicBase = getPublicNewsroomBasePath();
|
|
21
|
+
if (publicBase !== "") return internalPath;
|
|
22
|
+
if (internalPath === "/newsroom" || internalPath === "/newsroom/") return "/";
|
|
23
|
+
if (internalPath.startsWith("/newsroom/")) {
|
|
24
|
+
return internalPath.slice("/newsroom".length) || "/";
|
|
25
|
+
}
|
|
26
|
+
return internalPath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function toInternalNewsroomPath(publicPath: string): string {
|
|
30
|
+
const publicBase = getPublicNewsroomBasePath();
|
|
31
|
+
const normalized = publicPath.startsWith("/") ? publicPath : `/${publicPath}`;
|
|
32
|
+
if (publicBase !== "") {
|
|
33
|
+
if (normalized === publicBase || normalized === `${publicBase}/`) return "/newsroom";
|
|
34
|
+
if (normalized.startsWith(`${publicBase}/`)) return normalized;
|
|
35
|
+
if (normalized.startsWith("/newsroom")) return normalized;
|
|
36
|
+
return `/newsroom${normalized === "/" ? "" : normalized}`;
|
|
37
|
+
}
|
|
38
|
+
if (normalized === "/" || normalized === "") return "/newsroom";
|
|
39
|
+
if (normalized.startsWith("/newsroom")) return normalized;
|
|
40
|
+
return `/newsroom${normalized}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function newsroomHref(...segments: string[]): string {
|
|
44
|
+
const suffix = segments.filter(Boolean).join("/");
|
|
45
|
+
const publicBase = getPublicNewsroomBasePath();
|
|
46
|
+
if (!suffix) return publicBase || "/";
|
|
47
|
+
return publicBase ? `${publicBase}/${suffix}` : `/${suffix}`;
|
|
48
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export type NewsroomCardSpan = "1x1" | "2x1" | "1x2" | "2x2" | "3x1" | "3x2";
|
|
2
|
+
|
|
3
|
+
export type NewsroomCardTemplateRole = "lead" | "priority" | "secondary" | "tall" | "wide" | "wideFeature" | "standard";
|
|
4
|
+
|
|
5
|
+
type NewsroomCardTemplateInput = {
|
|
6
|
+
mode: "desk" | "overview";
|
|
7
|
+
index: number;
|
|
8
|
+
bodyLength?: number;
|
|
9
|
+
isUrgent?: boolean;
|
|
10
|
+
qualityRating?: number | null;
|
|
11
|
+
sectionKey?: "assignments" | "messages" | "references" | string;
|
|
12
|
+
title?: string | null;
|
|
13
|
+
updatedAt?: string | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type NewsroomCardTemplate = {
|
|
17
|
+
role: NewsroomCardTemplateRole;
|
|
18
|
+
span: NewsroomCardSpan;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function resolveNewsroomCardTemplate({
|
|
22
|
+
bodyLength = 0,
|
|
23
|
+
index,
|
|
24
|
+
isUrgent = false,
|
|
25
|
+
mode,
|
|
26
|
+
qualityRating,
|
|
27
|
+
sectionKey,
|
|
28
|
+
title,
|
|
29
|
+
}: NewsroomCardTemplateInput): NewsroomCardTemplate {
|
|
30
|
+
if (mode === "overview") {
|
|
31
|
+
if (index === 0) return { role: "lead", span: "2x2" };
|
|
32
|
+
if (index === 3) return { role: "secondary", span: "2x1" };
|
|
33
|
+
return { role: "standard", span: "1x1" };
|
|
34
|
+
}
|
|
35
|
+
if (sectionKey === "references") {
|
|
36
|
+
return resolveReferenceDeskTemplate({
|
|
37
|
+
bodyLength,
|
|
38
|
+
index,
|
|
39
|
+
isUrgent,
|
|
40
|
+
qualityRating,
|
|
41
|
+
title: title ?? "",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (index === 0 || isUrgent && index < 3) return { role: "priority", span: "2x1" };
|
|
45
|
+
if (bodyLength > 180 || index > 0 && index % 7 === 0) return { role: "tall", span: "1x2" };
|
|
46
|
+
if (index > 0 && index % 11 === 0) return { role: "secondary", span: "2x1" };
|
|
47
|
+
return { role: "standard", span: "1x1" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveReferenceDeskTemplate({
|
|
51
|
+
bodyLength,
|
|
52
|
+
index,
|
|
53
|
+
isUrgent,
|
|
54
|
+
qualityRating,
|
|
55
|
+
title,
|
|
56
|
+
}: {
|
|
57
|
+
bodyLength: number;
|
|
58
|
+
index: number;
|
|
59
|
+
isUrgent: boolean;
|
|
60
|
+
qualityRating?: number | null;
|
|
61
|
+
title: string;
|
|
62
|
+
}): NewsroomCardTemplate {
|
|
63
|
+
if (isLowQualityRating(qualityRating)) return { role: "standard", span: "1x1" };
|
|
64
|
+
|
|
65
|
+
const pressure = headlinePressure(title);
|
|
66
|
+
const bandSlot = index % 12;
|
|
67
|
+
const isBandLead = bandSlot === 0;
|
|
68
|
+
const isBandSecondary = bandSlot === 4 || bandSlot === 8;
|
|
69
|
+
|
|
70
|
+
if (pressure === "severe") {
|
|
71
|
+
return isBandLead
|
|
72
|
+
? { role: "wideFeature", span: "3x2" }
|
|
73
|
+
: { role: "wide", span: "3x1" };
|
|
74
|
+
}
|
|
75
|
+
if (pressure === "high") {
|
|
76
|
+
return isBandLead || isBandSecondary
|
|
77
|
+
? { role: "wide", span: "3x1" }
|
|
78
|
+
: { role: "secondary", span: "2x1" };
|
|
79
|
+
}
|
|
80
|
+
if (pressure === "medium" && (bandSlot === 1 || bandSlot === 5 || bandSlot === 9)) {
|
|
81
|
+
return { role: "secondary", span: "2x1" };
|
|
82
|
+
}
|
|
83
|
+
if (bodyLength > 340 && bandSlot === 7) return { role: "tall", span: "2x2" };
|
|
84
|
+
if (bodyLength > 220 && (bandSlot === 2 || bandSlot === 7 || bandSlot === 10)) return { role: "tall", span: "1x2" };
|
|
85
|
+
if (index === 0 || isUrgent && index < 3) return { role: "priority", span: "2x1" };
|
|
86
|
+
if (index > 0 && index % 11 === 0) return { role: "secondary", span: "2x1" };
|
|
87
|
+
return { role: "standard", span: "1x1" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function headlinePressure(title: string): "low" | "medium" | "high" | "severe" {
|
|
91
|
+
const words = title.trim().split(/\s+/).filter(Boolean);
|
|
92
|
+
const characterCount = words.join(" ").length;
|
|
93
|
+
const longestWord = words.reduce((max, word) => Math.max(max, word.length), 0);
|
|
94
|
+
if (characterCount >= 96 || longestWord >= 24) return "severe";
|
|
95
|
+
if (characterCount >= 72 || longestWord >= 18) return "high";
|
|
96
|
+
if (characterCount >= 52 || longestWord >= 14) return "medium";
|
|
97
|
+
return "low";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isLowQualityRating(value: number | null | undefined): boolean {
|
|
101
|
+
return typeof value === "number" && Number.isFinite(value) && value < 3;
|
|
102
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CategorySteeringCategory,
|
|
3
|
+
SemanticRelationRecord,
|
|
4
|
+
} from "./category-repository";
|
|
5
|
+
import type {
|
|
6
|
+
SemanticNeighborGroup,
|
|
7
|
+
SemanticObjectSummary,
|
|
8
|
+
} from "./semantic-graph";
|
|
9
|
+
|
|
10
|
+
export type CategoryDrilldownContext = {
|
|
11
|
+
aliases: string[];
|
|
12
|
+
categories: CategorySteeringCategory[];
|
|
13
|
+
includeDescendants: boolean;
|
|
14
|
+
primary: CategorySteeringCategory | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type CategoryGraph = {
|
|
18
|
+
neighbors(kind: string, lineageId: string): SemanticNeighborGroup[];
|
|
19
|
+
referencesForCategory(categoryLineageId: string): SemanticObjectSummary[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function categoryLineageId(category: CategorySteeringCategory): string {
|
|
23
|
+
return category.lineageId ?? category.id;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildCategoryDrilldownContext(categories: CategorySteeringCategory[], selection?: string | null): CategoryDrilldownContext {
|
|
27
|
+
if (!selection) return { aliases: [], categories: [], includeDescendants: false, primary: null };
|
|
28
|
+
const directMatches = uniqueCategoryRecords(categories.filter((category) => matchesCategorySelection(category, selection)));
|
|
29
|
+
const primary = directMatches[0] ?? null;
|
|
30
|
+
const selectedCategoryKey = primary?.categoryKey ?? selection;
|
|
31
|
+
const matchingCategoryKeyRecords = uniqueCategoryRecords(categories.filter((category) => category.categoryKey === selectedCategoryKey));
|
|
32
|
+
const directRecords = uniqueCategoryRecords([...directMatches, ...matchingCategoryKeyRecords]);
|
|
33
|
+
const includeDescendants = directRecords.some((category) => !category.parentCategoryKey);
|
|
34
|
+
const childRecords = includeDescendants
|
|
35
|
+
? categories.filter((category) => category.parentCategoryKey === selectedCategoryKey)
|
|
36
|
+
: [];
|
|
37
|
+
const records = uniqueCategoryRecords([...directRecords, ...childRecords]);
|
|
38
|
+
return {
|
|
39
|
+
aliases: categoryAliasValues(records),
|
|
40
|
+
categories: records,
|
|
41
|
+
includeDescendants,
|
|
42
|
+
primary: primary ?? records[0] ?? null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildTopicDrilldownContext(
|
|
47
|
+
root: { category: CategorySteeringCategory; subcategorys: CategorySteeringCategory[] },
|
|
48
|
+
selected: CategorySteeringCategory,
|
|
49
|
+
categoryByUid: Map<string, CategorySteeringCategory>,
|
|
50
|
+
): CategoryDrilldownContext {
|
|
51
|
+
const selectedFlatCategory = categoryByUid.get(selected.categoryKey) ?? null;
|
|
52
|
+
const directRecords = uniqueCategoryRecords([selected, selectedFlatCategory].filter((category): category is CategorySteeringCategory => Boolean(category)));
|
|
53
|
+
const includeDescendants = selected.categoryKey === root.category.categoryKey;
|
|
54
|
+
const childRecords = includeDescendants ? root.subcategorys : [];
|
|
55
|
+
const records = uniqueCategoryRecords([...directRecords, ...childRecords]);
|
|
56
|
+
return {
|
|
57
|
+
aliases: categoryAliasValues(records),
|
|
58
|
+
categories: records,
|
|
59
|
+
includeDescendants,
|
|
60
|
+
primary: directRecords[0] ?? records[0] ?? null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function referencesForCategoryContext(graph: CategoryGraph, context: CategoryDrilldownContext): SemanticObjectSummary[] {
|
|
65
|
+
return uniqueSemanticSummaries(context.aliases.flatMap((alias) => graph.referencesForCategory(alias)));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function semanticNodesForCategoryContext(graph: CategoryGraph, context: CategoryDrilldownContext): SemanticObjectSummary[] {
|
|
69
|
+
return uniqueSemanticSummaries(
|
|
70
|
+
context.aliases.flatMap((alias) => graph.neighbors("category", alias))
|
|
71
|
+
.flatMap((group) => group.objects)
|
|
72
|
+
.filter((object) => object.kind === "semanticNode"),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function uniqueNeighborGroupsForCategoryContext(graph: CategoryGraph, context: CategoryDrilldownContext): SemanticNeighborGroup[] {
|
|
77
|
+
const groups = new Map<string, SemanticNeighborGroup>();
|
|
78
|
+
for (const alias of context.aliases) {
|
|
79
|
+
for (const group of graph.neighbors("category", alias)) {
|
|
80
|
+
const key = `${group.direction}#${group.predicate}`;
|
|
81
|
+
const current = groups.get(key);
|
|
82
|
+
if (!current) {
|
|
83
|
+
groups.set(key, { ...group, relations: [...group.relations], objects: [...group.objects] });
|
|
84
|
+
} else {
|
|
85
|
+
current.relations = uniqueRelations([...current.relations, ...group.relations]);
|
|
86
|
+
current.objects = uniqueSemanticSummaries([...current.objects, ...group.objects]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return Array.from(groups.values());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function topicHref(rootCategoryKey: string, selectedCategoryKey?: string | null): string {
|
|
94
|
+
const encodedRoot = encodeURIComponent(rootCategoryKey);
|
|
95
|
+
const encodedSelected = selectedCategoryKey && selectedCategoryKey !== rootCategoryKey ? `/${encodeURIComponent(selectedCategoryKey)}` : "";
|
|
96
|
+
return `/newsroom/topics/${encodedRoot}${encodedSelected}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function categoryDrilldownHref(section: "references" | "concepts", categoryKey: string): string {
|
|
100
|
+
return `/newsroom/${section}/${encodeURIComponent(categoryKey)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function matchesCategorySelection(category: CategorySteeringCategory, selection: string): boolean {
|
|
104
|
+
return category.id === selection || category.lineageId === selection || category.categoryKey === selection;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function uniqueCategoryRecords(categories: CategorySteeringCategory[]): CategorySteeringCategory[] {
|
|
108
|
+
const records = new Map<string, CategorySteeringCategory>();
|
|
109
|
+
for (const category of categories) {
|
|
110
|
+
records.set(category.id, category);
|
|
111
|
+
}
|
|
112
|
+
return Array.from(records.values());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function categoryAliasValues(categories: CategorySteeringCategory[]): string[] {
|
|
116
|
+
const aliases = new Set<string>();
|
|
117
|
+
for (const category of categories) {
|
|
118
|
+
aliases.add(category.id);
|
|
119
|
+
aliases.add(category.categoryKey);
|
|
120
|
+
if (category.lineageId) aliases.add(category.lineageId);
|
|
121
|
+
}
|
|
122
|
+
return Array.from(aliases).filter(Boolean);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function uniqueRelations(relations: SemanticRelationRecord[]): SemanticRelationRecord[] {
|
|
126
|
+
return Array.from(new Map(relations.map((relation) => [relation.id, relation])).values());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function uniqueSemanticSummaries(objects: SemanticObjectSummary[]): SemanticObjectSummary[] {
|
|
130
|
+
const map = new Map<string, SemanticObjectSummary>();
|
|
131
|
+
for (const object of objects) map.set(`${object.kind}#${object.lineageId}`, object);
|
|
132
|
+
return Array.from(map.values()).sort((left, right) => left.label.localeCompare(right.label));
|
|
133
|
+
}
|